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 token type auth header #521

Merged
merged 4 commits into from
Sep 21, 2023
Merged
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
5 changes: 5 additions & 0 deletions .changeset/tasty-avocados-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@monokle/synchronizer": minor
---

Require token object instead of string for API queries
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions packages/synchronizer/src/__tests__/apiHandler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {assert} from 'chai';
import {ApiHandler} from '../handlers/apiHandler.js';

describe('ApiHandler Tests', () => {
describe('Authorization Header', () => {
it('formats header correctly (bearer)', async () => {
const synchronizer = new ApiHandler();
const header = (synchronizer as any).formatAuthorizationHeader({
accessToken: 'SAMPLE_TOKEN',
tokenType: 'Bearer',
});

assert.equal('Bearer SAMPLE_TOKEN', header);
});

it('formats header correctly (apikey)', async () => {
const synchronizer = new ApiHandler();
const header = (synchronizer as any).formatAuthorizationHeader({
accessToken: 'SAMPLE_TOKEN',
tokenType: 'ApiKey',
});

assert.equal('ApiKey SAMPLE_TOKEN', header);
});

it('formats header correctly (invalid)', async () => {
const synchronizer = new ApiHandler();
const header = (synchronizer as any).formatAuthorizationHeader({
accessToken: 'SAMPLE_TOKEN',
tokenType: 'invalid',
});

assert.equal('Bearer SAMPLE_TOKEN', header);
});

it('formats header correctly (not set)', async () => {
const synchronizer = new ApiHandler();
const header = (synchronizer as any).formatAuthorizationHeader({
accessToken: 'SAMPLE_TOKEN',
tokenType: undefined,
});

assert.equal('Bearer SAMPLE_TOKEN', header);
});
});
});
8 changes: 4 additions & 4 deletions packages/synchronizer/src/__tests__/authenticator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parse, stringify } from 'yaml'
import {parse, stringify} from 'yaml';
import {dirname, resolve} from 'path';
import {fileURLToPath} from 'url';
import {rm, mkdir, cp, readFile, writeFile} from 'fs/promises';
Expand Down Expand Up @@ -44,7 +44,7 @@ describe('Authenticator Tests', () => {
assert.isTrue(user.isAuthenticated);
assert.equal(user.email, '[email protected]');
assert.equal(user.token, 'USER1_ACCESS_TOKEN');
assert.equal(user.data?.auth?.token.token_type, 'access_token');
assert.equal(user.data?.auth?.token.token_type, 'ApiKey');
});

