Skip to content

Commit

Permalink
feat(token-manager): Introduce a token-manager class for token handli…
Browse files Browse the repository at this point in the history
…ng (#89)

token-manager class will be responsible only for the token handling specifically and change some private fields to protected: tokenInfo, expireTime, refreshTime.
  • Loading branch information
vmatyus authored May 5, 2020
1 parent a203e71 commit 23c5f3f
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 178 deletions.
4 changes: 2 additions & 2 deletions auth/token-managers/cp4d-token-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

import extend = require('extend');
import { computeBasicAuthHeader, validateInput } from '../utils';
import { JwtTokenManager, TokenManagerOptions } from './jwt-token-manager';
import { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager';

/** Configuration options for CP4D token retrieval. */
interface Options extends TokenManagerOptions {
interface Options extends JwtTokenManagerOptions {
/** The endpoint for CP4D token requests. */
url: string;
/** The username portion of basic authentication. */
Expand Down
4 changes: 2 additions & 2 deletions auth/token-managers/iam-token-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import extend = require('extend');
import { OutgoingHttpHeaders } from 'http';
import logger from '../../lib/logger';
import { computeBasicAuthHeader, validateInput } from '../utils';
import { JwtTokenManager, TokenManagerOptions } from './jwt-token-manager';
import { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager';

/**
* Check for only one of two elements being defined.
Expand All @@ -37,7 +37,7 @@ function onlyOne(a: any, b: any): boolean {
const CLIENT_ID_SECRET_WARNING = 'Warning: Client ID and Secret must BOTH be given, or the header will not be included.';

/** Configuration options for IAM token retrieval. */
interface Options extends TokenManagerOptions {
interface Options extends JwtTokenManagerOptions {
apikey: string;
clientId?: string;
clientSecret?: string;
Expand Down
3 changes: 2 additions & 1 deletion auth/token-managers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@

export { IamTokenManager } from './iam-token-manager';
export { Cp4dTokenManager } from './cp4d-token-manager';
export { JwtTokenManager } from './jwt-token-manager';
export { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager';
export { TokenManager, TokenManagerOptions } from './token-manager';
185 changes: 12 additions & 173 deletions auth/token-managers/jwt-token-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,22 @@
*/

import extend = require('extend');
import { OutgoingHttpHeaders } from 'http';
import jwt = require('jsonwebtoken');
import logger from '../../lib/logger';
import { RequestWrapper } from '../../lib/request-wrapper';

function getCurrentTime(): number {
return Math.floor(Date.now() / 1000);
}
import { TokenManager, TokenManagerOptions } from "./token-manager";

/** Configuration options for JWT token retrieval. */
export type TokenManagerOptions = {
/** The endpoint for token requests. */
url?: string;
/** Headers to be sent with every service token request. */
headers?: OutgoingHttpHeaders;
/**
* A flag that indicates whether verification of
* the server's SSL certificate should be disabled or not.
*/
disableSslVerification?: boolean;
/** Allow additional request config parameters */
[propName: string]: any;
}
export type JwtTokenManagerOptions = TokenManagerOptions;

/**
* A class for shared functionality for parsing, storing, and requesting
* JWT tokens. Intended to be used as a parent to be extended for token
* request management. Child classes should implement `requestToken()`
* to retrieve the bearer token from intended sources.
*/
export class JwtTokenManager {
protected url: string;
export class JwtTokenManager extends TokenManager {
protected tokenName: string;
protected disableSslVerification: boolean;
protected headers: OutgoingHttpHeaders;
protected requestWrapperInstance: RequestWrapper;
private tokenInfo: any;
private expireTime: number;
private refreshTime: number;
private requestTime: number;
private pendingRequests: any[];
protected tokenInfo: any;

/**
* Create a new [[JwtTokenManager]] instance.
Expand All @@ -68,112 +43,13 @@ export class JwtTokenManager {
* @param {object<string, string>} [options.headers] Headers to be sent with every
* outbound HTTP requests to token services.
*/
constructor(options: TokenManagerOptions) {
constructor(options: JwtTokenManagerOptions) {
// all parameters are optional
options = options || {} as TokenManagerOptions;
options = options || {} as JwtTokenManagerOptions;
super(options);

this.tokenInfo = {};
this.tokenName = 'access_token';

if (options.url) {
this.url = options.url;
}

// request options
this.disableSslVerification = Boolean(options.disableSslVerification);
this.headers = options.headers || {};

// any config options for the internal request library, like `proxy`, will be passed here
this.requestWrapperInstance = new RequestWrapper(options);

// Array of requests pending completion of an active token request -- initially empty
this.pendingRequests = [];
}

/**
* Retrieve a new token using `requestToken()` in the case there is not a
* currently stored token from a previous call, or the previous token
* has expired.
*/
public getToken(): Promise<any> {
if (!this.tokenInfo[this.tokenName] || this.isTokenExpired()) {
// 1. request a new token
return this.pacedRequestToken().then(() => {
return this.tokenInfo[this.tokenName];
});
} else {
// If refresh needed, kick one off
if (this.tokenNeedsRefresh()) {
this.requestToken().then(tokenResponse => {
this.saveTokenInfo(tokenResponse);
});
}
// 2. use valid, managed token
return Promise.resolve(this.tokenInfo[this.tokenName]);
}
}

/**
* Setter for the disableSslVerification property.
*
* @param {boolean} value - the new value for the disableSslVerification
* property
* @returns {void}
*/
public setDisableSslVerification(value: boolean): void {
// if they try to pass in a non-boolean value,
// use the "truthy-ness" of the value
this.disableSslVerification = Boolean(value);
}

/**
* Set a completely new set of headers.
*
* @param {OutgoingHttpHeaders} headers - the new set of headers as an object
* @returns {void}
*/
public setHeaders(headers: OutgoingHttpHeaders): void {
if (typeof headers !== 'object') {
// do nothing, for now
return;
}
this.headers = headers;
}

/**
* Paces requests to request_token.
*
* This method pseudo-serializes requests for an access_token
* when the current token is undefined or expired.
* The first caller to this method records its `requestTime` and
* then issues the token request. Subsequent callers will check the
* `requestTime` to see if a request is active (has been issued within
* the past 60 seconds), and if so will queue their promise for the
* active requestor to resolve when that request completes.
*/
protected pacedRequestToken(): Promise<any> {
const currentTime = getCurrentTime();
if (this.requestTime > (currentTime - 60)) {
// token request is active -- queue the promise for this request
return new Promise((resolve, reject) => {
this.pendingRequests.push({resolve, reject});
});
} else {
this.requestTime = currentTime;
return this.requestToken().then(tokenResponse => {
this.saveTokenInfo(tokenResponse);
this.pendingRequests.forEach(({resolve}) => {
resolve();
});
this.pendingRequests = [];
this.requestTime = 0;
}).catch(err => {
this.pendingRequests.forEach(({reject}) => {
reject(err);
});
throw(err);
});
}
this.tokenInfo = {};
}

/**
Expand All @@ -197,19 +73,19 @@ export class JwtTokenManager {
*/
protected saveTokenInfo(tokenResponse): void {
const responseBody = tokenResponse.result || {};
const accessToken = responseBody[this.tokenName];
this.accessToken = responseBody[this.tokenName];

if (!accessToken) {
if (!this.accessToken) {
const err = 'Access token not present in response';
logger.error(err);
throw new Error(err);
}

// the time of expiration is found by decoding the JWT access token
// exp is the time of expire and iat is the time of token retrieval
const decodedResponse = jwt.decode(accessToken);
const decodedResponse = jwt.decode(this.accessToken);
if (!decodedResponse) {
const err = 'Access token recieved is not a valid JWT'
const err = 'Access token recieved is not a valid JWT';
logger.error(err);
throw new Error(err);
}
Expand All @@ -229,41 +105,4 @@ export class JwtTokenManager {
this.tokenInfo = extend({}, responseBody);
}

/**
* Check if currently stored token is expired
*
* @private
* @returns {boolean}
*/
private isTokenExpired(): boolean {
const { expireTime } = this;

if (!expireTime) {
return true;
}

const currentTime = getCurrentTime();
return expireTime <= currentTime;
}

/**
* Check if currently stored token should be refreshed
* i.e. past the window to request a new token
*
* @private
* @returns {boolean}
*/
private tokenNeedsRefresh(): boolean {
const { refreshTime } = this;
const currentTime = getCurrentTime();

if (refreshTime && refreshTime > currentTime) {
return false;
}

// Update refreshTime to 60 seconds from now to avoid redundant refreshes
this.refreshTime = currentTime + 60;

return true;
}
}
Loading

0 comments on commit 23c5f3f

Please sign in to comment.