Skip to content

Commit

Permalink
fix: Rotate OAuth token when expired
Browse files Browse the repository at this point in the history
  • Loading branch information
jpd4emis committed Sep 3, 2024
1 parent ac46701 commit c27e39f
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 42 deletions.
133 changes: 133 additions & 0 deletions src/auth/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { mocked } from "jest-mock";
import { mockServices } from '@backstage/backend-test-utils';

import { getAuthToken, loadAuthConfig } from './auth';
import { RootConfigService } from "@backstage/backend-plugin-api";

global.fetch = jest.fn() as jest.Mock;

function mockedResponse(status: number, body: any): Promise<Response> {
return Promise.resolve({
json: () => Promise.resolve(body),
status
} as Response);
}

describe('PagerDuty Auth', () => {
const logger = mockServices.rootLogger();
let config : RootConfigService;

beforeAll(() => {
jest.useFakeTimers();
});

describe('getAuthToken', () => {
config = mockServices.rootConfig({
data: {
pagerDuty: {
oauth: {
clientId: 'foobar',
clientSecret: 'super-secret-wow',
subDomain: 'EU',
}

}
}
});

it('Get token with legacy OAuth config', async () => {
mocked(fetch).mockReturnValue(
mockedResponse(200, { access_token: 'sometoken', token_type: "bearer", expires_in: 86400 })
);
jest.setSystemTime(new Date(2024, 9, 1, 9, 0));
await loadAuthConfig(config, logger);

const result = await getAuthToken();
expect(result).toEqual('Bearer sometoken');
});

it('Get token with account OAuth config', async () => {
config = mockServices.rootConfig({
data: {
pagerDuty: {
accounts: [
{
id: 'test1',
oauth: {
clientId: 'foobar',
clientSecret: 'super-secret-wow',
subDomain: 'EU',
}
}
]
}
}
});
mocked(fetch).mockReturnValue(
mockedResponse(200, { access_token: 'sometoken', token_type: "bearer", expires_in: 86400 })
);
jest.setSystemTime(new Date(2024, 9, 1, 9, 0));
await loadAuthConfig(config, logger);

const defaultResult = await getAuthToken();
expect(defaultResult).toEqual('Bearer sometoken');
const accountResult = await getAuthToken('test1');
expect(accountResult).toEqual('Bearer sometoken');
});

it('Get refreshed token with legacy OAuth config', async () => {
mocked(fetch).mockReturnValueOnce(
mockedResponse(200, { access_token: 'sometoken1', token_type: "bearer", expires_in: 86400 })
);
mocked(fetch).mockReturnValueOnce(
mockedResponse(200, { access_token: 'sometoken2', token_type: "bearer", expires_in: 86400 })
);
jest.setSystemTime(new Date(2024, 9, 1, 9, 0));
await loadAuthConfig(config, logger);

const before = await getAuthToken();
expect(before).toEqual('Bearer sometoken1');

jest.setSystemTime(new Date(2024, 9, 2, 9, 1));
const result = await getAuthToken();
expect(result).toEqual('Bearer sometoken2');
});

it('Get legacy token', async () => {
config = mockServices.rootConfig({
data: {
pagerDuty: {
apiToken: 'some-api-token',
}
}
});
await loadAuthConfig(config, logger);

const result = await getAuthToken();
expect(result).toEqual('Token token=some-api-token');
});

it('Get account token', async () => {
config = mockServices.rootConfig({
data: {
pagerDuty: {
accounts: [
{
id: 'test2',
apiToken: 'some-api-token',
}
]
}
}
});
await loadAuthConfig(config, logger);

const defaultResult = await getAuthToken();
expect(defaultResult).toEqual('Token token=some-api-token');
const accountResult = await getAuthToken('test2');
expect(accountResult).toEqual('Token token=some-api-token');
const noResult = await getAuthToken('test1');
expect(noResult).toEqual('');
});
});
});
64 changes: 22 additions & 42 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,54 +16,34 @@ type Auth = {
let authPersistence: Auth;
let isLegacyConfig = false;

export async function getAuthToken(accountId? : string): Promise<string> {
async function checkForOAuthToken(tokenId: string): Promise<boolean> {
if (authPersistence.accountTokens[tokenId]?.authToken !== '' &&
authPersistence.accountTokens[tokenId]?.authToken.includes('Bearer')) {
if (authPersistence.accountTokens[tokenId].authTokenExpiryDate > Date.now()) {
return true
}
authPersistence.logger.info('OAuth token expired, renewing');
await loadAuthConfig(authPersistence.config, authPersistence.logger);
return authPersistence.accountTokens[tokenId].authTokenExpiryDate > Date.now()
}
return false
}

export async function getAuthToken(accountId? : string): Promise<string> {
// if authPersistence is not initialized, load the auth config
if (!authPersistence?.accountTokens) {
await loadAuthConfig(authPersistence.config, authPersistence.logger);
}

if(isLegacyConfig){
if (
(authPersistence.accountTokens.default.authToken !== '' &&
authPersistence.accountTokens.default.authToken.includes('Bearer') &&
authPersistence.accountTokens.default.authTokenExpiryDate > Date.now()) // case where OAuth token is still valid
||
(authPersistence.accountTokens.default.authToken !== '' &&
authPersistence.accountTokens.default.authToken.includes('Token'))) { // case where API token is used

if (isLegacyConfig && authPersistence.accountTokens.default.authToken !== ''
&& (await checkForOAuthToken('default') || authPersistence.accountTokens.default.authToken.includes('Token'))) {
return authPersistence.accountTokens.default.authToken;
}
}
else {
// check if accountId is provided
if (accountId && accountId !== '') {
if (
(authPersistence.accountTokens[accountId].authToken !== '' &&
authPersistence.accountTokens[accountId].authToken.includes('Bearer') &&
authPersistence.accountTokens[accountId].authTokenExpiryDate > Date.now()) // case where OAuth token is still valid
||
(authPersistence.accountTokens[accountId].authToken !== '' &&
authPersistence.accountTokens[accountId].authToken.includes('Token'))) { // case where API token is used

return authPersistence.accountTokens[accountId].authToken;
}
}

else { // return default account token if accountId is not provided
const defaultFallback = authPersistence.defaultAccount ?? "";
const key = accountId && accountId !== '' ? accountId : authPersistence.defaultAccount ?? '';

if (
(authPersistence.accountTokens[defaultFallback].authToken !== '' &&
authPersistence.accountTokens[defaultFallback].authToken.includes('Bearer') &&
authPersistence.accountTokens[defaultFallback].authTokenExpiryDate > Date.now()) // case where OAuth token is still valid
||
(authPersistence.accountTokens[defaultFallback].authToken !== '' &&
authPersistence.accountTokens[defaultFallback].authToken.includes('Token'))) { // case where API token is used

return authPersistence.accountTokens[defaultFallback].authToken;
}
}
if (authPersistence.accountTokens[key]?.authToken !== ''
&& (await checkForOAuthToken(key) || authPersistence.accountTokens[key]?.authToken.includes('Token'))) {
return authPersistence.accountTokens[key].authToken;
}

return '';
Expand Down Expand Up @@ -119,15 +99,15 @@ export async function loadAuthConfig(config : RootConfigService, logger: LoggerS
else { // new accounts config is present
logger.info('New PagerDuty accounts configuration found in config file.');
isLegacyConfig = false;
const accounts = config.getOptional<PagerDutyAccountConfig[]>('pagerDuty.accounts');
const accounts = config.getOptional<PagerDutyAccountConfig[]>('pagerDuty.accounts') || [];


if(accounts && accounts?.length === 1){
logger.info('Only one account found in config file. Setting it as default.');
authPersistence.defaultAccount = accounts[0].id;
}

accounts?.forEach(async account => {
await Promise.all(accounts.map(async account => {
const maskedAccountId = maskString(account.id);

if(account.isDefault && !authPersistence.defaultAccount){
Expand Down Expand Up @@ -161,7 +141,7 @@ export async function loadAuthConfig(config : RootConfigService, logger: LoggerS

logger.info(`PagerDuty API token loaded successfully for account ${maskedAccountId}.`);
}
});
}));

if(!authPersistence.defaultAccount){
logger.error('No default account found in config file. One account must be marked as default.');
Expand Down

0 comments on commit c27e39f

Please sign in to comment.