Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Rotate OAuth token when expired #88

Open
wants to merge 2 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading