Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle access token refresh - OKTA-291504 #138

Merged
merged 4 commits into from
May 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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