Skip to content

Commit c868376

Browse files
feat: Added OAuth Auth Code Flow for GH Codespaces (#223)
1 parent 2161da8 commit c868376

File tree

5 files changed

+210
-2
lines changed

5 files changed

+210
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
## Unreleased Changes
44

5+
- Fixed OAuth callback URL to work in GitHub Codespaces by detecting the environment and using the forwarded URL instead of localhost
6+
- Breaking: Changed default HTTP server host binding from `0.0.0.0` to `127.0.0.1` for improved security
7+
58
### Scopes
69

710
- Removed scope `app-engine:functions:run` as it's not needed
8-
- Breaking: Changed default HTTP server host binding from `0.0.0.0` to `127.0.0.1` for improved security
911

1012
## 0.11.0
1113

src/authentication/dynatrace-oauth-auth-code-flow.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,50 @@ describe('OAuth Authorization Code Flow', () => {
5757
// Clean up
5858
result.server.close();
5959
});
60+
61+
test('startOAuthRedirectServer uses forwarded URL in GitHub Codespaces', async () => {
62+
const originalEnv = process.env;
63+
64+
try {
65+
// Mock Codespaces environment variables
66+
process.env.CODESPACE_NAME = 'test-codespace';
67+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = 'app.github.dev';
68+
69+
const port = 8080;
70+
const result = await startOAuthRedirectServer(port);
71+
72+
expect(result.redirectUri).toBe('https://test-codespace-8080.app.github.dev/auth/login');
73+
expect(result.server).toBeDefined();
74+
expect(result.waitForAuthorizationCode).toBeDefined();
75+
76+
// Clean up
77+
result.server.close();
78+
} finally {
79+
// Restore original environment
80+
process.env = originalEnv;
81+
}
82+
});
83+
84+
test('startOAuthRedirectServer falls back to localhost when not in Codespaces', async () => {
85+
const originalEnv = process.env;
86+
87+
try {
88+
// Ensure Codespaces environment variables are not set
89+
delete process.env.CODESPACE_NAME;
90+
delete process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN;
91+
92+
const port = 8080;
93+
const result = await startOAuthRedirectServer(port);
94+
95+
expect(result.redirectUri).toBe('http://localhost:8080/auth/login');
96+
expect(result.server).toBeDefined();
97+
expect(result.waitForAuthorizationCode).toBeDefined();
98+
99+
// Clean up
100+
result.server.close();
101+
} finally {
102+
// Restore original environment
103+
process.env = originalEnv;
104+
}
105+
});
60106
});

