import axios, { AxiosResponse, AxiosRequestConfig } from 'axios' import { MutexInterface } from 'async-mutex' import { EndpointClientConfig } from './endpoint-client' /** * Implement this interface to implement a process for handling authentication. * * This is not meant to be a "service" in the traditional sense because * implementors are not expected to be stateless. */ export interface Authenticator { login?(): Promise<void> logout?(): Promise<void> refresh?(requestConfig: AxiosRequestConfig, clientConfig: EndpointClientConfig): Promise<void> acquireRefreshMutex?(): Promise<MutexInterface.Releaser> /** * Performs required authentication steps to add credentials to the axios config, typically via Bearer Auth headers. * Expected to call other functions such as @see refresh as needed to return valid credentials. * * @param requestConfig AxiosRequestConfig to add credentials to and return otherwise unmodified */ authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> /** * Performs required authentication steps and returns credentials as a string value * Expected to perform any required steps (such as token refresh) needed to return valid credentials. * * @returns {string} valid auth token */ authenticateGeneric?(): Promise<string> } /** * For use in tests or on endpoints that don't need any authentication. */ export class NoOpAuthenticator implements Authenticator { authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> { return Promise.resolve(requestConfig) } authenticateGeneric(): Promise<string> { return Promise.resolve('') } } /** * A simple bearer token authenticator that knows nothing about refreshing * or logging in our out. If the token is expired, it simply won't work. */ export class BearerTokenAuthenticator implements Authenticator { constructor(public token: string) { // simple } authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> { return Promise.resolve({ ...requestConfig, headers: { ...requestConfig.headers, Authorization: `Bearer ${this.token}`, }, }) } authenticateGeneric(): Promise<string> { return Promise.resolve(this.token) } } export interface AuthData { authToken: string refreshToken: string } export interface RefreshData { refreshToken: string clientId: string clientSecret: string } export interface RefreshTokenStore { getRefreshData(): Promise<RefreshData> putAuthData(data: AuthData): Promise<void> } /** * An authenticator that supports refreshing of the access token using a refresh token by loading * the refresh token, client ID, and client secret from a token store, performing the refresh, and * storing the new tokens. * * Note that corruption of the refresh token is unlikely but possible if two of the same * authenticators refresh the same token at the same time. */ export class RefreshTokenAuthenticator implements Authenticator { constructor(public token: string, private tokenStore: RefreshTokenStore) { // simple } authenticate(requestConfig: AxiosRequestConfig): Promise<AxiosRequestConfig> { return Promise.resolve({ ...requestConfig, headers: { ...requestConfig.headers, Authorization: `Bearer ${this.token}`, }, }) } async refresh(requestConfig: AxiosRequestConfig, clientConfig: EndpointClientConfig): Promise<void> { const refreshData: RefreshData = await this.tokenStore.getRefreshData() const headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic ' + Buffer.from(`${refreshData.clientId}:${refreshData.clientSecret}`, 'ascii').toString('base64'), 'Accept': 'application/json', } const axiosConfig: AxiosRequestConfig = { url: clientConfig.urlProvider?.authURL, method: 'POST', headers, data: `grant_type=refresh_token&client_id=${refreshData.clientId}&refresh_token=${refreshData.refreshToken}`, } const response: AxiosResponse = await axios.request(axiosConfig) if (response.status > 199 && response.status < 300) { const authData: AuthData = { authToken: response.data.access_token, refreshToken: response.data.refresh_token, } this.token = authData.authToken requestConfig.headers = { ...(requestConfig.headers ?? {}), Authorization: `Bearer ${this.token}` } return this.tokenStore.putAuthData(authData) } throw Error(`error ${response.status} refreshing token, with message ${response.data}`) } } /** * A an authenticator that works like RefreshTokenAuthenticator but which can use a mutex to help * prevent corruption of the refresh token. * * Note that while `acquireRefreshMutex` is provided for you to use the mutex, the mutex is not * automatically used. */ export class SequentialRefreshTokenAuthenticator extends RefreshTokenAuthenticator { constructor(token: string, tokenStore: RefreshTokenStore, private refreshMutex: MutexInterface) { super(token, tokenStore) } acquireRefreshMutex(): Promise<MutexInterface.Releaser> { return this.refreshMutex.acquire() } }