Skip to content

Commit

Permalink
add support to auth caching (#575)
Browse files Browse the repository at this point in the history
  • Loading branch information
MCarlomagno committed May 22, 2024
1 parent 93e678e commit 0fc43da
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 28 deletions.
7 changes: 5 additions & 2 deletions examples/create-relayer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ require('dotenv').config();
const { RelayClient } = require('@openzeppelin/defender-relay-client');

async function main() {
const creds = { apiKey: process.env.ADMIN_API_KEY, apiSecret: process.env.ADMIN_API_SECRET };
const client = new RelayClient(creds);
const client = new RelayClient({
apiKey: process.env.ADMIN_API_KEY,
apiSecret: process.env.ADMIN_API_SECRET,
useCredentialsCaching: false,
});

const createParams = {
name: 'MyNewRelayer',
Expand Down
2 changes: 1 addition & 1 deletion packages/base/src/api/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const token = 'token';

describe('createApi', () => {
test('passes correct arguments to axios', () => {
createApi(key, token, apiUrl);
createApi(apiUrl, key, token);
expect(axios.create).toBeCalledWith({
baseURL: apiUrl,
headers: {
Expand Down
34 changes: 27 additions & 7 deletions packages/base/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,27 @@ export function rejectWithDefenderApiError(axiosError: AxiosError): Promise<neve
return Promise.reject(new DefenderApiResponseError(axiosError));
}

export function createApi(key: string, token: string, apiUrl: string, httpsAgent?: https.Agent): AxiosInstance {
export function createApi(
apiUrl: string,
key?: string,
token?: string,
httpsAgent?: https.Agent,
headers?: Record<string, string>,
): AxiosInstance {
const authHeaders =
key && token
? {
'X-Api-Key': key,
Authorization: `Bearer ${token}`,
}
: {};

const instance = axios.create({
baseURL: apiUrl,
headers: {
'X-Api-Key': key,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...authHeaders,
...headers,
},
httpsAgent,
});
Expand All @@ -25,11 +39,17 @@ export function createApi(key: string, token: string, apiUrl: string, httpsAgent

export function createAuthenticatedApi(
username: string,
session: CognitoUserSession,
accessToken: string,
apiUrl: string,
httpsAgent?: https.Agent,
): AxiosInstance {
const accessToken = session.getAccessToken().getJwtToken();

return createApi(username, accessToken, apiUrl, httpsAgent);
return createApi(apiUrl, username, accessToken, httpsAgent);
}

export function createUnauthorizedApi(
apiUrl: string,
httpsAgent?: https.Agent,
headers?: Record<string, string>,
): AxiosInstance {
return createApi(apiUrl, undefined, undefined, httpsAgent, headers);
}
43 changes: 43 additions & 0 deletions packages/base/src/api/auth-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import retry from 'async-retry';
import { createUnauthorizedApi } from './api';
import { DefenderApiResponseError } from './api-error';

export type AuthType = 'admin' | 'relay';

export type AuthCredentials = {
apiKey: string;
secretKey: string;
type: AuthType;
};

export type RefreshCredentials = {
apiKey: string;
secretKey: string;
refreshToken: string;
type: AuthType;
};

export type AuthResponse = {
accessToken: string;
refreshToken: string;
};

export async function authenticateV2(credentials: AuthCredentials, apiUrl: string): Promise<AuthResponse> {
const api = createUnauthorizedApi(apiUrl);
try {
return await retry(() => api.post('/auth/login', credentials), { retries: 3 });
} catch (err) {
const errorMessage = (err as DefenderApiResponseError).response.statusText || err;
throw new Error(`Failed to get a token for the API key ${credentials.apiKey}: ${errorMessage}`);
}
}

export async function refreshSessionV2(credentials: RefreshCredentials, apiUrl: string): Promise<AuthResponse> {
const api = createUnauthorizedApi(apiUrl);
try {
return await retry(() => api.post('/auth/refresh-token', credentials), { retries: 3 });
} catch (err) {
const errorMessage = (err as DefenderApiResponseError).response.statusText || err;
throw new Error(`Failed to refresh token for the API key ${credentials.apiKey}: ${errorMessage}`);
}
}
70 changes: 59 additions & 11 deletions packages/base/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,98 @@ import https from 'https';

import { createAuthenticatedApi } from './api';
import { authenticate, refreshSession } from './auth';
import { AuthType, authenticateV2, refreshSessionV2 } from './auth-v2';

export type ApiVersion = 'v1' | 'v2';

export type AuthConfig = {
useCredentialsCaching: boolean;
type: AuthType;
};

export abstract class BaseApiClient {
private api: AxiosInstance | undefined;
private version: ApiVersion | undefined;
private apiKey: string;
private session: CognitoUserSession | undefined;
private sessionV2: { accessToken: string; refreshToken: string } | undefined;
private apiSecret: string;
private httpsAgent?: https.Agent;
private authConfig: AuthConfig;

protected abstract getPoolId(): string;
protected abstract getPoolClientId(): string;
protected abstract getApiUrl(v: ApiVersion): string;
protected abstract getApiUrl(v: ApiVersion, type?: AuthType): string;

public constructor(params: { apiKey: string; apiSecret: string; httpsAgent?: https.Agent }) {
public constructor(params: { apiKey: string; apiSecret: string; httpsAgent?: https.Agent; authConfig?: AuthConfig }) {
if (!params.apiKey) throw new Error(`API key is required`);
if (!params.apiSecret) throw new Error(`API secret is required`);

this.apiKey = params.apiKey;
this.apiSecret = params.apiSecret;
this.httpsAgent = params.httpsAgent;
this.authConfig = params.authConfig ?? { useCredentialsCaching: false, type: 'admin' };
}

private async getAccessToken(): Promise<string> {
const userPass = { Username: this.apiKey, Password: this.apiSecret };
const poolData = { UserPoolId: this.getPoolId(), ClientId: this.getPoolClientId() };
this.session = await authenticate(userPass, poolData);
return this.session.getAccessToken().getJwtToken();
}

private async getAccessTokenV2(): Promise<string> {
if (!this.authConfig.type) throw new Error('Auth type is required to authenticate in auth v2');
const credentials = {
apiKey: this.apiKey,
secretKey: this.apiSecret,
type: this.authConfig.type,
};
this.sessionV2 = await authenticateV2(credentials, this.getApiUrl('v1', 'admin'));
return this.sessionV2.accessToken;
}

private async refreshSession(): Promise<string> {
if (!this.session) return this.getAccessToken();
const userPass = { Username: this.apiKey, Password: this.apiSecret };
const poolData = { UserPoolId: this.getPoolId(), ClientId: this.getPoolClientId() };
this.session = await refreshSession(userPass, poolData, this.session);
return this.session.getAccessToken().getJwtToken();
}

private async refreshSessionV2(): Promise<string> {
if (!this.authConfig.type) throw new Error('Auth type is required to refresh session in auth v2');
if (!this.sessionV2) return this.getAccessTokenV2();
const credentials = {
apiKey: this.apiKey,
secretKey: this.apiSecret,
refreshToken: this.sessionV2.refreshToken,
type: this.authConfig.type,
};
this.sessionV2 = await refreshSessionV2(credentials, this.getApiUrl('v1' ,'admin'));
return this.sessionV2.accessToken;
}

protected async init(v: ApiVersion = 'v1'): Promise<AxiosInstance> {
if (!this.api || this.version !== v) {
const userPass = { Username: this.apiKey, Password: this.apiSecret };
const poolData = { UserPoolId: this.getPoolId(), ClientId: this.getPoolClientId() };
this.session = await authenticate(userPass, poolData);
this.api = createAuthenticatedApi(userPass.Username, this.session, this.getApiUrl(v), this.httpsAgent);
const accessToken = this.authConfig.useCredentialsCaching
? await this.getAccessTokenV2()
: await this.getAccessToken();
this.api = createAuthenticatedApi(this.apiKey, accessToken, this.getApiUrl(v, 'admin'), this.httpsAgent);
this.version = v;
}
return this.api;
}

protected async refresh(v: ApiVersion = 'v1'): Promise<AxiosInstance> {
if (!this.session) {
if (!this.session && !this.sessionV2) {
return this.init(v);
}
try {
const userPass = { Username: this.apiKey, Password: this.apiSecret };
const poolData = { UserPoolId: this.getPoolId(), ClientId: this.getPoolClientId() };
this.session = await refreshSession(userPass, poolData, this.session);
this.api = createAuthenticatedApi(userPass.Username, this.session, this.getApiUrl(v), this.httpsAgent);
const accessToken = this.authConfig.useCredentialsCaching
? await this.refreshSessionV2()
: await this.refreshSession();
this.api = createAuthenticatedApi(this.apiKey, accessToken, this.getApiUrl(v, 'admin'), this.httpsAgent);

return this.api;
} catch (e) {
Expand Down
3 changes: 2 additions & 1 deletion packages/base/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { createApi, createAuthenticatedApi } from './api/api';
export { authenticate } from './api/auth';
export { BaseApiClient, ApiVersion } from './api/client';
export { BaseApiClient, ApiVersion, AuthConfig } from './api/client';
export { AuthType } from './api/auth-v2';
export * from './utils/network';

// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand Down
27 changes: 23 additions & 4 deletions packages/relay/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseApiClient, ApiVersion } from '@openzeppelin/defender-base-client';
import { BaseApiClient, ApiVersion, AuthType } from '@openzeppelin/defender-base-client';
import {
ApiRelayerParams,
IRelayer,
Expand All @@ -23,7 +23,19 @@ import {
export const RelaySignerApiUrl = () =>
process.env.DEFENDER_RELAY_SIGNER_API_URL || 'https://api.defender.openzeppelin.com/';

export const getAdminApiUrl = () => process.env.DEFENDER_API_URL || 'https://defender-api.openzeppelin.com/';

export class RelayClient extends BaseApiClient {

public constructor(params: ApiRelayerParams) {
super({
apiKey: params.apiKey,
apiSecret: params.apiSecret,
httpsAgent: params.httpsAgent,
authConfig: { useCredentialsCaching: params.useCredentialsCaching ?? false, type: 'admin' },
});
}

protected getPoolId(): string {
return process.env.DEFENDER_RELAY_POOL_ID || 'us-west-2_94f3puJWv';
}
Expand All @@ -32,7 +44,8 @@ export class RelayClient extends BaseApiClient {
return process.env.DEFENDER_RELAY_POOL_CLIENT_ID || '40e58hbc7pktmnp9i26hh5nsav';
}

protected getApiUrl(v: ApiVersion = 'v1'): string {
protected getApiUrl(v: ApiVersion = 'v1', type?: AuthType): string {
if (type === 'admin') return getAdminApiUrl();
if (v === 'v2') {
return process.env.DEFENDER_API_V2_URL || 'https://defender-api.openzeppelin.com/v2/';
}
Expand Down Expand Up @@ -109,7 +122,12 @@ export class ApiRelayer extends BaseApiClient implements IRelayer {
private jsonRpcRequestNextId: number;

public constructor(params: ApiRelayerParams) {
super(params);
super({
apiKey: params.apiKey,
apiSecret: params.apiSecret,
httpsAgent: params.httpsAgent,
authConfig: { useCredentialsCaching: params.useCredentialsCaching ?? false, type: 'relay' },
});
this.jsonRpcRequestNextId = 1;
}

Expand All @@ -121,7 +139,8 @@ export class ApiRelayer extends BaseApiClient implements IRelayer {
return process.env.DEFENDER_RELAY_SIGNER_POOL_CLIENT_ID || '1bpd19lcr33qvg5cr3oi79rdap';
}

protected getApiUrl(_: ApiVersion): string {
protected getApiUrl(_: ApiVersion, type?: AuthType): string {
if (type === 'admin') return getAdminApiUrl();
return RelaySignerApiUrl();
}

Expand Down
4 changes: 2 additions & 2 deletions packages/relay/src/relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ export type RelayerTransaction = RelayerLegacyTransaction | RelayerEIP1559Transa
export type PaginatedTransactionListResponse = RelayerTransaction[] | { items: RelayerTransaction[]; next?: string };

export type RelayerParams = ApiRelayerParams | AutotaskRelayerParams;
export type ApiRelayerParams = { apiKey: string; apiSecret: string; httpsAgent?: https.Agent };
export type AutotaskRelayerParams = { credentials: string; relayerARN: string; httpsAgent?: https.Agent };
export type ApiRelayerParams = { apiKey: string; apiSecret: string; httpsAgent?: https.Agent, useCredentialsCaching?: boolean; };
export type AutotaskRelayerParams = { credentials: string; relayerARN: string; httpsAgent?: https.Agent, useCredentialsCaching?: boolean; };

export type JsonRpcResponse = {
id: number | null;
Expand Down

0 comments on commit 0fc43da

Please sign in to comment.