Skip to content

Commit

Permalink
feat: handle access token refresh - OKTA-291504 (#138)
Browse files Browse the repository at this point in the history
  • Loading branch information
shuowu authored May 25, 2020
1 parent 2c8c358 commit 161205e
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 168 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Okta Node SDK Changelog

## 3.3.1

- [#138](https://github.com/okta/okta-sdk-nodejs/pull/138) Add strategy to handle access token refresh

## 3.2.0

- [#128](https://github.com/okta/okta-sdk-nodejs/pull/128) Adds support for OAuth
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okta/okta-sdk-nodejs",
"version": "3.3.0",
"version": "3.3.1",
"description": "Okta API wrapper for Node.js",
"engines": {
"node": ">=8.11"
Expand Down
2 changes: 1 addition & 1 deletion src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Client extends GeneratedApiClient {
errors.push('Okta Org URL not provided');
}

if (!parsedConfig.client.token) {
if (!parsedConfig.client.token && parsedConfig.client.authorizationMode !== 'PrivateKey') {
errors.push('Okta API token not provided');
}

Expand Down
111 changes: 57 additions & 54 deletions src/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ const defaultCacheMiddleware = require('./default-cache-middleware');
* @class Http
*/
class Http {
static errorFilter(response) {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response);
} else {
return response.text()
.then(body => {
let err;

// If the response is JSON, assume it's an Okta API error. Otherwise, assume it's some other HTTP error

try {
err = new OktaApiError(response.url, response.status, JSON.parse(body), response.headers);
} catch (e) {
err = new HttpError(response.url, response.status, body, response.headers);
}
throw err;
});
}
}

constructor(httpConfig) {
this.defaultHeaders = {};
this.requestExecutor = httpConfig.requestExecutor;
Expand All @@ -36,74 +56,57 @@ class Http {
return Promise.resolve();
}

let getToken;
if (this.accessToken) {
getToken = Promise.resolve(this.accessToken);
} else {
getToken = this.oauth.getAccessToken()
.then(this.errorFilter)
.then(res => res.json())
.then(accessToken => {
this.accessToken = accessToken;
return accessToken;
});
}

return getToken
return this.oauth.getAccessToken()
.then(accessToken => {
request.headers.Authorization = `Bearer ${accessToken.access_token}`;
});
}

errorFilter(response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
return response.text()
.then(body => {
let err;

// If the response is JSON, assume it's an Okta API error. Otherwise, assume it's some other HTTP error

try {
err = new OktaApiError(response.url, response.status, JSON.parse(body), response.headers);
} catch (e) {
err = new HttpError(response.url, response.status, body, response.headers);
}
throw err;
});
}
}

http(uri, request, context) {
request = request || {};
context = context || {};
request.url = uri;
request.headers = Object.assign(this.defaultHeaders, request.headers);
request.method = request.method || 'get';
if (!this.cacheMiddleware) {
return this.prepareRequest(request)

let retriedOnAuthError = false;
const execute = () => {
const promise = this.prepareRequest(request)
.then(() => this.requestExecutor.fetch(request))
.then(this.errorFilter);
}
const ctx = {
uri, // TODO: remove unused property. req.url should be the key.
isCollection: context.isCollection,
resources: context.resources,
req: request,
cacheStore: this.cacheStore
};
return this.cacheMiddleware(ctx, () => {
if (ctx.res) {
return;
.then(Http.errorFilter)
.catch(error => {
// Clear cached token then retry request one more time
if (this.oauth && error && error.status === 401 && !retriedOnAuthError) {
retriedOnAuthError = true;
this.oauth.clearCachedAccessToken();
return execute();
}

throw error;
});

if (!this.cacheMiddleware) {
return promise;
}

return this.prepareRequest(request)
.then(() => this.requestExecutor.fetch(request))
.then(this.errorFilter)
.then(res => ctx.res = res);
})
.then(() => ctx.res);
const ctx = {
uri, // TODO: remove unused property. req.url should be the key.
isCollection: context.isCollection,
resources: context.resources,
req: request,
cacheStore: this.cacheStore
};
return this.cacheMiddleware(ctx, () => {
if (ctx.res) {
return;
}

return promise.then(res => ctx.res = res);
})
.then(() => ctx.res);
};

return execute();
}

delete(uri, request, context) {
Expand Down
4 changes: 2 additions & 2 deletions src/jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ function getPemAndJwk(privateKey) {
}
}

function makeJwt(client) {
function makeJwt(client, endpoint) {
const now = Math.floor(new Date().getTime() / 1000); // seconds since epoch
const plus5Minutes = new Date((now + (5 * 60)) * 1000); // Date object

const claims = {
aud: `${client.baseUrl}/oauth2/v1/token`,
aud: `${client.baseUrl}${endpoint}`,
};
return getPemAndJwk(client.privateKey)
.then(res => {
Expand Down
34 changes: 22 additions & 12 deletions src/oauth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { makeJwt } = require('./jwt');
const Http = require('./http');

function formatParams(obj) {
var str = [];
Expand All @@ -21,11 +22,16 @@ function formatParams(obj) {
class OAuth {
constructor(client) {
this.client = client;
this.jwt = null;
this.accessToken = null;
}

getAccessToken() {
return this.getJwt()
if (this.accessToken) {
return Promise.resolve(this.accessToken);
}

const endpoint = '/oauth2/v1/token';
return this.getJwt(endpoint)
.then(jwt => {
const params = formatParams({
grant_type: 'client_credentials',
Expand All @@ -34,26 +40,30 @@ class OAuth {
client_assertion: jwt
});
return this.client.requestExecutor.fetch({
url: `${this.client.baseUrl}/oauth2/v1/token`,
url: `${this.client.baseUrl}${endpoint}`,
method: 'POST',
body: params,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
}
});
})
.then(Http.errorFilter)
.then(res => res.json())
.then(accessToken => {
this.accessToken = accessToken;
return this.accessToken;
});
}

getJwt() {
if (!this.jwt) {
return makeJwt(this.client)
.then(jwt => {
this.jwt = jwt.compact();
return this.jwt;
});
}
return Promise.resolve(this.jwt);
clearCachedAccessToken() {
this.accessToken = null;
}

getJwt(endpoint) {
return makeJwt(this.client, endpoint)
.then(jwt => jwt.compact());
}
}

Expand Down
Loading

0 comments on commit 161205e

Please sign in to comment.