Skip to content

Commit 628792f

Browse files
authored
chore: get auth logic in sync with the VSCE (#2311)
1 parent b59b49c commit 628792f

File tree

4 files changed

+110
-73
lines changed

4 files changed

+110
-73
lines changed

packages/cli/src/auth/device-flow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type Credentials = {
88
access_token: string;
99
refresh_token: string;
1010
expires_in: number;
11-
residency?: string;
11+
residency?: string; // injected by RedoclyOAuthDeviceFlow to preserve credentials with custom residency
1212
token_type?: string; // from login response
1313
};
1414

packages/cli/src/auth/oauth-client.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import crypto from 'node:crypto';
55
import { Buffer } from 'node:buffer';
66
import { logger } from '@redocly/openapi-core';
77
import { type Credentials, RedoclyOAuthDeviceFlow } from './device-flow.js';
8+
import { isValidReuniteUrl } from '../reunite/api/domains.js';
89

9-
const CREDENTIAL_SALT = '4618dbc9-8aed-4e27-aaf0-225f4603e5a4';
10+
const CREDENTIALS_SALT = '4618dbc9-8aed-4e27-aaf0-225f4603e5a4';
1011
const CRYPTO_ALGORITHM = 'aes-256-cbc';
1112

1213
export class RedoclyOAuthClient {
@@ -24,7 +25,7 @@ export class RedoclyOAuthClient {
2425
this.credentialsFileName = 'credentials';
2526
this.credentialsFilePath = path.join(this.credentialsFolderPath, this.credentialsFileName);
2627

27-
this.key = crypto.createHash('sha256').update(`${homeDirPath}${CREDENTIAL_SALT}`).digest();
28+
this.key = crypto.createHash('sha256').update(`${homeDirPath}${CREDENTIALS_SALT}`).digest();
2829
this.iv = crypto.createHash('md5').update(homeDirPath).digest();
2930

3031
mkdirSync(this.credentialsFolderPath, { recursive: true });
@@ -63,7 +64,11 @@ export class RedoclyOAuthClient {
6364
const deviceFlow = new RedoclyOAuthDeviceFlow(reuniteUrl);
6465
const credentials = await this.readCredentials();
6566

66-
if (!credentials) {
67+
if (
68+
!credentials ||
69+
!isValidReuniteUrl(reuniteUrl) ||
70+
(credentials.residency && credentials.residency !== reuniteUrl)
71+
) {
6772
return null;
6873
}
6974

Lines changed: 91 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,120 @@
11
import { type Config, createConfig } from '@redocly/openapi-core';
2-
import { getDomain } from '../domains.js';
2+
import { getDomain, isValidReuniteUrl } from '../domains.js';
33
import { getReuniteUrl } from '../domains.js';
44

5-
describe('getDomain()', () => {
6-
afterEach(() => {
7-
delete process.env.REDOCLY_DOMAIN;
8-
});
5+
describe('domains', () => {
6+
describe('getDomain()', () => {
7+
afterEach(() => {
8+
delete process.env.REDOCLY_DOMAIN;
9+
});
910

10-
it('should return the domain from environment variable', () => {
11-
process.env.REDOCLY_DOMAIN = 'test-domain';
11+
it('should return the domain from environment variable', () => {
12+
process.env.REDOCLY_DOMAIN = 'test-domain';
1213

13-
expect(getDomain()).toBe('test-domain');
14-
});
14+
expect(getDomain()).toBe('test-domain');
15+
});
1516

16-
it('should return the default domain if no domain provided', () => {
17-
process.env.REDOCLY_DOMAIN = '';
17+
it('should return the default domain if no domain provided', () => {
18+
process.env.REDOCLY_DOMAIN = '';
1819

19-
expect(getDomain()).toBe('https://app.cloud.redocly.com');
20+
expect(getDomain()).toBe('https://app.cloud.redocly.com');
21+
});
2022
});
21-
});
2223

23-
describe('getReuniteUrl()', () => {
24-
let testConfig: Config;
25-
beforeAll(async () => {
26-
testConfig = await createConfig();
27-
});
24+
describe('getReuniteUrl()', () => {
25+
let testConfig: Config;
26+
beforeAll(async () => {
27+
testConfig = await createConfig();
28+
});
2829

29-
it('should return US API URL when US region specified', () => {
30-
expect(getReuniteUrl(testConfig, 'us')).toBe('https://app.cloud.redocly.com');
31-
});
30+
it('should return US API URL when US region specified', () => {
31+
expect(getReuniteUrl(testConfig, 'us')).toBe('https://app.cloud.redocly.com');
32+
});
3233

33-
it('should return EU API URL when EU region specified', () => {
34-
expect(getReuniteUrl(testConfig, 'eu')).toBe('https://app.cloud.eu.redocly.com');
35-
});
34+
it('should return EU API URL when EU region specified', () => {
35+
expect(getReuniteUrl(testConfig, 'eu')).toBe('https://app.cloud.eu.redocly.com');
36+
});
3637

37-
it('should return custom domain API URL when custom domain specified', () => {
38-
const customDomain = 'https://custom.domain.com';
39-
expect(getReuniteUrl(testConfig, customDomain)).toBe('https://custom.domain.com');
40-
});
38+
it('should return custom domain API URL when custom domain specified', () => {
39+
const customDomain = 'https://custom.domain.com';
40+
expect(getReuniteUrl(testConfig, customDomain)).toBe('https://custom.domain.com');
41+
});
4142

42-
it('should return US API URL when no region specified', () => {
43-
expect(getReuniteUrl(testConfig)).toBe('https://app.cloud.redocly.com');
44-
});
43+
it('should return US API URL when no region specified', () => {
44+
expect(getReuniteUrl(testConfig)).toBe('https://app.cloud.redocly.com');
45+
});
4546

46-
it('should use residency from config when no second parameter provided', async () => {
47-
const config = await createConfig({ residency: 'eu' });
48-
expect(getReuniteUrl(config)).toBe('https://app.cloud.eu.redocly.com');
49-
});
47+
it('should use residency from config when no second parameter provided', async () => {
48+
const config = await createConfig({ residency: 'eu' });
49+
expect(getReuniteUrl(config)).toBe('https://app.cloud.eu.redocly.com');
50+
});
5051

51-
it('should use fromProjectUrl from config when no residency provided', async () => {
52-
const config = await createConfig({
53-
scorecard: { fromProjectUrl: 'https://app.cloud.eu.redocly.com/org/test/project/test' },
52+
it('should use fromProjectUrl from config when no residency provided', async () => {
53+
const config = await createConfig({
54+
scorecard: { fromProjectUrl: 'https://app.cloud.eu.redocly.com/org/test/project/test' },
55+
});
56+
expect(getReuniteUrl(config)).toBe('https://app.cloud.eu.redocly.com');
5457
});
55-
expect(getReuniteUrl(config)).toBe('https://app.cloud.eu.redocly.com');
56-
});
5758

58-
it('should prioritize second parameter over config residency and fromProjectUrl', async () => {
59-
const config = await createConfig({
60-
residency: 'us',
61-
scorecard: { fromProjectUrl: 'https://app.cloud.eu.redocly.com/org/test/project/test' },
59+
it('should prioritize second parameter over config residency and fromProjectUrl', async () => {
60+
const config = await createConfig({
61+
residency: 'us',
62+
scorecard: { fromProjectUrl: 'https://app.cloud.eu.redocly.com/org/test/project/test' },
63+
});
64+
expect(getReuniteUrl(config, 'eu')).toBe('https://app.cloud.eu.redocly.com');
6265
});
63-
expect(getReuniteUrl(config, 'eu')).toBe('https://app.cloud.eu.redocly.com');
64-
});
6566

66-
it('should prioritize residency over fromProjectUrl when both are provided', async () => {
67-
const config = await createConfig({
68-
residency: 'us',
69-
scorecard: { fromProjectUrl: 'https://app.cloud.eu.redocly.com/org/test/project/test' },
67+
it('should prioritize residency over fromProjectUrl when both are provided', async () => {
68+
const config = await createConfig({
69+
residency: 'us',
70+
scorecard: { fromProjectUrl: 'https://app.cloud.eu.redocly.com/org/test/project/test' },
71+
});
72+
expect(getReuniteUrl(config)).toBe('https://app.cloud.redocly.com');
7073
});
71-
expect(getReuniteUrl(config)).toBe('https://app.cloud.redocly.com');
72-
});
7374

74-
it('should handle custom domain from second parameter', async () => {
75-
const config = await createConfig({ residency: 'us' });
76-
expect(getReuniteUrl(config, 'https://custom.redocly.com')).toBe('https://custom.redocly.com');
77-
});
75+
it('should handle custom domain from second parameter', async () => {
76+
const config = await createConfig({ residency: 'us' });
77+
expect(getReuniteUrl(config, 'https://custom.redocly.com')).toBe(
78+
'https://custom.redocly.com'
79+
);
80+
});
7881

79-
it('should handle fromProjectUrl with custom domain and port', async () => {
80-
const config = await createConfig({
81-
scorecard: { fromProjectUrl: 'https://custom.redocly.com:8080/org/test/project/test' },
82+
it('should handle fromProjectUrl with custom domain and port', async () => {
83+
const config = await createConfig({
84+
scorecard: { fromProjectUrl: 'https://custom.redocly.com:8080/org/test/project/test' },
85+
});
86+
expect(getReuniteUrl(config)).toBe('https://custom.redocly.com:8080');
87+
});
88+
89+
it('should throw error for invalid residency or malformed URL', async () => {
90+
const config = await createConfig({ residency: 'invalid-region' });
91+
expect(() => getReuniteUrl(config)).toThrow('Invalid Reunite URL');
92+
expect(() => getReuniteUrl(testConfig, 'not-a-valid-url')).toThrow('Invalid Reunite URL');
8293
});
83-
expect(getReuniteUrl(config)).toBe('https://custom.redocly.com:8080');
84-
});
8594

86-
it('should throw error for invalid residency or malformed URL', async () => {
87-
const config = await createConfig({ residency: 'invalid-region' });
88-
expect(() => getReuniteUrl(config)).toThrow('Invalid Reunite URL');
89-
expect(() => getReuniteUrl(testConfig, 'not-a-valid-url')).toThrow('Invalid Reunite URL');
95+
it('should throw error for invalid fromProjectUrl', async () => {
96+
const config = await createConfig({
97+
scorecard: { fromProjectUrl: 'not-a-valid-url' },
98+
});
99+
expect(() => getReuniteUrl(config)).toThrow('Invalid Reunite URL');
100+
});
90101
});
91102

92-
it('should throw error for invalid fromProjectUrl', async () => {
93-
const config = await createConfig({
94-
scorecard: { fromProjectUrl: 'not-a-valid-url' },
103+
describe('isValidReuniteUrl', () => {
104+
it('should return true for default config', async () => {
105+
expect(isValidReuniteUrl('us')).toBe(true);
106+
});
107+
108+
it('should return true when residency is a valid URL', async () => {
109+
expect(isValidReuniteUrl('https://app.cloud.cba.redocly.com')).toBe(true);
110+
});
111+
112+
it('should return false when residency is not a valid URL', async () => {
113+
expect(isValidReuniteUrl('not-a-valid-url')).toBe(false);
114+
});
115+
116+
it('should return false when residency has an invalid protocol', async () => {
117+
expect(isValidReuniteUrl('http:app.cloud.redocly.com')).toBe(false);
95118
});
96-
expect(() => getReuniteUrl(config)).toThrow('Invalid Reunite URL');
97119
});
98120
});

packages/cli/src/reunite/api/domains.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ export const getReuniteUrl = withHttpsValidation(
3333
}
3434
);
3535

36+
export function isValidReuniteUrl(reuniteUrl: string): boolean {
37+
try {
38+
getReuniteUrl(undefined, reuniteUrl);
39+
} catch {
40+
return false;
41+
}
42+
43+
return true;
44+
}
45+
3646
function withHttpsValidation<Fn extends (...args: any[]) => string>(fn: Fn) {
3747
return (...args: Parameters<Fn>) => {
3848
const url = fn(...args);

0 commit comments

Comments
 (0)