Skip to content

Commit 8dcea8a

Browse files
feat: Added OAuth Auth Code Flow for GH Codespaces
1 parent 4d19ec4 commit 8dcea8a

File tree

5 files changed

+209
-1
lines changed

5 files changed

+209
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
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+
57
## 0.11.0
68

79
- Fixed usage percentage to no longer be printed when no budget is set

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

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

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)