From 00b78fa4500431de883d1d7e1ece4ff38a05012c Mon Sep 17 00:00:00 2001 From: Stan Dzhumaev Date: Thu, 13 Feb 2025 10:33:04 -0800 Subject: [PATCH] [AXON-46] chore: add unit tests for auth strategies (to avoid creating problems when changing this code) --- src/atlclients/strategy.test.ts | 103 +++++++++++++++++++++++++++++++ src/atlclients/strategy.ts | 24 ++----- src/atlclients/strategyCrypto.ts | 20 ++++++ 3 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 src/atlclients/strategy.test.ts create mode 100644 src/atlclients/strategyCrypto.ts diff --git a/src/atlclients/strategy.test.ts b/src/atlclients/strategy.test.ts new file mode 100644 index 00000000..b5e01c0c --- /dev/null +++ b/src/atlclients/strategy.test.ts @@ -0,0 +1,103 @@ +// mock crypto.randomBytes +jest.mock('./strategyCrypto', () => { + return { + createVerifier: jest.fn(() => 'verifier'), + base64URLEncode: jest.fn(() => 'base64URLEncode'), + sha256: jest.fn(() => 'sha256'), + basicAuth: jest.fn(() => 'basicAuth'), + }; +}); + +import { OAuthProvider } from './authInfo'; +import { strategyForProvider } from './strategy'; + +const expectedData = { + bbcloud: { + provider: 'bbcloud', + authorizeUrl: + 'https://bitbucket.org/site/oauth2/authorize?client_id=3hasX42a7Ugka2FJja&response_type=code&state=state', + accessibleResourcesUrl: '', + tokenAuthorizationData: 'grant_type=authorization_code&code=code', + tokenUrl: 'https://bitbucket.org/site/oauth2/access_token', + apiUrl: 'https://bitbucket.org', + refreshHeaders: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'basicAuth', + }, + tokenRefreshData: 'grant_type=refresh_token&refresh_token=refreshToken', + profileUrl: 'https://api.bitbucket.org/2.0/user', + emailsUrl: 'https://api.bitbucket.org/2.0/user/emails', + }, + bbcloudstaging: { + provider: 'bbcloudstaging', + authorizeUrl: + 'https://staging.bb-inf.net/site/oauth2/authorize?client_id=7jspxC7fgemuUbnWQL&response_type=code&state=state', + accessibleResourcesUrl: '', + tokenAuthorizationData: 'grant_type=authorization_code&code=code', + tokenUrl: 'https://staging.bb-inf.net/site/oauth2/access_token', + apiUrl: 'https://staging.bb-inf.net', + refreshHeaders: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'basicAuth', + }, + tokenRefreshData: 'grant_type=refresh_token&refresh_token=refreshToken', + profileUrl: 'https://api-staging.bb-inf.net/2.0/user', + emailsUrl: 'https://api-staging.bb-inf.net/2.0/user/emails', + }, + jiracloud: { + provider: 'jiracloud', + authorizeUrl: + 'https://auth.atlassian.com/authorize?client_id=bJChVgBQd0aNUPuFZ8YzYBVZz3X4QTe2&redirect_uri=http%3A%2F%2F127.0.0.1%3A31415%2Fjiracloud&response_type=code&scope=read%3Ajira-user+read%3Ajira-work+write%3Ajira-work+offline_access+manage%3Ajira-project&audience=api.atlassian.com&prompt=consent&state=state&code_challenge=base64URLEncode&code_challenge_method=S256', + accessibleResourcesUrl: 'https://api.atlassian.com/oauth/token/accessible-resources', + tokenAuthorizationData: + '{"grant_type":"authorization_code","code":"code","redirect_uri":"http://127.0.0.1:31415/jiracloud","client_id":"bJChVgBQd0aNUPuFZ8YzYBVZz3X4QTe2","code_verifier":"verifier"}', + tokenUrl: 'https://auth.atlassian.com/oauth/token', + apiUrl: 'api.atlassian.com', + refreshHeaders: { + 'Content-Type': 'application/json', + }, + tokenRefreshData: + '{"grant_type":"refresh_token","client_id":"bJChVgBQd0aNUPuFZ8YzYBVZz3X4QTe2","refresh_token":"refreshToken"}', + profileUrl: '', + emailsUrl: '', + }, + jiracloudstaging: { + provider: 'jiracloudstaging', + authorizeUrl: + 'https://auth.stg.atlassian.com/authorize?client_id=pmzXmUav3Rr5XEL0Sie7Biec0WGU8BKg&redirect_uri=http%3A%2F%2F127.0.0.1%3A31415%2Fjiracloudstaging&response_type=code&scope=read%3Ajira-user+read%3Ajira-work+write%3Ajira-work+offline_access+manage%3Ajira-project&audience=api.stg.atlassian.com&prompt=consent&state=state&code_challenge=base64URLEncode&code_challenge_method=S256', + accessibleResourcesUrl: 'https://api.stg.atlassian.com/oauth/token/accessible-resources', + tokenAuthorizationData: + '{"grant_type":"authorization_code","code":"code","redirect_uri":"http://127.0.0.1:31415/jiracloudstaging","client_id":"pmzXmUav3Rr5XEL0Sie7Biec0WGU8BKg","code_verifier":"verifier"}', + tokenUrl: 'https://auth.stg.atlassian.com/oauth/token', + apiUrl: 'api.stg.atlassian.com', + refreshHeaders: { + 'Content-Type': 'application/json', + }, + tokenRefreshData: + '{"grant_type":"refresh_token","client_id":"pmzXmUav3Rr5XEL0Sie7Biec0WGU8BKg","refresh_token":"refreshToken"}', + profileUrl: '', + emailsUrl: '', + }, +}; + +describe('Authentication strategies', () => { + it.each([ + [OAuthProvider.BitbucketCloud], + [OAuthProvider.BitbucketCloudStaging], + [OAuthProvider.JiraCloud], + [OAuthProvider.JiraCloudStaging], + ])('Strategy for provider %s yields expected results', (provider: OAuthProvider) => { + const expected = expectedData[provider] as any; + const strategy = strategyForProvider(provider); + expect(strategy.provider()).toBe(expected.provider); + expect(strategy.authorizeUrl('state')).toBe(expected.authorizeUrl); + expect(strategy.accessibleResourcesUrl()).toBe(expected.accessibleResourcesUrl); + expect(strategy.tokenAuthorizationData('code')).toBe(expected.tokenAuthorizationData); + expect(strategy.tokenUrl()).toBe(expected.tokenUrl); + expect(strategy.apiUrl()).toBe(expected.apiUrl); + expect(strategy.refreshHeaders()).toStrictEqual(expected.refreshHeaders); + expect(strategy.tokenRefreshData('refreshToken')).toBe(expected.tokenRefreshData); + expect(strategy.profileUrl()).toBe(expected.profileUrl); + expect(strategy.emailsUrl()).toBe(expected.emailsUrl); + }); +}); diff --git a/src/atlclients/strategy.ts b/src/atlclients/strategy.ts index 1e3ce36b..cd9c3da5 100644 --- a/src/atlclients/strategy.ts +++ b/src/atlclients/strategy.ts @@ -1,5 +1,5 @@ import { OAuthProvider } from './authInfo'; -import crypto from 'crypto'; +import { createVerifier, base64URLEncode, sha256, basicAuth } from './strategyCrypto'; const JiraProdStrategyData = { clientID: 'bJChVgBQd0aNUPuFZ8YzYBVZz3X4QTe2', @@ -90,7 +90,7 @@ class PKCEJiraProdStrategy extends Strategy { public constructor() { super(); - this.verifier = base64URLEncode(crypto.randomBytes(32)); + this.verifier = createVerifier(); } public provider(): OAuthProvider { @@ -151,20 +151,12 @@ class PKCEJiraProdStrategy extends Strategy { } } -function base64URLEncode(str: Buffer): string { - return str.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -} - -function sha256(buffer: any) { - return crypto.createHash('sha256').update(buffer).digest(); -} - class PKCEJiraStagingStrategy extends Strategy { private verifier: string; public constructor() { super(); - this.verifier = base64URLEncode(crypto.randomBytes(32)); + this.verifier = createVerifier(); } public provider(): OAuthProvider { @@ -258,12 +250,9 @@ class BitbucketProdStrategy extends Strategy { // We kinda abuse refreshHeaders for bitbucket. Maybe have a authorizationHeaders as well? Just rename? public refreshHeaders() { - const basicAuth = Buffer.from( - `${BitbucketProdStrategyData.clientID}:${BitbucketProdStrategyData.clientSecret}`, - ).toString('base64'); return { 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${basicAuth}`, + Authorization: basicAuth(BitbucketProdStrategyData.clientID, BitbucketProdStrategyData.clientSecret), }; } @@ -311,12 +300,9 @@ class BitbucketStagingStrategy extends Strategy { } public refreshHeaders() { - const basicAuth = Buffer.from( - `${BitbucketStagingStrategyData.clientID}:${BitbucketStagingStrategyData.clientSecret}`, - ).toString('base64'); return { 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: `Basic ${basicAuth}`, + Authorization: basicAuth(BitbucketStagingStrategyData.clientID, BitbucketStagingStrategyData.clientSecret), }; } diff --git a/src/atlclients/strategyCrypto.ts b/src/atlclients/strategyCrypto.ts new file mode 100644 index 00000000..1755f9a2 --- /dev/null +++ b/src/atlclients/strategyCrypto.ts @@ -0,0 +1,20 @@ +import crypto from 'crypto'; + +// for some reason jest doesn't play nice with crypto, +// so these are now in a separate file for easy mocking + +export function createVerifier() { + return base64URLEncode(crypto.randomBytes(32)); +} + +export function base64URLEncode(str: Buffer): string { + return str.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +} + +export function sha256(buffer: any) { + return crypto.createHash('sha256').update(buffer).digest(); +} + +export function basicAuth(username: string, password: string) { + return 'Basic ' + Buffer.from(username + ':' + password).toString('base64'); +}