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

Fix error on tokens renew with refresh token #632

Closed
Closed
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
23 changes: 21 additions & 2 deletions lib/oidc/endpoints/token.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@

import { AuthSdkError } from '../../errors';
import { CustomUrls, OAuthParams, OAuthResponse, TokenParams } from '../../types';
import { CustomUrls, OAuthParams, OAuthResponse, RefreshToken, TokenParams } from '../../types';
import { removeNils, toQueryString } from '../../util';
import http from '../../http';

Expand Down Expand Up @@ -56,4 +55,24 @@ export function postToTokenEndpoint(sdk, options: TokenParams, urls: CustomUrls)
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}

export function postRefreshToken(sdk, options: TokenParams, refreshToken: RefreshToken): Promise<OAuthResponse> {
return http.httpRequest(sdk, {
url: refreshToken.tokenUrl,
method: 'POST',
withCredentials: false,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},

args: Object.entries({
client_id: options.clientId, // eslint-disable-line camelcase
grant_type: 'refresh_token', // eslint-disable-line camelcase
scope: refreshToken.scopes.join(' '),
refresh_token: refreshToken.refreshToken, // eslint-disable-line camelcase
}).map(function ([name, value]) {
return name + '=' + encodeURIComponent(value);
}).join('&'),
});
}
7 changes: 6 additions & 1 deletion lib/oidc/handleOAuthResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ export function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res
responseType = [responseType];
}

var scopes = clone(tokenParams.scopes);
var scopes;
if (res.scope) {
scopes = res.scope.split(' ');
} else {
scopes = clone(tokenParams.scopes);
}
var clientId = tokenParams.clientId || sdk.options.clientId;

// Handling the result from implicit flow or PKCE token exchange
Expand Down
35 changes: 10 additions & 25 deletions lib/oidc/renewTokensWithRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,26 @@
*
*/
import { AuthSdkError } from '../errors';
import http from '../http';
import { getOAuthUrls } from './util/oauth';
import { OktaAuth, TokenParams, RefreshToken, Tokens } from '../types';
import { handleOAuthResponse } from './handleOAuthResponse';
import { postRefreshToken } from './endpoints/token';

export async function renewTokensWithRefresh(
sdk: OktaAuth,
tokenParams: TokenParams,
refreshTokenObject: RefreshToken
): Promise<Tokens> {
var clientId = sdk.options.clientId;
const { clientId } = sdk.options;
if (!clientId) {
throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to revoke a token');
throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to renew tokens');
}

var urls = getOAuthUrls(sdk, tokenParams);


const response = await http.httpRequest(sdk, {
url: refreshTokenObject.tokenUrl,
method: 'POST',
withCredentials: false,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},

args: Object.entries({
client_id: clientId, // eslint-disable-line camelcase
grant_type: 'refresh_token', // eslint-disable-line camelcase
scope: refreshTokenObject.scopes.join(' '),
refresh_token: refreshTokenObject.refreshToken, // eslint-disable-line camelcase
}).map(function ([name, value]) {
return name + '=' + encodeURIComponent(value);
}).join('&'),
const renewTokenParams: TokenParams = Object.assign({}, tokenParams, {
clientId,
});
return handleOAuthResponse(sdk, tokenParams, response, urls).then(res => res.tokens);

}
const tokenResponse = await postRefreshToken(sdk, renewTokenParams, refreshTokenObject);
const urls = getOAuthUrls(sdk, tokenParams);
const { tokens } = await handleOAuthResponse(sdk, renewTokenParams, tokenResponse, urls);
return tokens;
}
1 change: 1 addition & 0 deletions lib/types/OAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface OAuthResponse {
access_token?: string;
id_token?: string;
refresh_token?: string;
scope?: string;
error?: string;
error_description?: string;
}
Expand Down
71 changes: 71 additions & 0 deletions test/spec/oidc/renewTokensWithRefresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { TokenResponse } from './../../../build/lib/types/api.d';
import { OktaAuth } from '@okta/okta-auth-js';
import tokens from '@okta/test.support/tokens';
import util from '@okta/test.support/util';
import * as tokenEndpoint from '../../../lib/oidc/endpoints/token';
import * as renewTokensWithRefreshTokenModule from '../../../lib/oidc/renewTokensWithRefresh';
import * as getWithoutPromptModule from '../../../lib/oidc/getWithoutPrompt';
import oauthUtil from '@okta/test.support/oauthUtil';