src/authentication/dynatrace-oauth-auth-code-flow.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { OAuthAuthorizationConfig, OAuthAuthorizationResult, OAuthTokenResponse
55
import { requestOAuthToken } from './dynatrace-oauth-base';
66
import { base64URLEncode, generateRandomState } from './utils';
77
import open from 'open';
8+
import { getCodespacesForwardedUrl } from '../utils/environment-detection';
89

910
/**
1011
* Generates PKCE code verifier and challenge according to RFC 7636
@@ -101,7 +102,9 @@ export async function startOAuthRedirectServer(port: number = 5344): Promise<{
101102
redirectUri: string;
102103
waitForAuthorizationCode: () => Promise<{ code: string; state: string }>;
103104
}> {
104-
const redirectUri = `http://localhost:${port}/auth/login`;
105+
// Check if we're running in GitHub Codespaces and use forwarded URL if so
106+
const forwardedUrl = getCodespacesForwardedUrl(port);
107+
const redirectUri = forwardedUrl ? `${forwardedUrl}/auth/login` : `http://localhost:${port}/auth/login`;
105108

106109
let resolveAuthCode: (value: { code: string; state: string }) => void;
107110
let rejectAuthCode: (error: Error) => void;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { isRunningInCodespaces, getCodespacesForwardedUrl } from './environment-detection';
2+
3+
describe('isRunningInCodespaces', () => {
4+
const originalEnv = process.env;
5+
6+
beforeEach(() => {
7+
// Reset environment variables before each test
8+
process.env = { ...originalEnv };
9+
});
10+
11+
afterEach(() => {
12+
// Restore original environment after each test
13+
process.env = originalEnv;
14+
});
15+
16+
it('should return true when both CODESPACE_NAME and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN are set', () => {
17+
process.env.CODESPACE_NAME = 'my-codespace';
18+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = 'app.github.dev';
19+
20+
expect(isRunningInCodespaces()).toBe(true);
21+
});
22+
23+
it('should return false when CODESPACE_NAME is missing', () => {
24+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = 'app.github.dev';
25+
// CODESPACE_NAME is not set
26+
27+
expect(isRunningInCodespaces()).toBe(false);
28+
});
29+
30+
it('should return false when GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN is missing', () => {
31+
process.env.CODESPACE_NAME = 'my-codespace';
32+
// GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN is not set
33+
34+
expect(isRunningInCodespaces()).toBe(false);
35+
});
36+
37+
it('should return false when both environment variables are missing', () => {
38+
// Neither CODESPACE_NAME nor GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN are set
39+
40+
expect(isRunningInCodespaces()).toBe(false);
41+
});
42+
43+
it('should return false when CODESPACE_NAME is empty string', () => {
44+
process.env.CODESPACE_NAME = '';
45+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = 'app.github.dev';
46+
47+
expect(isRunningInCodespaces()).toBe(false);
48+
});
49+
50+
it('should return false when GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN is empty string', () => {
51+
process.env.CODESPACE_NAME = 'my-codespace';
52+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = '';
53+
54+
expect(isRunningInCodespaces()).toBe(false);
55+
});
56+
});
57+
58+
describe('getCodespacesForwardedUrl', () => {
59+
const originalEnv = process.env;
60+
61+
beforeEach(() => {
62+
// Reset environment variables before each test
63+
process.env = { ...originalEnv };
64+
});
65+
66+
afterEach(() => {
67+
// Restore original environment after each test
68+
process.env = originalEnv;
69+
});
70+
71+
it('should return the correct forwarded URL when running in Codespaces', () => {
72+
process.env.CODESPACE_NAME = 'my-codespace';
73+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = 'app.github.dev';
74+
75+
const result = getCodespacesForwardedUrl(8080);
76+
77+
expect(result).toBe('https://my-codespace-8080.app.github.dev');
78+
});
79+
80+
it('should return null when not running in Codespaces', () => {
81+
// Neither environment variable is set
82+
83+
const result = getCodespacesForwardedUrl(8080);
84+
85+
expect(result).toBeNull();
86+
});
87+
88+
it('should return null when CODESPACE_NAME is missing', () => {
89+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = 'app.github.dev';
90+
91+
const result = getCodespacesForwardedUrl(8080);
92+
93+
expect(result).toBeNull();
94+
});
95+
96+
it('should return null when GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN is missing', () => {
97+
process.env.CODESPACE_NAME = 'my-codespace';
98+
99+
const result = getCodespacesForwardedUrl(8080);
100+
101+
expect(result).toBeNull();
102+
});
103+
104+
it('should handle different port numbers correctly', () => {
105+
process.env.CODESPACE_NAME = 'test-codespace';
106+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = 'preview.app.github.dev';
107+
108+
expect(getCodespacesForwardedUrl(3000)).toBe('https://test-codespace-3000.preview.app.github.dev');
109+
expect(getCodespacesForwardedUrl(5344)).toBe('https://test-codespace-5344.preview.app.github.dev');
110+
expect(getCodespacesForwardedUrl(9999)).toBe('https://test-codespace-9999.preview.app.github.dev');
111+
});
112+
113+
it('should handle codespace names with special characters', () => {
114+
process.env.CODESPACE_NAME = 'my-special_codespace.v1';
115+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = 'app.github.dev';
116+
117+
const result = getCodespacesForwardedUrl(8080);
118+
119+
expect(result).toBe('https://my-special_codespace.v1-8080.app.github.dev');
120+
});
121+
122+
it('should handle different domain formats', () => {
123+
process.env.CODESPACE_NAME = 'codespace-123';
124+
process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = 'githubpreview.dev';
125+
126+
const result = getCodespacesForwardedUrl(4000);
127+
128+
expect(result).toBe('https://codespace-123-4000.githubpreview.dev');
129+
});
130+
});

src/utils/environment-detection.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Utility functions for detecting and handling different runtime environments
3+
*/
4+
5+
/**
6+
* Detects if the application is running in GitHub Codespaces
7+
* @returns true if running in Codespaces, false otherwise
8+
*/
9+
export function isRunningInCodespaces(): boolean {
10+
return !!(process.env.CODESPACE_NAME && process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN);
11+
}
12+
13+
/**
14+
* Gets the forwarded URL for OAuth redirect in GitHub Codespaces
15+
* @param port The port number to use in the forwarded URL
16+
* @returns The forwarded URL if in Codespaces, null otherwise
17+
*/
18+
export function getCodespacesForwardedUrl(port: number): string | null {
19+
if (!isRunningInCodespaces()) {
20+
return null;
21+
}
22+
23+
const codespaceName = process.env.CODESPACE_NAME!;
24+
const domain = process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN!;
25+
26+
return `https://${codespaceName}-${port}.${domain}`;
27+
}

0 commit comments

Comments
 (0)