-
Notifications
You must be signed in to change notification settings - Fork 308
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e1bf9f3
commit d5902d7
Showing
8 changed files
with
663 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './management-client.options'; | ||
export * from './management-client'; | ||
export * from './token-provider.middleware'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
export interface ManagementClientOptions { | ||
domain: string; | ||
audience?: string; | ||
} | ||
|
||
export interface ManagementClientOptionsWithToken extends ManagementClientOptions { | ||
token: string; | ||
} | ||
|
||
export interface ManagementClientOptionsWithClientSecret extends ManagementClientOptions { | ||
clientId: string; | ||
clientSecret: string; | ||
} | ||
|
||
export interface ManagementClientOptionsWithClientAssertion extends ManagementClientOptions { | ||
clientId: string; | ||
clientAssertionSigningKey: string; | ||
} | ||
|
||
export type ManagementClientOptionsWithClientCredentials = | ||
| ManagementClientOptionsWithClientSecret | ||
| ManagementClientOptionsWithClientAssertion; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import fetch, { RequestInfo as NFRequestInfo, RequestInit as NFRequestInit } from 'node-fetch'; | ||
import { Configuration } from './runtime'; | ||
import { ManagementClientBase } from './__generated'; | ||
import { | ||
ManagementClientOptionsWithToken, | ||
ManagementClientOptionsWithClientCredentials, | ||
} from './management-client.options'; | ||
import { tokenProviderFactory } from './management-client.utils'; | ||
import { TokenProviderMiddleware } from './token-provider.middleware'; | ||
|
||
export class ManagementClient extends ManagementClientBase { | ||
constructor( | ||
options: ManagementClientOptionsWithToken | ManagementClientOptionsWithClientCredentials | ||
) { | ||
super( | ||
new Configuration({ | ||
baseUrl: 'https://' + options.domain + '/api/v2', | ||
fetchApi: (url: RequestInfo, init: RequestInit) => { | ||
return fetch(url as NFRequestInfo, init as NFRequestInit) as unknown as Promise<Response>; | ||
}, | ||
middleware: [], | ||
}) | ||
); | ||
|
||
this.configuration.middleware.push(new TokenProviderMiddleware(tokenProviderFactory(options))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { | ||
ManagementClientOptionsWithClientCredentials, | ||
ManagementClientOptionsWithToken, | ||
} from './management-client.options'; | ||
import { TokenProviderOptions, TokenProvider } from './token-provider'; | ||
|
||
export function toTokenProviderOptions( | ||
options: ManagementClientOptionsWithClientCredentials | ||
): TokenProviderOptions { | ||
const base = { | ||
domain: options.domain, | ||
audience: options.audience ?? `https://${options.domain}/api/v2/`, | ||
}; | ||
|
||
if ('clientAssertionSigningKey' in options) { | ||
return { | ||
...base, | ||
clientId: options.clientId, | ||
clientAssertionSigningKey: options.clientAssertionSigningKey, | ||
}; | ||
} else { | ||
return { | ||
...base, | ||
clientId: options.clientId, | ||
clientSecret: options.clientSecret, | ||
}; | ||
} | ||
} | ||
|
||
export function tokenProviderFactory( | ||
options: ManagementClientOptionsWithToken | ManagementClientOptionsWithClientCredentials | ||
) { | ||
if ('token' in options) { | ||
return { | ||
getAccessToken: () => { | ||
return Promise.resolve(options.token); | ||
}, | ||
} as TokenProvider; | ||
} else { | ||
return new TokenProvider(toTokenProviderOptions(options)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './../runtime'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { FetchParams, Middleware, RequestContext } from './__generated'; | ||
import { TokenProvider } from './token-provider'; | ||
|
||
export class TokenProviderMiddleware implements Middleware { | ||
constructor(private tokenProvider: TokenProvider) {} | ||
|
||
async pre?(context: RequestContext): Promise<FetchParams | void> { | ||
const token = await this.tokenProvider.getAccessToken(); | ||
context.init.headers = { | ||
...context.init.headers, | ||
Authorization: `Bearer ${token}`, | ||
}; | ||
return { | ||
url: context.url, | ||
init: context.init, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import memoizer from 'lru-memoizer'; | ||
import { promisify } from 'util'; | ||
|
||
import AuthenticationClient from '../auth'; | ||
|
||
interface TokenResponse { | ||
access_token: string; | ||
expires_in: number; | ||
} | ||
export interface BaseTokenProviderOptions { | ||
domain: string; | ||
audience: string; | ||
clientId: string; | ||
scope?: string; | ||
|
||
enableCache?: boolean; | ||
cacheTTLInSeconds?: number; | ||
} | ||
|
||
export interface TokenProviderOptionsWithClientSecret extends BaseTokenProviderOptions { | ||
clientSecret: string; | ||
} | ||
|
||
export interface TokenProviderOptionsWithClientAssertion extends BaseTokenProviderOptions { | ||
clientAssertionSigningKey: string; | ||
} | ||
|
||
export type TokenProviderOptions = | ||
| TokenProviderOptionsWithClientSecret | ||
| TokenProviderOptionsWithClientAssertion; | ||
|
||
export class TokenProvider { | ||
private options: TokenProviderOptions; | ||
|
||
// Due to lack of ESM support, using any for now. | ||
private authenticationClient: any; | ||
|
||
constructor(options: TokenProviderOptions) { | ||
if (!options || typeof options !== 'object') { | ||
throw new Error('Options must be an object'); | ||
} | ||
|
||
const params = { enableCache: true, ...options }; | ||
|
||
if (!params.domain || params.domain.length === 0) { | ||
throw new Error('Must provide a domain'); | ||
} | ||
|
||
if (!params.clientId || params.clientId.length === 0) { | ||
throw new Error('Must provide a clientId'); | ||
} | ||
|
||
if (!('clientSecret' in params) && !('clientAssertionSigningKey' in params)) { | ||
throw new Error('Must provide a clientSecret or a clientAssertionSigningKey'); | ||
} else if ( | ||
('clientSecret' in params && params.clientSecret.length === 0) || | ||
('clientAssertionSigningKey' in params && params.clientAssertionSigningKey.length === 0) | ||
) { | ||
throw new Error('Must provide a clientSecret or a clientAssertionSigningKey'); | ||
} | ||
|
||
if (!params.audience || params.audience.length === 0) { | ||
throw new Error('Must provide a audience'); | ||
} | ||
|
||
if (typeof params.enableCache !== 'boolean') { | ||
throw new Error('enableCache must be a boolean'); | ||
} | ||
|
||
if (params.enableCache && params.cacheTTLInSeconds) { | ||
if (typeof params.cacheTTLInSeconds !== 'number') { | ||
throw new Error('cacheTTLInSeconds must be a number'); | ||
} | ||
|
||
if (params.cacheTTLInSeconds <= 0) { | ||
throw new Error('cacheTTLInSeconds must be a greater than 0'); | ||
} | ||
} | ||
|
||
if (params.scope && typeof params.scope !== 'string') { | ||
throw new Error('scope must be a string'); | ||
} | ||
|
||
this.options = params; | ||
|
||
const { scope, audience, enableCache, cacheTTLInSeconds, ...authenticationClientOptions } = | ||
this.options; | ||
|
||
this.authenticationClient = new AuthenticationClient(authenticationClientOptions); | ||
} | ||
|
||
private getCachedAccessToken = promisify<TokenProviderOptions, TokenResponse>( | ||
memoizer<TokenProviderOptions, TokenResponse>({ | ||
load: (options: TokenProviderOptions, callback: (err: any, data: TokenResponse) => void) => { | ||
this.clientCredentialsGrant(options.domain, options.scope, options.audience) | ||
.then((data: TokenResponse) => { | ||
callback(null, data); | ||
}) | ||
.catch((err: any) => { | ||
callback(err, null); | ||
}); | ||
}, | ||
hash(options: TokenProviderOptions) { | ||
return `${options.domain}-${options.clientId}-${options.scope}`; | ||
}, | ||
itemMaxAge(options: TokenProviderOptions, data: TokenResponse) { | ||
if (options.cacheTTLInSeconds) { | ||
return options.cacheTTLInSeconds * 1000; | ||
} | ||
|
||
// if the expires_in is lower or equal to than 10 seconds, do not subtract 10 additional seconds. | ||
if (data.expires_in && data.expires_in <= 10 /* seconds */) { | ||
return data.expires_in * 1000; | ||
} else if (data.expires_in) { | ||
// Subtract 10 seconds from expires_in to fetch a new one, before it expires. | ||
return data.expires_in * 1000 - 10000 /* milliseconds */; | ||
} | ||
return 60 * 60 * 1000; //1h | ||
}, | ||
|
||
// TODO: Need to patch lru-memoizer to accept a max on its types. | ||
// max: 100, | ||
}) | ||
); | ||
|
||
/** | ||
* Returns the access_token. | ||
* | ||
* @returns {Promise} Promise returning an access_token. | ||
*/ | ||
async getAccessToken() { | ||
if (this.options.enableCache) { | ||
const data = await this.getCachedAccessToken(this.options); | ||
return data.access_token; | ||
} else { | ||
const data = await this.clientCredentialsGrant( | ||
this.options.domain, | ||
this.options.scope, | ||
this.options.audience | ||
); | ||
return data.access_token; | ||
} | ||
} | ||
|
||
private clientCredentialsGrant( | ||
domain: string, | ||
scope: string, | ||
audience: string | ||
): Promise<TokenResponse> { | ||
return this.authenticationClient.clientCredentialsGrant({ | ||
audience, | ||
scope, | ||
}); | ||
} | ||
} |
Oops, something went wrong.