describe('renewTokensWithRefresh', function () {
let renewTokenSpy;
let authInstance;

beforeEach(function () {
jest.spyOn(getWithoutPromptModule, 'getWithoutPrompt').mockImplementation(function () {
const tokenResponse: TokenResponse = {
tokens: {},
state: '',
code: ''
};
return Promise.resolve(tokenResponse);
});
jest.spyOn(tokenEndpoint, 'postRefreshToken').mockImplementation(function () {
return Promise.resolve({
'id_token': tokens.standardIdToken,
'refresh_token': tokens.standardRefreshToken2,
'expires_in': '0',
'scope': 'openid email',
});
});
renewTokenSpy = jest.spyOn(renewTokensWithRefreshTokenModule, 'renewTokensWithRefresh');

util.warpToUnixTime(tokens.standardIdToken2Claims.iat);
authInstance = new OktaAuth({
issuer: 'https://auth-js-test.okta.com',
clientId: 'NPSfOkH5eZrTy8PMDlvx',
});
authInstance.tokenManager.clear();
oauthUtil.loadWellKnownAndKeysCache(authInstance);
});


it('is called when refresh token is available in browser storage', async function() {
await authInstance.token.renewTokens();
expect(renewTokenSpy).not.toHaveBeenCalled();

authInstance.tokenManager.add('refreshToken', tokens.standardRefreshTokenParsed);
await authInstance.token.renewTokens();

const renewTokenArguments = renewTokenSpy.mock.calls[0];
expect(renewTokenSpy).toHaveBeenCalled();
expect(renewTokenArguments[2]).toMatchObject(tokens.standardRefreshTokenParsed);
});

it('returns tokens dict', async function() {
authInstance.tokenManager.add('refreshToken', tokens.standardRefreshTokenParsed);

const newTokens = await authInstance.token.renewTokens();
expect(newTokens['idToken']).toEqual(tokens.standardIdTokenParsed);
expect(newTokens['refreshToken']).toEqual(tokens.standardRefreshToken2Parsed);
});

it('throws when SDK has no clientId configured', async function() {
authInstance = new OktaAuth({
issuer: 'https://auth-js-test.okta.com',
});
authInstance.tokenManager.add('refreshToken', tokens.standardRefreshTokenParsed);
await expect(authInstance.token.renewTokens()).rejects.toThrow(
'A clientId must be specified in the OktaAuth constructor to renew tokens');
});
});
8 changes: 8 additions & 0 deletions test/spec/oidc/util/handleOAuthResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ describe('handleOAuthResponse', () => {
expect(res.tokens.refreshToken).toBeTruthy();
expect(res.tokens.refreshToken.refreshToken).toBe('bloo');
});
it('prefers "scope" value from endpoint response over method parameter', async () => {
const tokenParams = { responseType: ['token', 'id_token', 'refresh_token'], scopes: ['profile'] };
const oauthRes = { id_token: 'foo', access_token: 'blar', refresh_token: 'bloo', scope: 'openid offline_access' };
const res = await handleOAuthResponse(sdk, tokenParams, oauthRes, undefined);
expect(res.tokens.accessToken.scopes).toEqual(['openid', 'offline_access']);
expect(res.tokens.idToken.scopes).toEqual(['openid', 'offline_access']);
expect(res.tokens.refreshToken.scopes).toEqual(['openid', 'offline_access']);
});

describe('errors', () => {
beforeEach(() => {
Expand Down
28 changes: 28 additions & 0 deletions test/support/tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,34 @@ tokens.authServerAccessTokenParsed = {
userinfoUrl: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/userinfo'
};

tokens.standardRefreshToken = 'NrJBJ5-89k9CHZ7CJz4Q42yNqoIjm7BclN-1TH_B7z0';
tokens.standardRefreshTokenParsed = {
'value': 'NrJBJ5-89k9CHZ7CJz4Q42yNqoIjm7BclN-1TH_B7z0',
'refreshToken': 'NrJBJ5-89k9CHZ7CJz4Q42yNqoIjm7BclN-1TH_B7z0',
'expiresAt': 1613750805,
'scopes': [
'openid',
'email',
],
'tokenUrl': 'https://auth-js-test.okta.com/oauth2/v1/token',
'authorizeUrl': 'https://auth-js-test.okta.com/oauth2/v1/authorize',
'issuer': 'https://auth-js-test.okta.com'
};

tokens.standardRefreshToken2 = 'fUlkhRyaAFvlsEHXzkz0KYnThBEs-j3yRZwXBwbPTUA';
tokens.standardRefreshToken2Parsed = {
'value': 'fUlkhRyaAFvlsEHXzkz0KYnThBEs-j3yRZwXBwbPTUA',
'refreshToken': 'fUlkhRyaAFvlsEHXzkz0KYnThBEs-j3yRZwXBwbPTUA',
'expiresAt': 1449696330,
'scopes': [
'openid',
'email',
],
'tokenUrl': 'https://auth-js-test.okta.com/oauth2/v1/token',
'authorizeUrl': 'https://auth-js-test.okta.com/oauth2/v1/authorize',
'issuer': 'https://auth-js-test.okta.com'
};

tokens.standardAuthorizationCode = '35cFyfgCU2u0a1EzAqbO';

tokens.standardKey = {
Expand Down