Skip to content

Commit

Permalink
feat: Pace requests to token server for new auth tokens (#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mike Kistler authored Mar 5, 2020
1 parent 9cf7942 commit b14dc4e
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 2 deletions.
44 changes: 42 additions & 2 deletions auth/token-managers/jwt-token-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export class JwtTokenManager {
private tokenInfo: any;
private expireTime: number;
private refreshTime: number;
private requestTime: number;
private pendingRequests: any[];

/**
* Create a new [[JwtTokenManager]] instance.
Expand Down Expand Up @@ -83,6 +85,9 @@ export class JwtTokenManager {

// 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 = [];
}

/**
Expand All @@ -93,8 +98,7 @@ export class JwtTokenManager {
public getToken(): Promise<any> {
if (!this.tokenInfo[this.tokenName] || this.isTokenExpired()) {
// 1. request a new token
return this.requestToken().then(tokenResponse => {
this.saveTokenInfo(tokenResponse.result);
return this.pacedRequestToken().then(() => {
return this.tokenInfo[this.tokenName];
});
} else {
Expand Down Expand Up @@ -136,6 +140,42 @@ export class JwtTokenManager {
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.result);
this.pendingRequests.forEach(({resolve}) => {
resolve();
});
this.pendingRequests = [];
this.requestTime = 0;
}).catch(err => {
this.pendingRequests.forEach(({reject}) => {
reject(err);
});
throw(err);
});
}
}

/**
* Request a JWT using an API key.
*
Expand Down
63 changes: 63 additions & 0 deletions test/unit/jwt-token-manager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,69 @@ describe('JWT Token Manager', () => {
done();
});

it('should pace token requests', async done => {
const instance = new JwtTokenManager();

const decodeSpy = jest
.spyOn(jwt, 'decode')
.mockImplementation(token => ({ iat: 10, exp: 100 }));

const requestTokenSpy = jest.spyOn(instance, 'requestToken').mockImplementation(() => {
return new Promise(resolve => {
setTimeout(resolve, 500, { result: { access_token: ACCESS_TOKEN } });
});
});

const tokens = await Promise.all([
instance.getToken(),
instance.getToken(),
instance.getToken(),
]);

expect(tokens.length).toBe(3);
expect(
tokens.every(token => {
token === tokens[0];
})
);
expect(requestTokenSpy).toHaveBeenCalled();
expect(requestTokenSpy.mock.calls.length).toBe(1);

decodeSpy.mockRestore();
requestTokenSpy.mockRestore();
done();
});

it('should reject all paced token requests on error from token service', async done => {
const instance = new JwtTokenManager();

const requestTokenSpy = jest.spyOn(instance, 'requestToken').mockImplementation(() => {
return new Promise(reject => {
setTimeout(reject, 500, new Error('Sumpin bad happened'));
});
});

const reqs = [instance.getToken(), instance.getToken(), instance.getToken()];

let token;
let errCount = 0;
for (let i = 0; i < reqs.length; i++) {
try {
token = await reqs[i];
} catch (e) {
errCount++;
}
}

expect(token).toBeUndefined;
expect(errCount).toBe(3);
expect(requestTokenSpy).toHaveBeenCalled();
expect(requestTokenSpy.mock.calls.length).toBe(1);

requestTokenSpy.mockRestore();
done();
});

it('should request a token if token is stored but needs refresh', async done => {
const instance = new JwtTokenManager();
instance.tokenInfo.access_token = CURRENT_ACCESS_TOKEN;
Expand Down

0 comments on commit b14dc4e

Please sign in to comment.