it('should return user data on init when auth file present (device code method)', async () => {
Expand Down Expand Up @@ -138,7 +138,7 @@ describe('Authenticator Tests', () => {
assert.isTrue(user.isAuthenticated);
assert.equal(user.email, '[email protected]');
assert.equal(user.token, 'USER3_ACCESS_TOKEN');
assert.equal(user.data?.auth?.token.token_type, 'access_token');
assert.equal(user.data?.auth?.token.token_type, 'ApiKey');
});

it('should login with device code', async () => {
Expand Down Expand Up @@ -267,7 +267,7 @@ describe('Authenticator Tests', () => {
assert.isTrue(userNew.isAuthenticated);
assert.equal(userNew.email, '[email protected]');
assert.equal(userNew.token, 'USER1_ACCESS_TOKEN');
assert.equal(userNew.data?.auth?.token.token_type, 'access_token');
assert.equal(userNew.data?.auth?.token.token_type, 'ApiKey');

assert.equal(deviceFlowRefreshSpy.callCount, 0);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ auth:
email: [email protected]
token:
access_token: 'USER1_ACCESS_TOKEN'
token_type: access_token
token_type: ApiKey
40 changes: 31 additions & 9 deletions packages/synchronizer/src/__tests__/synchronizer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ describe('Synchronizer Tests', () => {

assert.isFalse(policy.valid);

const newPolicy = await synchronizer.getPolicy(repoData, true, 'SAMPLE_ACCESS_TOKEN');
const newPolicy = await synchronizer.getPolicy(repoData, true, {
accessToken: 'SAMPLE_ACCESS_TOKEN',
tokenType: 'Bearer',
});

assert.isObject(newPolicy);
assert.isTrue(newPolicy.valid);
Expand Down Expand Up @@ -248,7 +251,10 @@ describe('Synchronizer Tests', () => {

assert.isFalse(policy.valid);

const newPolicy = await synchronizer.getPolicy(policyData, true, 'SAMPLE_ACCESS_TOKEN');
const newPolicy = await synchronizer.getPolicy(policyData, true, {
accessToken: 'SAMPLE_ACCESS_TOKEN',
tokenType: 'ApiKey',
});

assert.isObject(newPolicy);
assert.isTrue(newPolicy.valid);
Expand Down Expand Up @@ -361,7 +367,7 @@ describe('Synchronizer Tests', () => {
'Synchronize event not triggered'
);

await synchronizer.getPolicy(repoData, true, 'SAMPLE_ACCESS_TOKEN');
await synchronizer.getPolicy(repoData, true, {accessToken: 'SAMPLE_ACCESS_TOKEN', tokenType: 'Bearer'});

return result;
});
Expand Down Expand Up @@ -417,7 +423,10 @@ describe('Synchronizer Tests', () => {
name: 'monokle-core',
};

const projectInfo = await synchronizer.getProjectInfo(repoData, 'SAMPLE_ACCESS_TOKEN');
const projectInfo = await synchronizer.getProjectInfo(repoData, {
accessToken: 'SAMPLE_ACCESS_TOKEN',
tokenType: 'ApiKey',
});

assert.isObject(projectInfo);
assert.equal(projectInfo!.id, 6000);
Expand Down Expand Up @@ -454,7 +463,10 @@ describe('Synchronizer Tests', () => {
slug: 'user7-proj-foobar',
};

const projectInfo = await synchronizer.getProjectInfo(projectData, 'SAMPLE_ACCESS_TOKEN');
const projectInfo = await synchronizer.getProjectInfo(projectData, {
accessToken: 'SAMPLE_ACCESS_TOKEN',
tokenType: 'Bearer',
});

assert.isObject(projectInfo);
assert.equal(projectInfo!.id, 7000);
Expand Down Expand Up @@ -512,19 +524,29 @@ describe('Synchronizer Tests', () => {
name: 'monokle-core',
};

const projectInfo = await synchronizer.getProjectInfo(repoData, 'SAMPLE_ACCESS_TOKEN');
const projectInfo = await synchronizer.getProjectInfo(repoData, {
accessToken: 'SAMPLE_ACCESS_TOKEN',
tokenType: 'ApiKey',
});
assert.equal(projectInfo!.id, 6000);
assert.equal(queryApiStub.callCount, 1);

const projectInfoRetry = await synchronizer.getProjectInfo(repoData, 'SAMPLE_ACCESS_TOKEN');
const projectInfoRetry = await synchronizer.getProjectInfo(repoData, {
accessToken: 'SAMPLE_ACCESS_TOKEN',
tokenType: 'ApiKey',
});
assert.equal(projectInfoRetry!.id, 6000);
assert.equal(queryApiStub.callCount, 1);

const projectInfoRetryForce = await synchronizer.getProjectInfo(repoData, 'SAMPLE_ACCESS_TOKEN', true);
const projectInfoRetryForce = await synchronizer.getProjectInfo(
repoData,
{accessToken: 'SAMPLE_ACCESS_TOKEN', tokenType: 'ApiKey'},
true
);
assert.equal(projectInfoRetryForce!.id, 6000);
assert.equal(queryApiStub.callCount, 2);
});
})
});
});

async function createTmpConfigDir(copyPolicyFixture = '') {
Expand Down
29 changes: 19 additions & 10 deletions packages/synchronizer/src/handlers/apiHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import normalizeUrl from 'normalize-url';
import fetch from 'node-fetch';
import {SuppressionStatus} from '@monokle/types';
import {DEFAULT_API_URL} from '../constants.js';
import type {TokenInfo} from './storageHandlerAuth.js';

const getUserQuery = `
query getUser {
Expand Down Expand Up @@ -161,20 +162,20 @@ export class ApiHandler {
return normalizeUrl(this._apiUrl);
}

async getUser(accessToken: string): Promise<ApiUserData | undefined> {
return this.queryApi(getUserQuery, accessToken);
async getUser(tokenInfo: TokenInfo): Promise<ApiUserData | undefined> {
return this.queryApi(getUserQuery, tokenInfo);
}

async getProject(slug: string, accessToken: string): Promise<ApiProjectData | undefined> {
return this.queryApi(getProjectQuery, accessToken, {slug});
async getProject(slug: string, tokenInfo: TokenInfo): Promise<ApiProjectData | undefined> {
return this.queryApi(getProjectQuery, tokenInfo, {slug});
}

async getPolicy(slug: string, accessToken: string): Promise<ApiPolicyData | undefined> {
return this.queryApi(getPolicyQuery, accessToken, {slug});
async getPolicy(slug: string, tokenInfo: TokenInfo): Promise<ApiPolicyData | undefined> {
return this.queryApi(getPolicyQuery, tokenInfo, {slug});
}

async getSuppressions(repositoryId: string, accessToken: string): Promise<ApiSuppressionsData | undefined> {
return this.queryApi(getSuppressionsQuery, accessToken, {repositoryId});
async getSuppressions(repositoryId: string, tokenInfo: TokenInfo): Promise<ApiSuppressionsData | undefined> {
return this.queryApi(getSuppressionsQuery, tokenInfo, {repositoryId});
}

generateDeepLink(path: string) {
Expand All @@ -189,14 +190,14 @@ export class ApiHandler {
return normalizeUrl(`${this.apiUrl}/${path}`);
}

private async queryApi<OUT>(query: string, token: string, variables = {}): Promise<OUT | undefined> {
private async queryApi<OUT>(query: string, tokenInfo: TokenInfo, variables = {}): Promise<OUT | undefined> {
const apiEndpointUrl = normalizeUrl(`${this.apiUrl}/graphql`);

const response = await fetch(apiEndpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
Authorization: this.formatAuthorizationHeader(tokenInfo),
},
body: JSON.stringify({
query,
Expand All @@ -212,4 +213,12 @@ export class ApiHandler {

return response.json() as Promise<OUT>;
}

private formatAuthorizationHeader(tokenInfo: TokenInfo) {
const tokenType =
tokenInfo?.tokenType?.toLowerCase() === 'bearer' || tokenInfo?.tokenType?.toLowerCase() === 'apikey'
? tokenInfo.tokenType
: 'Bearer';
return `${tokenType} ${tokenInfo.accessToken}`;
}
}
2 changes: 1 addition & 1 deletion packages/synchronizer/src/handlers/storageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export abstract class StorageHandler<TData> {
await mkdirp(dir);
await writeFile(file, data);
} catch (err: any) {
throw new Error(`Failed to write configuration to '${file}' with error: ${err.message} and data: ${data}`,);
throw new Error(`Failed to write configuration to '${file}' with error: ${err.message} and data: ${data}`);
}
}
}
9 changes: 8 additions & 1 deletion packages/synchronizer/src/handlers/storageHandlerAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ import {StorageHandler} from './storageHandler.js';
import {DEFAULT_STORAGE_CONFIG_FILE_AUTH, DEFAULT_STORAGE_CONFIG_FOLDER} from '../constants.js';
import type {TokenSet} from './deviceFlowHandler.js';

export type TokenType = 'Bearer' | 'ApiKey';

export type TokenInfo = {
accessToken: string;
tokenType: TokenType;
};

export type AccessToken = {
access_token: string;
token_type: 'access_token';
token_type: 'ApiKey';
};

export type Token = AccessToken | TokenSet;
Expand Down
8 changes: 7 additions & 1 deletion packages/synchronizer/src/models/user.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type {StorageAuthFormat} from '../handlers/storageHandlerAuth.js';
import type {StorageAuthFormat, TokenType, TokenInfo} from '../handlers/storageHandlerAuth.js';

export class User {
private _email: string | null = null;
private _token: string | null = null;
private _tokenInfo: TokenInfo | null = null;
private _data: StorageAuthFormat | null = null;
private _isAuthenticated: boolean = false;

Expand All @@ -12,6 +13,7 @@ export class User {
if (this._isAuthenticated) {
this._email = data!.auth!.email;
this._token = data!.auth!.token.access_token!;
this._tokenInfo = {accessToken: this._token, tokenType: data!.auth!.token.token_type! as TokenType};
this._data = data;
}
}
Expand All @@ -28,6 +30,10 @@ export class User {
return this._token;
}

get tokenInfo() {
return this._tokenInfo;
}

get data() {
return this._data;
}
Expand Down
11 changes: 7 additions & 4 deletions packages/synchronizer/src/utils/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {User} from '../models/user.js';
import {StorageHandlerAuth} from '../handlers/storageHandlerAuth.js';
import {ApiHandler} from '../handlers/apiHandler.js';
import {DeviceFlowHandler} from '../handlers/deviceFlowHandler.js';
import type {Token} from '../handlers/storageHandlerAuth.js';
import type {Token, TokenType} from '../handlers/storageHandlerAuth.js';
import type {DeviceFlowHandle, TokenSet} from '../handlers/deviceFlowHandler.js';

export type AuthMethod = 'device code' | 'token';
Expand Down Expand Up @@ -80,7 +80,7 @@ export class Authenticator extends EventEmitter {
return;
}

if (tokenData.token_type === 'access_token') {
if (tokenData.token_type === 'ApiKey') {
return;
}

Expand Down Expand Up @@ -112,7 +112,7 @@ export class Authenticator extends EventEmitter {
private async loginWithToken(token: string): Promise<AuthenticatorLoginResponse> {
const tokenData: Token = {
access_token: token,
token_type: 'access_token',
token_type: 'ApiKey',
};

const donePromise: Promise<User> = new Promise((resolve, reject) => {
Expand Down Expand Up @@ -164,7 +164,10 @@ export class Authenticator extends EventEmitter {
}

private async setUserData(token: Token) {
const userApiData = await this._apiHandler.getUser(token.access_token!);
const userApiData = await this._apiHandler.getUser({
accessToken: token.access_token!,
tokenType: token.token_type! as TokenType,
});

await this._storageHandler.setStoreData({
auth: {
Expand Down
Loading
Loading