From 8a1dc2abc4e5c91b0b7062dae5eec67d395ed1f3 Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Wed, 28 Oct 2020 11:38:37 -0700 Subject: [PATCH 01/17] Store RT, add scopes to static-spa sample --- lib/AuthStateManager.ts | 4 ++- lib/TokenManager.ts | 34 ++++++++++++++----- lib/browser/browser.ts | 5 +++ lib/constants.ts | 1 + lib/token.ts | 17 ++++++++++ lib/types/AuthState.ts | 3 +- lib/types/OAuth.ts | 1 + lib/types/Token.ts | 15 ++++++-- lib/types/api.ts | 3 +- samples/config.js | 3 +- .../generated/express-web-no-oidc/server.js | 2 +- .../generated/express-web-with-oidc/server.js | 2 +- samples/generated/static-spa/public/app.js | 16 ++++++--- .../generated/static-spa/public/index.html | 6 ++-- samples/generated/static-spa/server.js | 2 +- .../generated/webpack-spa/public/index.html | 2 ++ samples/generated/webpack-spa/src/index.js | 16 ++++++--- samples/templates/express-web/server.js | 2 +- samples/templates/partials/spa/app.js | 16 ++++++--- samples/templates/partials/spa/form.html | 2 ++ samples/templates/static-spa/server.js | 2 +- test/app/server.js | 2 +- 22 files changed, 120 insertions(+), 36 deletions(-) diff --git a/lib/AuthStateManager.ts b/lib/AuthStateManager.ts index 1aaef1afe..a98745170 100644 --- a/lib/AuthStateManager.ts +++ b/lib/AuthStateManager.ts @@ -136,7 +136,7 @@ export class AuthStateManager { }; this._sdk.tokenManager.getTokens() - .then(({ accessToken, idToken }) => { + .then(({ accessToken, idToken, refreshToken }) => { if (cancelablePromise.isCanceled) { resolve(); return; @@ -157,6 +157,7 @@ export class AuthStateManager { const authState = { accessToken, idToken, + refreshToken, isPending, isAuthenticated: !!(accessToken && idToken) }; @@ -169,6 +170,7 @@ export class AuthStateManager { .catch(error => emitAndResolve({ accessToken, idToken, + refreshToken, isAuthenticated: false, isPending: false, error diff --git a/lib/TokenManager.ts b/lib/TokenManager.ts index 677643549..675a62c5e 100644 --- a/lib/TokenManager.ts +++ b/lib/TokenManager.ts @@ -24,9 +24,10 @@ import { TokenType, TokenManagerOptions, isIDToken, - isAccessToken + isAccessToken, + isRefreshToken } from './types'; -import { ID_TOKEN_STORAGE_KEY, ACCESS_TOKEN_STORAGE_KEY } from './constants'; +import { ID_TOKEN_STORAGE_KEY, ACCESS_TOKEN_STORAGE_KEY, REFRESH_TOKEN_STORAGE_KEY } from './constants'; const DEFAULT_OPTIONS = { autoRenew: true, @@ -153,7 +154,7 @@ function validateToken(token: Token) { if (!isObject(token) || !token.scopes || (!token.expiresAt && token.expiresAt !== 0) || - (!isIDToken(token) && !isAccessToken(token))) { + (!isIDToken(token) && !isAccessToken(token) && !isRefreshToken(token))) { throw new AuthSdkError('Token must be an Object with scopes, expiresAt, and an idToken or accessToken properties'); } } @@ -185,7 +186,8 @@ function getKeyByType(storage, type: TokenType): string { const key = Object.keys(tokenStorage).filter(key => { const token = tokenStorage[key]; return (isAccessToken(token) && type === 'accessToken') - || (isIDToken(token) && type === 'idToken'); + || (isIDToken(token) && type === 'idToken') + || (isRefreshToken(token) && type === 'refreshToken'); })[0]; return key; } @@ -206,6 +208,8 @@ function getTokens(storage): Tokens { tokens.accessToken = token; } else if (isIDToken(token)) { tokens.idToken = token; + } else if (isRefreshToken(token)) { + tokens.refreshToken = token; } }); return tokens; @@ -223,9 +227,10 @@ function setTokens( sdk, tokenMgmtRef, storage, - { accessToken, idToken }: Tokens, + { accessToken, idToken, refreshToken }: Tokens, accessTokenCb?: Function, - idTokenCb?: Function + idTokenCb?: Function, + refreshTokenCb?: Function ): void { const handleAdded = (key, token, tokenCb) => { emitAdded(tokenMgmtRef, key, token); @@ -250,11 +255,13 @@ function setTokens( } const idTokenKey = getKeyByType(storage, 'idToken') || ID_TOKEN_STORAGE_KEY; const accessTokenKey = getKeyByType(storage, 'accessToken') || ACCESS_TOKEN_STORAGE_KEY; + const refreshTokenKey = getKeyByType(storage, 'refreshToken') || REFRESH_TOKEN_STORAGE_KEY; // add token to storage const tokenStorage = { ...(idToken && { [idTokenKey]: idToken }), - ...(accessToken && { [accessTokenKey]: accessToken }) + ...(accessToken && { [accessTokenKey]: accessToken }), + ...(refreshToken && { [refreshTokenKey]: refreshToken }) }; storage.setStorage(tokenStorage); @@ -270,6 +277,11 @@ function setTokens( } else if (existingTokens.accessToken) { handleRemoved(accessTokenKey, existingTokens.accessToken, accessTokenCb); } + if (refreshToken) { + handleAdded(refreshTokenKey, refreshToken, refreshTokenCb); + } else if (existingTokens.refreshToken) { + handleRemoved(refreshTokenKey, existingTokens.refreshToken, refreshTokenCb); + } } /* eslint-enable max-params */ @@ -304,10 +316,14 @@ function renew(sdk, tokenMgmtRef, storage, key) { // Remove existing autoRenew timeouts clearExpireEventTimeoutAll(tokenMgmtRef); + // A refresh token means a refresh instead of renewal + // TODO: XXX + // Store the renew promise state, to avoid renewing again - // Renew both tokens in one process + // Renew/refresh all tokens in one process tokenMgmtRef.renewPromise[key] = sdk.token.renewTokens({ - scopes: token.scopes + scopes: token.scopes, + refreshToken: storage.getStorage() }) .then(function(freshTokens) { // store and emit events for freshTokens diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index 331a93f83..00010ddf5 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -426,6 +426,11 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { return accessToken ? accessToken.accessToken : undefined; } + getRefreshToken(): string | undefined { + const { refreshToken } = this.authStateManager.getAuthState(); + return refreshToken ? refreshToken.refreshToken : undefined; + } + /** * Store parsed tokens from redirect url */ diff --git a/lib/constants.ts b/lib/constants.ts index a9ae5d926..f914aa444 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -22,4 +22,5 @@ export const CACHE_STORAGE_NAME = 'okta-cache-storage'; export const PKCE_STORAGE_NAME = 'okta-pkce-storage'; export const ACCESS_TOKEN_STORAGE_KEY = 'accessToken'; export const ID_TOKEN_STORAGE_KEY = 'idToken'; +export const REFRESH_TOKEN_STORAGE_KEY = 'refreshToken'; export const REFERRER_PATH_STORAGE_KEY = 'referrerPath'; diff --git a/lib/token.ts b/lib/token.ts index 0ada3f327..3ca114b51 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -259,6 +259,7 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth var tokenType = res.token_type; var accessToken = res.access_token; var idToken = res.id_token; + var refreshToken = res.refresh_token; if (accessToken) { tokenDict.accessToken = { @@ -272,6 +273,16 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth }; } + if (refreshToken) { + tokenDict.refreshToken = { + refreshToken: refreshToken, + value: refreshToken, + expiresAt: Number(expiresIn) + Math.floor(Date.now()/1000), + scopes: scopes, + authorizeUrl: urls.authorizeUrl, + }; + } + if (idToken) { var jwt = sdk.token.decode(idToken); @@ -703,6 +714,7 @@ function getWithRedirect(sdk: OktaAuth, options: TokenParams): Promise { } function renewToken(sdk: OktaAuth, token: Token): Promise { + // Note: This is not used when a refresh token is present if (!isToken(token)) { return Promise.reject(new AuthSdkError('Renew must be passed a token with ' + 'an array of scopes and an accessToken or idToken')); @@ -746,6 +758,11 @@ function renewTokens(sdk: OktaAuth, options: TokenParams): Promise { options.responseType = ['token', 'id_token']; } + // XXX: Magical detection of refresh token. Passed? + // if( refreshToken ) { + // return renewWithRefreshToken(); + // } + return getWithoutPrompt(sdk, options) .then(res => res.tokens); } diff --git a/lib/types/AuthState.ts b/lib/types/AuthState.ts index 2f0a32197..c5535a085 100644 --- a/lib/types/AuthState.ts +++ b/lib/types/AuthState.ts @@ -1,8 +1,9 @@ -import { AccessToken, IDToken, Token } from './Token'; +import { AccessToken, IDToken, RefreshToken, Token } from './Token'; export interface AuthState { accessToken?: AccessToken; idToken?: IDToken; + refreshToken?: RefreshToken; isAuthenticated?: boolean; isPending?: boolean; error?: Error; diff --git a/lib/types/OAuth.ts b/lib/types/OAuth.ts index e58f80cbc..a35ef790e 100644 --- a/lib/types/OAuth.ts +++ b/lib/types/OAuth.ts @@ -37,6 +37,7 @@ export interface OAuthResponse { token_type?: string; access_token?: string; id_token?: string; + refresh_token?: string; error?: string; error_description?: string; } diff --git a/lib/types/Token.ts b/lib/types/Token.ts index 937a59997..f59a54c12 100644 --- a/lib/types/Token.ts +++ b/lib/types/Token.ts @@ -25,6 +25,10 @@ export interface AccessToken extends AbstractToken { userinfoUrl: string; } +export interface RefreshToken extends AbstractToken { + refreshToken: string; +} + // eslint-disable-next-line @typescript-eslint/interface-name-prefix export interface IDToken extends AbstractToken { idToken: string; @@ -33,13 +37,13 @@ export interface IDToken extends AbstractToken { clientId: string; } -export type Token = AccessToken | IDToken; +export type Token = AccessToken | IDToken | RefreshToken; -export type TokenType = 'accessToken' | 'idToken'; +export type TokenType = 'accessToken' | 'idToken' | 'refreshToken'; export function isToken(obj: any): obj is Token { if (obj && - (obj.accessToken || obj.idToken) && + (obj.accessToken || obj.idToken || obj.refreshToken) && Array.isArray(obj.scopes)) { return true; } @@ -53,3 +57,8 @@ export function isAccessToken(obj: any): obj is AccessToken { export function isIDToken(obj: any): obj is IDToken { return obj && obj.idToken; } + +export function isRefreshToken(obj: any): obj is RefreshToken { + return obj && obj.refreshToken; +} + diff --git a/lib/types/api.ts b/lib/types/api.ts index 7da8babe0..cbc9dce55 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -11,7 +11,7 @@ */ import { AuthTransaction } from '../tx/AuthTransaction'; -import { Token, AccessToken, IDToken } from './Token'; +import { Token, AccessToken, IDToken, RefreshToken } from './Token'; import { JWTObject } from './JWT'; import { UserClaims } from './UserClaims'; import { CustomUrls, OktaAuthOptions } from './OktaAuthOptions'; @@ -98,6 +98,7 @@ export interface TokenParams extends CustomUrls { export interface Tokens { accessToken?: AccessToken; idToken?: IDToken; + refreshToken?: RefreshToken; } export interface TokenResponse { diff --git a/samples/config.js b/samples/config.js index af7213054..552ab1551 100644 --- a/samples/config.js +++ b/samples/config.js @@ -20,6 +20,7 @@ const defaults = { const spaDefaults = Object.assign({ redirectPath: '/implicit/callback', flow: 'redirect', + scopes: 'openid email', storage: 'sessionStorage', requireUserSession: true, signinForm: true, @@ -90,4 +91,4 @@ module.exports = { getModuleVersion, getSampleNames, getSampleConfig -}; \ No newline at end of file +}; diff --git a/samples/generated/express-web-no-oidc/server.js b/samples/generated/express-web-no-oidc/server.js index 36c37ea7a..b3e4dc6b8 100644 --- a/samples/generated/express-web-no-oidc/server.js +++ b/samples/generated/express-web-no-oidc/server.js @@ -64,5 +64,5 @@ app.post('/login', function(req, res) { app.listen(port, function () { - console.log(`Test app running at http://localhost/${port}!\n`); + console.log(`Test app running at http://localhost:${port}!\n`); }); diff --git a/samples/generated/express-web-with-oidc/server.js b/samples/generated/express-web-with-oidc/server.js index e21789f59..2d84b35fc 100644 --- a/samples/generated/express-web-with-oidc/server.js +++ b/samples/generated/express-web-with-oidc/server.js @@ -158,5 +158,5 @@ app.get('/authorization-code/callback', function(req, res) { post.end(); }); app.listen(port, function () { - console.log(`Test app running at http://localhost/${port}!\n`); + console.log(`Test app running at http://localhost:${port}!\n`); }); diff --git a/samples/generated/static-spa/public/app.js b/samples/generated/static-spa/public/app.js index 16bfc4f86..10edf4699 100644 --- a/samples/generated/static-spa/public/app.js +++ b/samples/generated/static-spa/public/app.js @@ -9,6 +9,7 @@ var config = { issuer: '', clientId: '', + scopes: 'openid email', storage: 'sessionStorage', requireUserSession: true, flow: 'redirect' @@ -144,7 +145,6 @@ function handleLoginRedirect() { }); } -// called when the "get user info" link is clicked function getUserInfo() { return authClient.token.getUserInfo() .then(function(value) { @@ -282,7 +282,8 @@ function redirectToGetTokens(additionalParams) { function redirectToLogin(additionalParams) { // Redirect to Okta and show the signin widget if there is no active session authClient.token.getWithRedirect(Object.assign({ - state: JSON.stringify(config.state) + state: JSON.stringify(config.state), + // scopes: config.state.scopes.split(/\s+/) || config.scopes, // getWithRedirect doesn't obey scopes in constructor yet }, additionalParams)); } @@ -299,6 +300,7 @@ function createAuthClient() { issuer: config.issuer, clientId: config.clientId, redirectUri: config.redirectUri, + scopes: config.scopes.split(/\s+/), tokenManager: { storage: config.storage }, @@ -335,6 +337,7 @@ function showForm() { // Set values from config document.getElementById('issuer').value = config.issuer; document.getElementById('clientId').value = config.clientId; + document.getElementById('scopes').value = config.scopes; try { document.querySelector(`#flow [value="${config.flow || ''}"]`).selected = true; } catch (e) { showError(e); } @@ -377,6 +380,7 @@ function loadConfig() { var storage; var flow; var requireUserSession; + var scopes; var state; if (stateParam) { @@ -387,6 +391,7 @@ function loadConfig() { storage = state.storage; flow = state.flow; requireUserSession = state.requireUserSession; + scopes = state.scopes; } else { // Read from URL issuer = url.searchParams.get('issuer') || config.issuer; @@ -395,6 +400,7 @@ function loadConfig() { flow = url.searchParams.get('flow') || config.flow; requireUserSession = url.searchParams.get('requireUserSession') ? url.searchParams.get('requireUserSession') === 'true' : config.requireUserSession; + scopes = url.searchParams.get('scopes') || config.scopes; } // Create a canonical app URI that allows clean reloading with this config appUri = window.location.origin + '/' + @@ -402,7 +408,8 @@ function loadConfig() { '&clientId=' + encodeURIComponent(clientId) + '&storage=' + encodeURIComponent(storage) + '&requireUserSession=' + encodeURIComponent(requireUserSession) + - '&flow=' + encodeURIComponent(flow); + '&flow=' + encodeURIComponent(flow), + '&scopes=' + encodeURIComponent(scopes); // Add all app options to the state, to preserve config across redirects state = { @@ -410,7 +417,8 @@ function loadConfig() { clientId, storage, requireUserSession, - flow + flow, + scopes, }; var newConfig = {}; Object.assign(newConfig, state); diff --git a/samples/generated/static-spa/public/index.html b/samples/generated/static-spa/public/index.html index 2ca0b3175..9d4726728 100644 --- a/samples/generated/static-spa/public/index.html +++ b/samples/generated/static-spa/public/index.html @@ -1,8 +1,8 @@ - - + + @@ -46,6 +46,8 @@
+ +

ON
diff --git a/samples/generated/static-spa/server.js b/samples/generated/static-spa/server.js index 205e42383..8847f1f0d 100644 --- a/samples/generated/static-spa/server.js +++ b/samples/generated/static-spa/server.js @@ -16,5 +16,5 @@ app.use(express.static('./public')); // app html app.use(express.static(authJSAssets)); // okta-auth-js assets app.listen(port, function () { - console.log(`Test app running at http://localhost/${port}!\n`); + console.log(`Test app running at http://localhost:${port}!\n`); }); diff --git a/samples/generated/webpack-spa/public/index.html b/samples/generated/webpack-spa/public/index.html index bb3c0fa28..abb46f25e 100644 --- a/samples/generated/webpack-spa/public/index.html +++ b/samples/generated/webpack-spa/public/index.html @@ -39,6 +39,8 @@
+ +

ON
diff --git a/samples/generated/webpack-spa/src/index.js b/samples/generated/webpack-spa/src/index.js index c881e90c8..7b02552f0 100644 --- a/samples/generated/webpack-spa/src/index.js +++ b/samples/generated/webpack-spa/src/index.js @@ -8,6 +8,7 @@ import OktaSignIn from '@okta/okta-signin-widget'; var config = { issuer: '', clientId: '', + scopes: 'openid email', storage: 'sessionStorage', requireUserSession: true, flow: 'redirect' @@ -143,7 +144,6 @@ function handleLoginRedirect() { }); } -// called when the "get user info" link is clicked function getUserInfo() { return authClient.token.getUserInfo() .then(function(value) { @@ -281,7 +281,8 @@ function redirectToGetTokens(additionalParams) { function redirectToLogin(additionalParams) { // Redirect to Okta and show the signin widget if there is no active session authClient.token.getWithRedirect(Object.assign({ - state: JSON.stringify(config.state) + state: JSON.stringify(config.state), + // scopes: config.state.scopes.split(/\s+/) || config.scopes, // getWithRedirect doesn't obey scopes in constructor yet }, additionalParams)); } @@ -298,6 +299,7 @@ function createAuthClient() { issuer: config.issuer, clientId: config.clientId, redirectUri: config.redirectUri, + scopes: config.scopes.split(/\s+/), tokenManager: { storage: config.storage }, @@ -334,6 +336,7 @@ function showForm() { // Set values from config document.getElementById('issuer').value = config.issuer; document.getElementById('clientId').value = config.clientId; + document.getElementById('scopes').value = config.scopes; try { document.querySelector(`#flow [value="${config.flow || ''}"]`).selected = true; } catch (e) { showError(e); } @@ -376,6 +379,7 @@ function loadConfig() { var storage; var flow; var requireUserSession; + var scopes; var state; if (stateParam) { @@ -386,6 +390,7 @@ function loadConfig() { storage = state.storage; flow = state.flow; requireUserSession = state.requireUserSession; + scopes = state.scopes; } else { // Read from URL issuer = url.searchParams.get('issuer') || config.issuer; @@ -394,6 +399,7 @@ function loadConfig() { flow = url.searchParams.get('flow') || config.flow; requireUserSession = url.searchParams.get('requireUserSession') ? url.searchParams.get('requireUserSession') === 'true' : config.requireUserSession; + scopes = url.searchParams.get('scopes') || config.scopes; } // Create a canonical app URI that allows clean reloading with this config appUri = window.location.origin + '/' + @@ -401,7 +407,8 @@ function loadConfig() { '&clientId=' + encodeURIComponent(clientId) + '&storage=' + encodeURIComponent(storage) + '&requireUserSession=' + encodeURIComponent(requireUserSession) + - '&flow=' + encodeURIComponent(flow); + '&flow=' + encodeURIComponent(flow), + '&scopes=' + encodeURIComponent(scopes); // Add all app options to the state, to preserve config across redirects state = { @@ -409,7 +416,8 @@ function loadConfig() { clientId, storage, requireUserSession, - flow + flow, + scopes, }; var newConfig = {}; Object.assign(newConfig, state); diff --git a/samples/templates/express-web/server.js b/samples/templates/express-web/server.js index 2c19072aa..0a2ea4557 100644 --- a/samples/templates/express-web/server.js +++ b/samples/templates/express-web/server.js @@ -108,5 +108,5 @@ app.post('/login', function(req, res) { {{/if}} app.listen(port, function () { - console.log(`Test app running at http://localhost/${port}!\n`); + console.log(`Test app running at http://localhost:${port}!\n`); }); diff --git a/samples/templates/partials/spa/app.js b/samples/templates/partials/spa/app.js index c9f60269b..a2bf6fde1 100644 --- a/samples/templates/partials/spa/app.js +++ b/samples/templates/partials/spa/app.js @@ -4,6 +4,7 @@ var config = { issuer: '', clientId: '', + scopes: '{{ scopes }}', storage: '{{ storage }}', requireUserSession: {{ requireUserSession }}, flow: '{{ flow }}' @@ -141,7 +142,6 @@ function handleLoginRedirect() { }); } -// called when the "get user info" link is clicked function getUserInfo() { return authClient.token.getUserInfo() .then(function(value) { @@ -287,7 +287,8 @@ function redirectToGetTokens(additionalParams) { function redirectToLogin(additionalParams) { // Redirect to Okta and show the signin widget if there is no active session authClient.token.getWithRedirect(Object.assign({ - state: JSON.stringify(config.state) + state: JSON.stringify(config.state), + // scopes: config.state.scopes.split(/\s+/) || config.scopes, // getWithRedirect doesn't obey scopes in constructor yet }, additionalParams)); } @@ -304,6 +305,7 @@ function createAuthClient() { issuer: config.issuer, clientId: config.clientId, redirectUri: config.redirectUri, + scopes: config.scopes.split(/\s+/), tokenManager: { storage: config.storage }, @@ -340,6 +342,7 @@ function showForm() { // Set values from config document.getElementById('issuer').value = config.issuer; document.getElementById('clientId').value = config.clientId; + document.getElementById('scopes').value = config.scopes; try { document.querySelector(`#flow [value="${config.flow || ''}"]`).selected = true; } catch (e) { showError(e); } @@ -382,6 +385,7 @@ function loadConfig() { var storage; var flow; var requireUserSession; + var scopes; var state; if (stateParam) { @@ -392,6 +396,7 @@ function loadConfig() { storage = state.storage; flow = state.flow; requireUserSession = state.requireUserSession; + scopes = state.scopes; } else { // Read from URL issuer = url.searchParams.get('issuer') || config.issuer; @@ -400,6 +405,7 @@ function loadConfig() { flow = url.searchParams.get('flow') || config.flow; requireUserSession = url.searchParams.get('requireUserSession') ? url.searchParams.get('requireUserSession') === 'true' : config.requireUserSession; + scopes = url.searchParams.get('scopes') || config.scopes; } // Create a canonical app URI that allows clean reloading with this config appUri = window.location.origin + '/' + @@ -407,7 +413,8 @@ function loadConfig() { '&clientId=' + encodeURIComponent(clientId) + '&storage=' + encodeURIComponent(storage) + '&requireUserSession=' + encodeURIComponent(requireUserSession) + - '&flow=' + encodeURIComponent(flow); + '&flow=' + encodeURIComponent(flow), + '&scopes=' + encodeURIComponent(scopes); // Add all app options to the state, to preserve config across redirects state = { @@ -415,7 +422,8 @@ function loadConfig() { clientId, storage, requireUserSession, - flow + flow, + scopes, }; var newConfig = {}; Object.assign(newConfig, state); diff --git a/samples/templates/partials/spa/form.html b/samples/templates/partials/spa/form.html index 1aa0c090c..9bee3f3a6 100644 --- a/samples/templates/partials/spa/form.html +++ b/samples/templates/partials/spa/form.html @@ -14,6 +14,8 @@ {{/if}}
+ +

ON
diff --git a/samples/templates/static-spa/server.js b/samples/templates/static-spa/server.js index 008758517..cf2e3ef06 100644 --- a/samples/templates/static-spa/server.js +++ b/samples/templates/static-spa/server.js @@ -16,5 +16,5 @@ app.use(express.static('./public')); // app html app.use(express.static(authJSAssets)); // okta-auth-js assets app.listen(port, function () { - console.log(`Test app running at http://localhost/${port}!\n`); + console.log(`Test app running at http://localhost:${port}!\n`); }); diff --git a/test/app/server.js b/test/app/server.js index 1131fafe1..cb152de83 100644 --- a/test/app/server.js +++ b/test/app/server.js @@ -65,5 +65,5 @@ app.post('/login', function(req, res) { const port = config.devServer.port; app.listen(port, function () { - console.log(`Test app running at http://localhost/${port}!\n`); + console.log(`Test app running at http://localhost:${port}!\n`); }); From a0598bb459e2f548fa8fa519ae902cd2b9c1abda Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Thu, 29 Oct 2020 12:53:00 -0700 Subject: [PATCH 02/17] base pattern for renewTokensWithRefresh --- lib/TokenManager.ts | 2 +- lib/browser/browser.ts | 3 +- lib/token.ts | 36 +++++++++++++++++++--- lib/types/Token.ts | 5 +-- samples/generated/static-spa/public/app.js | 1 + samples/generated/webpack-spa/src/index.js | 1 + samples/templates/partials/spa/app.js | 1 + 7 files changed, 40 insertions(+), 9 deletions(-) diff --git a/lib/TokenManager.ts b/lib/TokenManager.ts index 675a62c5e..f06899c89 100644 --- a/lib/TokenManager.ts +++ b/lib/TokenManager.ts @@ -155,7 +155,7 @@ function validateToken(token: Token) { !token.scopes || (!token.expiresAt && token.expiresAt !== 0) || (!isIDToken(token) && !isAccessToken(token) && !isRefreshToken(token))) { - throw new AuthSdkError('Token must be an Object with scopes, expiresAt, and an idToken or accessToken properties'); + throw new AuthSdkError('Token must be an Object with scopes, expiresAt, and one of: an idToken, accessToken, or refreshToken property'); } } diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index 00010ddf5..7bc5a2df8 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -62,6 +62,7 @@ import { OktaAuthOptions, AccessToken, IDToken, + RefreshToken, TokenAPI, FeaturesAPI, SignoutAPI, @@ -430,7 +431,7 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { const { refreshToken } = this.authStateManager.getAuthState(); return refreshToken ? refreshToken.refreshToken : undefined; } - + /** * Store parsed tokens from redirect url */ diff --git a/lib/token.ts b/lib/token.ts index 3ca114b51..b99f4c6ad 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -58,7 +58,8 @@ import { CustomUrls, PKCEMeta, ParseFromUrlOptions, - Tokens + Tokens, + RefreshToken } from './types'; const cookies = browserStorage.storage; @@ -263,7 +264,6 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth if (accessToken) { tokenDict.accessToken = { - value: accessToken, accessToken: accessToken, expiresAt: Number(expiresIn) + Math.floor(Date.now()/1000), tokenType: tokenType, @@ -276,10 +276,9 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth if (refreshToken) { tokenDict.refreshToken = { refreshToken: refreshToken, - value: refreshToken, expiresAt: Number(expiresIn) + Math.floor(Date.now()/1000), scopes: scopes, - authorizeUrl: urls.authorizeUrl, + tokenUrl: urls.tokenUrl, }; } @@ -287,7 +286,6 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth var jwt = sdk.token.decode(idToken); var idTokenObj: IDToken = { - value: idToken, idToken: idToken, claims: jwt.payload, expiresAt: jwt.payload.exp, @@ -715,6 +713,7 @@ function getWithRedirect(sdk: OktaAuth, options: TokenParams): Promise { function renewToken(sdk: OktaAuth, token: Token): Promise { // Note: This is not used when a refresh token is present + console.log('renewing', token); if (!isToken(token)) { return Promise.reject(new AuthSdkError('Renew must be passed a token with ' + 'an array of scopes and an accessToken or idToken')); @@ -744,7 +743,34 @@ function renewToken(sdk: OktaAuth, token: Token): Promise { }); } +// async function renewTokensWithRefresh(sdk: OktaAuth, refreshTokenObject: RefreshToken): Promise { +async function renewTokensWithRefresh(sdk: OktaAuth, refreshTokenObject: RefreshToken): Promise { + console.log('starting use of refresh token', refreshTokenObject.tokenUrl); + var url = refreshTokenObject.tokenUrl + "?" + + Object.entries({ + grant_type: 'refresh_token', + refresh_token: refreshTokenObject.refreshToken, + scope: refreshTokenObject.scopes.join(' '), + }).map( function(name, value) { + return name + "=" + encodeURIComponent(value); + }) + .join('&'); + console.log({url}); + // try { + // const result = await http.httpRequest(sdk, { + // url: refreshTokenObject.refreshUrl, + // method: 'GET', + // refreshToken: refreshTokenObject.refreshToken + // }); + // console.log('result', result); + // return result; + // } catch (err) { + // console.log({ err }); + // } +} + function renewTokens(sdk: OktaAuth, options: TokenParams): Promise { + console.log('renewing all tokens?'); options = Object.assign({ scopes: sdk.options.scopes, authorizeUrl: sdk.options.authorizeUrl, diff --git a/lib/types/Token.ts b/lib/types/Token.ts index f59a54c12..ecaf693ba 100644 --- a/lib/types/Token.ts +++ b/lib/types/Token.ts @@ -14,19 +14,19 @@ import { UserClaims } from './UserClaims'; export interface AbstractToken { expiresAt: number; - value: string; - authorizeUrl: string; scopes: string[]; } export interface AccessToken extends AbstractToken { accessToken: string; tokenType: string; + authorizeUrl: string; userinfoUrl: string; } export interface RefreshToken extends AbstractToken { refreshToken: string; + tokenUrl: string; } // eslint-disable-next-line @typescript-eslint/interface-name-prefix @@ -35,6 +35,7 @@ export interface IDToken extends AbstractToken { claims: UserClaims; issuer: string; clientId: string; + authorizeUrl: string; } export type Token = AccessToken | IDToken | RefreshToken; diff --git a/samples/generated/static-spa/public/app.js b/samples/generated/static-spa/public/app.js index 10edf4699..091fef5ed 100644 --- a/samples/generated/static-spa/public/app.js +++ b/samples/generated/static-spa/public/app.js @@ -302,6 +302,7 @@ function createAuthClient() { redirectUri: config.redirectUri, scopes: config.scopes.split(/\s+/), tokenManager: { + expireEarlySeconds: 3600, // DEBUG storage: config.storage }, transformAuthState diff --git a/samples/generated/webpack-spa/src/index.js b/samples/generated/webpack-spa/src/index.js index 7b02552f0..7639d52cc 100644 --- a/samples/generated/webpack-spa/src/index.js +++ b/samples/generated/webpack-spa/src/index.js @@ -301,6 +301,7 @@ function createAuthClient() { redirectUri: config.redirectUri, scopes: config.scopes.split(/\s+/), tokenManager: { + expireEarlySeconds: 3600, // DEBUG storage: config.storage }, transformAuthState diff --git a/samples/templates/partials/spa/app.js b/samples/templates/partials/spa/app.js index a2bf6fde1..df02eb54e 100644 --- a/samples/templates/partials/spa/app.js +++ b/samples/templates/partials/spa/app.js @@ -307,6 +307,7 @@ function createAuthClient() { redirectUri: config.redirectUri, scopes: config.scopes.split(/\s+/), tokenManager: { + expireEarlySeconds: 3600, // DEBUG storage: config.storage }, transformAuthState From b66ce6e9101011025903863d22a193b0e7135131 Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Fri, 30 Oct 2020 12:02:43 -0700 Subject: [PATCH 03/17] renew successfully (once) --- lib/AuthStateManager.ts | 1 + lib/browser/browser.ts | 2 +- lib/token.ts | 664 +++++++++--------- lib/types/Token.ts | 4 +- samples/generated/static-spa/public/app.js | 3 +- .../generated/static-spa/public/index.html | 4 +- samples/generated/webpack-spa/src/index.js | 3 +- samples/templates/partials/spa/app.js | 3 +- 8 files changed, 346 insertions(+), 338 deletions(-) diff --git a/lib/AuthStateManager.ts b/lib/AuthStateManager.ts index a98745170..86dfcfeaf 100644 --- a/lib/AuthStateManager.ts +++ b/lib/AuthStateManager.ts @@ -10,6 +10,7 @@ export const DEFAULT_AUTH_STATE = { isAuthenticated: false, idToken: null, accessToken: null, + refreshToken: null, }; const DEFAULT_PENDING = { updateAuthStatePromise: null, diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index 7bc5a2df8..348a89633 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -339,7 +339,7 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { // Clear all local tokens this.tokenManager.clear(); - + // TODO: handle refresh token if (revokeAccessToken && accessToken) { await this.revokeAccessToken(accessToken); } diff --git a/lib/token.ts b/lib/token.ts index b99f4c6ad..3796491b6 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -67,29 +67,29 @@ const cookies = browserStorage.storage; // Only the access token can be revoked in SPA applications function revokeToken(sdk: OktaAuth, token: AccessToken): Promise { return Promise.resolve() - .then(function() { - if (!token || !token.accessToken) { - throw new AuthSdkError('A valid access token object is required'); - } - var clientId = sdk.options.clientId; - if (!clientId) { - throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to revoke a token'); - } - var revokeUrl = getOAuthUrls(sdk).revokeUrl; - var accessToken = token.accessToken; - var args = toQueryString({ - // eslint-disable-next-line camelcase - token_type_hint: 'access_token', - token: accessToken - }).slice(1); - var creds = btoa(clientId); - return http.post(sdk, revokeUrl, args, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': 'Basic ' + creds + .then(function () { + if (!token || !token.accessToken) { + throw new AuthSdkError('A valid access token object is required'); } + var clientId = sdk.options.clientId; + if (!clientId) { + throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to revoke a token'); + } + var revokeUrl = getOAuthUrls(sdk).revokeUrl; + var accessToken = token.accessToken; + var args = toQueryString({ + // eslint-disable-next-line camelcase + token_type_hint: 'access_token', + token: accessToken + }).slice(1); + var creds = btoa(clientId); + return http.post(sdk, revokeUrl, args, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic ' + creds + } + }); }); - }); } function decodeToken(token: string): JWTObject { @@ -102,7 +102,7 @@ function decodeToken(token: string): JWTObject { payload: JSON.parse(base64UrlToString(jwt[1])), signature: jwt[2] }; - } catch(e) { + } catch (e) { throw new AuthSdkError('Malformed token'); } @@ -112,57 +112,57 @@ function decodeToken(token: string): JWTObject { // Verify the id token function verifyToken(sdk: OktaAuth, token: IDToken, validationParams: TokenVerifyParams): Promise { return Promise.resolve() - .then(function() { - if (!token || !token.idToken) { - throw new AuthSdkError('Only idTokens may be verified'); - } - - var jwt = decodeToken(token.idToken); + .then(function () { + if (!token || !token.idToken) { + throw new AuthSdkError('Only idTokens may be verified'); + } - var validationOptions: TokenVerifyParams = { - clientId: sdk.options.clientId, - issuer: sdk.options.issuer, - ignoreSignature: sdk.options.ignoreSignature - }; + var jwt = decodeToken(token.idToken); - Object.assign(validationOptions, validationParams); + var validationOptions: TokenVerifyParams = { + clientId: sdk.options.clientId, + issuer: sdk.options.issuer, + ignoreSignature: sdk.options.ignoreSignature + }; - // Standard claim validation - validateClaims(sdk, jwt.payload, validationOptions); + Object.assign(validationOptions, validationParams); - // If the browser doesn't support native crypto or we choose not - // to verify the signature, bail early - if (validationOptions.ignoreSignature == true || !sdk.features.isTokenVerifySupported()) { - return token; - } + // Standard claim validation + validateClaims(sdk, jwt.payload, validationOptions); - return getKey(sdk, token.issuer, jwt.header.kid) - .then(function(key) { - return sdkCrypto.verifyToken(token.idToken, key); - }) - .then(function(valid) { - if (!valid) { - throw new AuthSdkError('The token signature is not valid'); + // If the browser doesn't support native crypto or we choose not + // to verify the signature, bail early + if (validationOptions.ignoreSignature == true || !sdk.features.isTokenVerifySupported()) { + return token; } - if (validationParams && validationParams.accessToken && token.claims.at_hash) { - return sdkCrypto.getOidcHash(validationParams.accessToken) - .then(hash => { - if (hash !== token.claims.at_hash) { - throw new AuthSdkError('Token hash verification failed'); - } - }); - } - }) - .then(() => { - return token; + + return getKey(sdk, token.issuer, jwt.header.kid) + .then(function (key) { + return sdkCrypto.verifyToken(token.idToken, key); + }) + .then(function (valid) { + if (!valid) { + throw new AuthSdkError('The token signature is not valid'); + } + if (validationParams && validationParams.accessToken && token.claims.at_hash) { + return sdkCrypto.getOidcHash(validationParams.accessToken) + .then(hash => { + if (hash !== token.claims.at_hash) { + throw new AuthSdkError('Token hash verification failed'); + } + }); + } + }) + .then(() => { + return token; + }); }); - }); } function addPostMessageListener(sdk: OktaAuth, timeout, state) { var responseHandler; var timeoutId; - var msgReceivedOrTimeout = new Promise(function(resolve, reject) { + var msgReceivedOrTimeout = new Promise(function (resolve, reject) { responseHandler = function responseHandler(e) { if (!e.data || e.data.state !== state) { @@ -182,13 +182,13 @@ function addPostMessageListener(sdk: OktaAuth, timeout, state) { addListener(window, 'message', responseHandler); - timeoutId = setTimeout(function() { + timeoutId = setTimeout(function () { reject(new AuthSdkError('OAuth flow timed out')); }, timeout || 120000); }); return msgReceivedOrTimeout - .finally(function() { + .finally(function () { clearTimeout(timeoutId); removeListener(window, 'message', responseHandler); }); @@ -205,13 +205,13 @@ function exchangeCodeForToken(sdk: OktaAuth, oauthParams: TokenParams, authoriza redirectUri: meta.redirectUri }; return PKCE.getToken(sdk, getTokenParams, urls) - .then(function(res) { - validateResponse(res, getTokenParams); - return res; - }) - .finally(function() { - PKCE.clearMeta(sdk); - }); + .then(function (res) { + validateResponse(res, getTokenParams); + return res; + }) + .finally(function () { + PKCE.clearMeta(sdk); + }); } function validateResponse(res: OAuthResponse, oauthParams: TokenParams) { @@ -238,100 +238,102 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth var pkce = sdk.options.pkce !== false; return Promise.resolve() - .then(function() { - validateResponse(res, tokenParams); - - // PKCE flow - // We do not support "hybrid" scenarios where the response includes both a code and a token. - // If the response contains a code it is used immediately to obtain new tokens. - if (res.code && pkce) { - // responseType is not sent to the token endpoint. - // We populate this array to validate the response below - responseType = ['token']; // an accessToken will always be returned - if (scopes.indexOf('openid') !== -1) { - responseType.push('id_token'); // an idToken will be returned if "openid" is in the scopes + .then(function () { + validateResponse(res, tokenParams); + + // PKCE flow + // We do not support "hybrid" scenarios where the response includes both a code and a token. + // If the response contains a code it is used immediately to obtain new tokens. + if (res.code && pkce) { + // responseType is not sent to the token endpoint. + // We populate this array to validate the response below + responseType = ['token']; // an accessToken will always be returned + if (scopes.indexOf('openid') !== -1) { + responseType.push('id_token'); // an idToken will be returned if "openid" is in the scopes + } + return exchangeCodeForToken(sdk, tokenParams, res.code, urls); + } + return res; + }).then(function (res: OAuthResponse) { + var tokenDict = {} as Tokens; + var expiresIn = res.expires_in; + var tokenType = res.token_type; + var accessToken = res.access_token; + var idToken = res.id_token; + var refreshToken = res.refresh_token; + + if (accessToken) { + tokenDict.accessToken = { + accessToken: accessToken, + expiresAt: Number(expiresIn) + Math.floor(Date.now() / 1000), + tokenType: tokenType, + scopes: scopes, + authorizeUrl: urls.authorizeUrl, + userinfoUrl: urls.userinfoUrl + }; } - return exchangeCodeForToken(sdk, tokenParams, res.code, urls); - } - return res; - }).then(function(res: OAuthResponse) { - var tokenDict = {} as Tokens; - var expiresIn = res.expires_in; - var tokenType = res.token_type; - var accessToken = res.access_token; - var idToken = res.id_token; - var refreshToken = res.refresh_token; - - if (accessToken) { - tokenDict.accessToken = { - accessToken: accessToken, - expiresAt: Number(expiresIn) + Math.floor(Date.now()/1000), - tokenType: tokenType, - scopes: scopes, - authorizeUrl: urls.authorizeUrl, - userinfoUrl: urls.userinfoUrl - }; - } - if (refreshToken) { - tokenDict.refreshToken = { - refreshToken: refreshToken, - expiresAt: Number(expiresIn) + Math.floor(Date.now()/1000), - scopes: scopes, - tokenUrl: urls.tokenUrl, - }; - } + if (refreshToken) { + tokenDict.refreshToken = { + refreshToken: refreshToken, + expiresAt: Number(expiresIn) + Math.floor(Date.now() / 1000), + scopes: scopes, + tokenUrl: urls.tokenUrl, + authorizeUrl: urls.authorizeUrl, + issuer: urls.issuer, + }; + } - if (idToken) { - var jwt = sdk.token.decode(idToken); - - var idTokenObj: IDToken = { - idToken: idToken, - claims: jwt.payload, - expiresAt: jwt.payload.exp, - scopes: scopes, - authorizeUrl: urls.authorizeUrl, - issuer: urls.issuer, - clientId: clientId - }; + if (idToken) { + var jwt = sdk.token.decode(idToken); + + var idTokenObj: IDToken = { + idToken: idToken, + claims: jwt.payload, + expiresAt: jwt.payload.exp, + scopes: scopes, + authorizeUrl: urls.authorizeUrl, + issuer: urls.issuer, + clientId: clientId + }; - var validationParams: TokenVerifyParams = { - clientId: clientId, - issuer: urls.issuer, - nonce: tokenParams.nonce, - accessToken: accessToken - }; + var validationParams: TokenVerifyParams = { + clientId: clientId, + issuer: urls.issuer, + nonce: tokenParams.nonce, + accessToken: accessToken + }; - if (tokenParams.ignoreSignature !== undefined) { - validationParams.ignoreSignature = tokenParams.ignoreSignature; - } + if (tokenParams.ignoreSignature !== undefined) { + validationParams.ignoreSignature = tokenParams.ignoreSignature; + } - return verifyToken(sdk, idTokenObj, validationParams) - .then(function() { - tokenDict.idToken = idTokenObj; - return tokenDict; - }); - } + return verifyToken(sdk, idTokenObj, validationParams) + .then(function () { + tokenDict.idToken = idTokenObj; + return tokenDict; + }); + } - return tokenDict; - }) - .then(function(tokenDict) { - // Validate received tokens against requested response types - if (responseType.indexOf('token') !== -1 && !tokenDict.accessToken) { - // eslint-disable-next-line max-len - throw new AuthSdkError('Unable to parse OAuth flow response: response type "token" was requested but "access_token" was not returned.'); - } - if (responseType.indexOf('id_token') !== -1 && !tokenDict.idToken) { - // eslint-disable-next-line max-len - throw new AuthSdkError('Unable to parse OAuth flow response: response type "id_token" was requested but "id_token" was not returned.'); - } + return tokenDict; + }) + .then(function (tokenDict) { + // Validate received tokens against requested response types + if (responseType.indexOf('token') !== -1 && !tokenDict.accessToken) { + // eslint-disable-next-line max-len + throw new AuthSdkError('Unable to parse OAuth flow response: response type "token" was requested but "access_token" was not returned.'); + } + if (responseType.indexOf('id_token') !== -1 && !tokenDict.idToken) { + // eslint-disable-next-line max-len + throw new AuthSdkError('Unable to parse OAuth flow response: response type "id_token" was requested but "id_token" was not returned.'); + } - return { - tokens: tokenDict, - state: res.state, - code: res.code - }; - }); + return { + tokens: tokenDict, + state: res.state, + code: res.code + }; + }); } function getDefaultTokenParams(sdk: OktaAuth): TokenParams { @@ -379,7 +381,7 @@ function convertTokenParamsToOAuthParams(tokenParams: TokenParams) { }; oauthParams = removeNils(oauthParams) as OAuthParams; - ['idp_scope', 'response_type'].forEach( function( mayBeArray ) { + ['idp_scope', 'response_type'].forEach(function (mayBeArray) { if (Array.isArray(oauthParams[mayBeArray])) { oauthParams[mayBeArray] = oauthParams[mayBeArray].join(' '); } @@ -454,123 +456,123 @@ function getToken(sdk: OktaAuth, options: TokenParams) { if (arguments.length > 2) { return Promise.reject(new AuthSdkError('As of version 3.0, "getToken" takes only a single set of options')); } - + options = options || {}; return prepareTokenParams(sdk, options) - .then(function(tokenParams: TokenParams) { + .then(function (tokenParams: TokenParams) { - // Start overriding any options that don't make sense - var sessionTokenOverrides = { - prompt: 'none', - responseMode: 'okta_post_message', - display: null - }; + // Start overriding any options that don't make sense + var sessionTokenOverrides = { + prompt: 'none', + responseMode: 'okta_post_message', + display: null + }; - var idpOverrides = { - display: 'popup' - }; + var idpOverrides = { + display: 'popup' + }; - if (options.sessionToken) { - Object.assign(tokenParams, sessionTokenOverrides); - } else if (options.idp) { - Object.assign(tokenParams, idpOverrides); - } + if (options.sessionToken) { + Object.assign(tokenParams, sessionTokenOverrides); + } else if (options.idp) { + Object.assign(tokenParams, idpOverrides); + } - // Use the query params to build the authorize url - var requestUrl, + // Use the query params to build the authorize url + var requestUrl, endpoint, urls; - // Get authorizeUrl and issuer - urls = getOAuthUrls(sdk, tokenParams); - endpoint = options.codeVerifier ? urls.tokenUrl : urls.authorizeUrl; - requestUrl = endpoint + buildAuthorizeParams(tokenParams); - - // Determine the flow type - var flowType; - if (tokenParams.sessionToken || tokenParams.display === null) { - flowType = 'IFRAME'; - } else if (tokenParams.display === 'popup') { - flowType = 'POPUP'; - } else { - flowType = 'IMPLICIT'; - } + // Get authorizeUrl and issuer + urls = getOAuthUrls(sdk, tokenParams); + endpoint = options.codeVerifier ? urls.tokenUrl : urls.authorizeUrl; + requestUrl = endpoint + buildAuthorizeParams(tokenParams); + + // Determine the flow type + var flowType; + if (tokenParams.sessionToken || tokenParams.display === null) { + flowType = 'IFRAME'; + } else if (tokenParams.display === 'popup') { + flowType = 'POPUP'; + } else { + flowType = 'IMPLICIT'; + } - // Execute the flow type - switch (flowType) { - case 'IFRAME': - var iframePromise = addPostMessageListener(sdk, options.timeout, tokenParams.state); - var iframeEl = loadFrame(requestUrl); - return iframePromise - .then(function(res) { - return handleOAuthResponse(sdk, tokenParams, res, urls); - }) - .finally(function() { - if (document.body.contains(iframeEl)) { - iframeEl.parentElement.removeChild(iframeEl); + // Execute the flow type + switch (flowType) { + case 'IFRAME': + var iframePromise = addPostMessageListener(sdk, options.timeout, tokenParams.state); + var iframeEl = loadFrame(requestUrl); + return iframePromise + .then(function (res) { + return handleOAuthResponse(sdk, tokenParams, res, urls); + }) + .finally(function () { + if (document.body.contains(iframeEl)) { + iframeEl.parentElement.removeChild(iframeEl); + } + }); + + case 'POPUP': + var oauthPromise; // resolves with OAuth response + + // Add listener on postMessage before window creation, so + // postMessage isn't triggered before we're listening + if (tokenParams.responseMode === 'okta_post_message') { + if (!sdk.features.isPopupPostMessageSupported()) { + throw new AuthSdkError('This browser doesn\'t have full postMessage support'); } - }); - - case 'POPUP': - var oauthPromise; // resolves with OAuth response - - // Add listener on postMessage before window creation, so - // postMessage isn't triggered before we're listening - if (tokenParams.responseMode === 'okta_post_message') { - if (!sdk.features.isPopupPostMessageSupported()) { - throw new AuthSdkError('This browser doesn\'t have full postMessage support'); + oauthPromise = addPostMessageListener(sdk, options.timeout, tokenParams.state); } - oauthPromise = addPostMessageListener(sdk, options.timeout, tokenParams.state); - } - // Create the window - var windowOptions = { - popupTitle: options.popupTitle - }; - var windowEl = loadPopup(requestUrl, windowOptions); - - // The popup may be closed without receiving an OAuth response. Setup a poller to monitor the window. - var popupPromise = new Promise(function(resolve, reject) { - var closePoller = setInterval(function() { - if (!windowEl || windowEl.closed) { - clearInterval(closePoller); - reject(new AuthSdkError('Unable to parse OAuth flow response')); - } - }, 100); - - // Proxy the OAuth promise results - oauthPromise - .then(function(res) { - clearInterval(closePoller); - resolve(res); - }) - .catch(function(err) { - clearInterval(closePoller); - reject(err); - }); - }); - - return popupPromise - .then(function(res) { - return handleOAuthResponse(sdk, tokenParams, res, urls); - }) - .finally(function() { - if (windowEl && !windowEl.closed) { - windowEl.close(); - } + // Create the window + var windowOptions = { + popupTitle: options.popupTitle + }; + var windowEl = loadPopup(requestUrl, windowOptions); + + // The popup may be closed without receiving an OAuth response. Setup a poller to monitor the window. + var popupPromise = new Promise(function (resolve, reject) { + var closePoller = setInterval(function () { + if (!windowEl || windowEl.closed) { + clearInterval(closePoller); + reject(new AuthSdkError('Unable to parse OAuth flow response')); + } + }, 100); + + // Proxy the OAuth promise results + oauthPromise + .then(function (res) { + clearInterval(closePoller); + resolve(res); + }) + .catch(function (err) { + clearInterval(closePoller); + reject(err); + }); }); - default: - throw new AuthSdkError('The full page redirect flow is not supported'); - } - }) - .catch(e => { - if (sdk.options.pkce) { - PKCE.clearMeta(sdk); - } - throw e; - }); + return popupPromise + .then(function (res) { + return handleOAuthResponse(sdk, tokenParams, res, urls); + }) + .finally(function () { + if (windowEl && !windowEl.closed) { + windowEl.close(); + } + }); + + default: + throw new AuthSdkError('The full page redirect flow is not supported'); + } + }) + .catch(e => { + if (sdk.options.pkce) { + PKCE.clearMeta(sdk); + } + throw e; + }); } function getWithoutPrompt(sdk: OktaAuth, options: TokenParams): Promise { @@ -640,13 +642,13 @@ function prepareTokenParams(sdk: OktaAuth, options: TokenParams): Promise { options = clone(options) || {}; return prepareTokenParams(sdk, options) - .then(function(tokenParams: TokenParams) { + .then(function (tokenParams: TokenParams) { var urls = getOAuthUrls(sdk, options); var requestUrl = urls.authorizeUrl + buildAuthorizeParams(tokenParams); @@ -713,7 +715,6 @@ function getWithRedirect(sdk: OktaAuth, options: TokenParams): Promise { function renewToken(sdk: OktaAuth, token: Token): Promise { // Note: This is not used when a refresh token is present - console.log('renewing', token); if (!isToken(token)) { return Promise.reject(new AuthSdkError('Renew must be passed a token with ' + 'an array of scopes and an accessToken or idToken')); @@ -736,41 +737,55 @@ function renewToken(sdk: OktaAuth, token: Token): Promise { userinfoUrl, issuer }) - .then(function(res) { - // Multiple tokens may have come back. Return only the token which was requested. - var tokens = res.tokens; - return isIDToken(token) ? tokens.idToken : tokens.accessToken; - }); + .then(function (res) { + // Multiple tokens may have come back. Return only the token which was requested. + var tokens = res.tokens; + return isIDToken(token) ? tokens.idToken : tokens.accessToken; + }); } -// async function renewTokensWithRefresh(sdk: OktaAuth, refreshTokenObject: RefreshToken): Promise { -async function renewTokensWithRefresh(sdk: OktaAuth, refreshTokenObject: RefreshToken): Promise { - console.log('starting use of refresh token', refreshTokenObject.tokenUrl); - var url = refreshTokenObject.tokenUrl + "?" + - Object.entries({ +async function renewTokensWithRefresh(sdk: OktaAuth, options: TokenParams, refreshTokenObject: RefreshToken): Promise { + var clientId = sdk.options.clientId; + if (!clientId) { + throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to revoke a token'); + } + + var urls = { + issuer: refreshTokenObject.issuer, + authorizeUrl: refreshTokenObject.authorizeUrl, + }; + + try { + 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, grant_type: 'refresh_token', - refresh_token: refreshTokenObject.refreshToken, scope: refreshTokenObject.scopes.join(' '), - }).map( function(name, value) { + refresh_token: refreshTokenObject.refreshToken, + }).map(function ([name, value]) { return name + "=" + encodeURIComponent(value); - }) - .join('&'); - console.log({url}); - // try { - // const result = await http.httpRequest(sdk, { - // url: refreshTokenObject.refreshUrl, - // method: 'GET', - // refreshToken: refreshTokenObject.refreshToken - // }); - // console.log('result', result); - // return result; - // } catch (err) { - // console.log({ err }); - // } + }).join('&'), + }); + return handleOAuthResponse(sdk, options, response, urls); + } catch (err) { + console.log({ err }); + } } -function renewTokens(sdk: OktaAuth, options: TokenParams): Promise { - console.log('renewing all tokens?'); +function renewTokens(sdk, options: TokenParams): Promise { + // If we have a refresh token, renew in a different way + var refreshTokenObject = sdk.authStateManager.getAuthState().refreshToken as RefreshToken; + + if (refreshTokenObject) { + return renewTokensWithRefresh(sdk, options, refreshTokenObject); + } + options = Object.assign({ scopes: sdk.options.scopes, authorizeUrl: sdk.options.authorizeUrl, @@ -784,11 +799,6 @@ function renewTokens(sdk: OktaAuth, options: TokenParams): Promise { options.responseType = ['token', 'id_token']; } - // XXX: Magical detection of refresh token. Passed? - // if( refreshToken ) { - // return renewWithRefreshToken(); - // } - return getWithoutPrompt(sdk, options) .then(res => res.tokens); } @@ -818,7 +828,7 @@ function removeSearch(sdk) { function _getOAuthParamsStrFromStorage() { let oauthParamsStr; if (browserStorage.browserHasSessionStorage()) { - oauthParamsStr = browserStorage.getSessionStorage().getItem(REDIRECT_OAUTH_PARAMS_NAME); + oauthParamsStr = browserStorage.getSessionStorage().getItem(REDIRECT_OAUTH_PARAMS_NAME); } if (!oauthParamsStr) { // fallback to cookies to support legacy browsers, e.g. IE/Edge @@ -828,8 +838,8 @@ function _getOAuthParamsStrFromStorage() { // clear storages if (browserStorage.browserHasSessionStorage()) { browserStorage.getSessionStorage().removeItem(REDIRECT_OAUTH_PARAMS_NAME); - } - cookies.delete(REDIRECT_OAUTH_PARAMS_NAME); + } + cookies.delete(REDIRECT_OAUTH_PARAMS_NAME); return oauthParamsStr; } @@ -859,7 +869,7 @@ function parseFromUrl(sdk, options: string | ParseFromUrlOptions): Promise { - // Only return the userinfo response if subjects match to mitigate token substitution attacks - if (userInfo.sub === idTokenObject.claims.sub) { - return userInfo; - } - return Promise.reject(new AuthSdkError('getUserInfo request was rejected due to token mismatch')); - }) - .catch(function(err) { - if (err.xhr && (err.xhr.status === 401 || err.xhr.status === 403)) { - var authenticateHeader; - if (err.xhr.headers && isFunction(err.xhr.headers.get) && err.xhr.headers.get('WWW-Authenticate')) { - authenticateHeader = err.xhr.headers.get('WWW-Authenticate'); - } else if (isFunction(err.xhr.getResponseHeader)) { - authenticateHeader = err.xhr.getResponseHeader('WWW-Authenticate'); + .then(userInfo => { + // Only return the userinfo response if subjects match to mitigate token substitution attacks + if (userInfo.sub === idTokenObject.claims.sub) { + return userInfo; } - if (authenticateHeader) { - var errorMatches = authenticateHeader.match(/error="(.*?)"/) || []; - var errorDescriptionMatches = authenticateHeader.match(/error_description="(.*?)"/) || []; - var error = errorMatches[1]; - var errorDescription = errorDescriptionMatches[1]; - if (error && errorDescription) { - err = new OAuthError(error, errorDescription); + return Promise.reject(new AuthSdkError('getUserInfo request was rejected due to token mismatch')); + }) + .catch(function (err) { + if (err.xhr && (err.xhr.status === 401 || err.xhr.status === 403)) { + var authenticateHeader; + if (err.xhr.headers && isFunction(err.xhr.headers.get) && err.xhr.headers.get('WWW-Authenticate')) { + authenticateHeader = err.xhr.headers.get('WWW-Authenticate'); + } else if (isFunction(err.xhr.getResponseHeader)) { + authenticateHeader = err.xhr.getResponseHeader('WWW-Authenticate'); + } + if (authenticateHeader) { + var errorMatches = authenticateHeader.match(/error="(.*?)"/) || []; + var errorDescriptionMatches = authenticateHeader.match(/error_description="(.*?)"/) || []; + var error = errorMatches[1]; + var errorDescription = errorDescriptionMatches[1]; + if (error && errorDescription) { + err = new OAuthError(error, errorDescription); + } } } - } - throw err; - }); + throw err; + }); } export { diff --git a/lib/types/Token.ts b/lib/types/Token.ts index ecaf693ba..df6162bc9 100644 --- a/lib/types/Token.ts +++ b/lib/types/Token.ts @@ -13,6 +13,7 @@ import { UserClaims } from './UserClaims'; export interface AbstractToken { + authorizeUrl: string; expiresAt: number; scopes: string[]; } @@ -20,13 +21,13 @@ export interface AbstractToken { export interface AccessToken extends AbstractToken { accessToken: string; tokenType: string; - authorizeUrl: string; userinfoUrl: string; } export interface RefreshToken extends AbstractToken { refreshToken: string; tokenUrl: string; + issuer: string; } // eslint-disable-next-line @typescript-eslint/interface-name-prefix @@ -35,7 +36,6 @@ export interface IDToken extends AbstractToken { claims: UserClaims; issuer: string; clientId: string; - authorizeUrl: string; } export type Token = AccessToken | IDToken | RefreshToken; diff --git a/samples/generated/static-spa/public/app.js b/samples/generated/static-spa/public/app.js index 091fef5ed..cb6bc447e 100644 --- a/samples/generated/static-spa/public/app.js +++ b/samples/generated/static-spa/public/app.js @@ -302,7 +302,6 @@ function createAuthClient() { redirectUri: config.redirectUri, scopes: config.scopes.split(/\s+/), tokenManager: { - expireEarlySeconds: 3600, // DEBUG storage: config.storage }, transformAuthState @@ -409,7 +408,7 @@ function loadConfig() { '&clientId=' + encodeURIComponent(clientId) + '&storage=' + encodeURIComponent(storage) + '&requireUserSession=' + encodeURIComponent(requireUserSession) + - '&flow=' + encodeURIComponent(flow), + '&flow=' + encodeURIComponent(flow) + '&scopes=' + encodeURIComponent(scopes); // Add all app options to the state, to preserve config across redirects diff --git a/samples/generated/static-spa/public/index.html b/samples/generated/static-spa/public/index.html index 9d4726728..b73536567 100644 --- a/samples/generated/static-spa/public/index.html +++ b/samples/generated/static-spa/public/index.html @@ -1,8 +1,8 @@ - - + + diff --git a/samples/generated/webpack-spa/src/index.js b/samples/generated/webpack-spa/src/index.js index 7639d52cc..f1947cbb9 100644 --- a/samples/generated/webpack-spa/src/index.js +++ b/samples/generated/webpack-spa/src/index.js @@ -301,7 +301,6 @@ function createAuthClient() { redirectUri: config.redirectUri, scopes: config.scopes.split(/\s+/), tokenManager: { - expireEarlySeconds: 3600, // DEBUG storage: config.storage }, transformAuthState @@ -408,7 +407,7 @@ function loadConfig() { '&clientId=' + encodeURIComponent(clientId) + '&storage=' + encodeURIComponent(storage) + '&requireUserSession=' + encodeURIComponent(requireUserSession) + - '&flow=' + encodeURIComponent(flow), + '&flow=' + encodeURIComponent(flow) + '&scopes=' + encodeURIComponent(scopes); // Add all app options to the state, to preserve config across redirects diff --git a/samples/templates/partials/spa/app.js b/samples/templates/partials/spa/app.js index df02eb54e..8b574286c 100644 --- a/samples/templates/partials/spa/app.js +++ b/samples/templates/partials/spa/app.js @@ -307,7 +307,6 @@ function createAuthClient() { redirectUri: config.redirectUri, scopes: config.scopes.split(/\s+/), tokenManager: { - expireEarlySeconds: 3600, // DEBUG storage: config.storage }, transformAuthState @@ -414,7 +413,7 @@ function loadConfig() { '&clientId=' + encodeURIComponent(clientId) + '&storage=' + encodeURIComponent(storage) + '&requireUserSession=' + encodeURIComponent(requireUserSession) + - '&flow=' + encodeURIComponent(flow), + '&flow=' + encodeURIComponent(flow) + '&scopes=' + encodeURIComponent(scopes); // Add all app options to the state, to preserve config across redirects From 88334ab412fb18c7edf094c367227b4be31ef42b Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Fri, 30 Oct 2020 13:34:40 -0700 Subject: [PATCH 04/17] Fixed renewal with refresh token --- lib/token.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/token.ts b/lib/token.ts index 3796491b6..ce98a359d 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -68,6 +68,7 @@ const cookies = browserStorage.storage; function revokeToken(sdk: OktaAuth, token: AccessToken): Promise { return Promise.resolve() .then(function () { + //if (!token || (!token.accessToken && !token.refreshToken)) { if (!token || !token.accessToken) { throw new AuthSdkError('A valid access token object is required'); } @@ -744,7 +745,7 @@ function renewToken(sdk: OktaAuth, token: Token): Promise { }); } -async function renewTokensWithRefresh(sdk: OktaAuth, options: TokenParams, refreshTokenObject: RefreshToken): Promise { +async function renewTokensWithRefresh(sdk: OktaAuth, options: TokenParams, refreshTokenObject: RefreshToken): Promise { var clientId = sdk.options.clientId; if (!clientId) { throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to revoke a token'); @@ -752,7 +753,8 @@ async function renewTokensWithRefresh(sdk: OktaAuth, options: TokenParams, refre var urls = { issuer: refreshTokenObject.issuer, - authorizeUrl: refreshTokenObject.authorizeUrl, + authorizeUrl: refreshTokenObject.authorizeUrl, + tokenUrl: refreshTokenObject.tokenUrl, }; try { @@ -772,7 +774,7 @@ async function renewTokensWithRefresh(sdk: OktaAuth, options: TokenParams, refre return name + "=" + encodeURIComponent(value); }).join('&'), }); - return handleOAuthResponse(sdk, options, response, urls); + return handleOAuthResponse(sdk, options, response, urls).then(res => res.tokens); } catch (err) { console.log({ err }); } From d22806fb0cd9fd7cfb1805e4a73e6bfdb0d119ba Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Mon, 2 Nov 2020 09:52:57 -0800 Subject: [PATCH 05/17] Adds base revoke for refresh tokens --- CHANGELOG.md | 13 ++++++++++ lib/browser/browser.ts | 35 ++++++++++++++++++++++----- lib/token.ts | 22 +++++++++++------ lib/types/Token.ts | 1 + lib/types/api.ts | 4 +-- samples/templates/partials/spa/app.js | 1 - 6 files changed, 59 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b00741c31..418189720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## PENDING + +### Features +- Adding the ability to use refresh tokens with single page applications (SPA) (Early Access feature - reach out to our support team) + - `scopes` configuration option now handles 'offline_access' as an option, which will use refresh tokens IF the app of the clientId being used is configured to do so in the Okta settings + - If you already have tokens (from a separate instance of auth-js or the okta-signin-widget) those tokens must already include a refresh token and have the 'offline_access' scope + - 'offline_access' is not requested by default. Anyone using the default `scopes` and wishing to add 'offline_access' should pass `scopes: ['openid', 'email', 'offline_access']` to their constructor + - `renewTokens()` will now use an XHR call to replace tokens if the app has a refresh token. This does not rely on "3rd party cookies" + - The `autoRenew` option (defaults to `true`) already calls `renewTokens()` shortly before tokens expire. The `autoRenew` feature will now automatically make use of the refresh token if present + - `signOut()` now revokes the refresh token (if present) by default, which in turn will revoke all tokens minted with that refresh token + - The revoke calls by `signOut()` follow the existing `revokeAccessToken` parameter - when `true` (the default) any refreshToken will be also be revoked, and when `false`, any tokens are not explicitly revoked. + ## 4.1.2 ### Bug Fixes @@ -12,6 +24,7 @@ - [#535](https://github.com/okta/okta-auth-js/pull/535) Respects `scopes` that are set in the constructor + ## 4.1.0 ### Features diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index 348a89633..e2ba3fa54 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -312,7 +312,21 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { return this.token.revoke(accessToken); } - // Revokes accessToken, clears all local tokens, then redirects to Okta to end the SSO session. + // Revokes the refresh token for the application session + async revokeRefreshToken(refreshToken?: RefreshToken) { + if (!refreshToken) { + refreshToken = (await this.tokenManager.getTokens()).refreshToken as RefreshToken; + const refreshTokenKey = this.tokenManager._getStorageKeyByType('refreshToken'); + this.tokenManager.remove(refreshTokenKey); + } + // Refresh token may have been removed. In this case, we will silently succeed. + if (!refreshToken) { + return Promise.resolve(); + } + return this.token.revoke(refreshToken); + } + + // Revokes refreshToken or accessToken, clears all local tokens, then redirects to Okta to end the SSO session. async signOut(options?) { options = Object.assign({}, options); @@ -324,7 +338,8 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { || defaultUri; var accessToken = options.accessToken; - var revokeAccessToken = options.revokeAccessToken !== false; + var refreshToken = options.refreshToken; + var revokeTokens = options.revokeTokens !== false; var idToken = options.idToken; var logoutUrl = getOAuthUrls(this).logoutUrl; @@ -333,17 +348,25 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { idToken = (await this.tokenManager.getTokens()).idToken as IDToken; } - if (revokeAccessToken && typeof accessToken === 'undefined') { + if (revokeTokens && typeof refreshToken === 'undefined') { + refreshToken = (await this.tokenManager.getTokens()).refreshToken as RefreshToken; + } + + if (revokeTokens && typeof accessToken === 'undefined') { accessToken = (await this.tokenManager.getTokens()).accessToken as AccessToken; } // Clear all local tokens this.tokenManager.clear(); - // TODO: handle refresh token - if (revokeAccessToken && accessToken) { + + if (revokeTokens && refreshToken) { await this.revokeAccessToken(accessToken); } - + + if (revokeTokens && accessToken) { + await this.revokeAccessToken(accessToken); + } + // No idToken? This can happen if the storage was cleared. // Fallback to XHR signOut, then simulate a redirect to the post logout uri if (!idToken) { diff --git a/lib/token.ts b/lib/token.ts index ce98a359d..bdaaebef9 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -43,6 +43,7 @@ import PKCE from './pkce'; import { OktaAuth, Token, + RevocableToken, isToken, isAccessToken, isIDToken, @@ -64,24 +65,29 @@ import { const cookies = browserStorage.storage; -// Only the access token can be revoked in SPA applications -function revokeToken(sdk: OktaAuth, token: AccessToken): Promise { +// refresh tokens have precedence to be revoked if no token is specified +function revokeToken(sdk: OktaAuth, token: RevocableToken): Promise { return Promise.resolve() .then(function () { - //if (!token || (!token.accessToken && !token.refreshToken)) { - if (!token || !token.accessToken) { - throw new AuthSdkError('A valid access token object is required'); + var accessToken: string; + var refreshToken: string; + if (token) { + accessToken = (token as AccessToken).accessToken; + refreshToken = (token as RefreshToken).refreshToken; + } + + if(!accessToken && !refreshToken) { + throw new AuthSdkError('A valid access or refresh token object is required'); } var clientId = sdk.options.clientId; if (!clientId) { throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to revoke a token'); } var revokeUrl = getOAuthUrls(sdk).revokeUrl; - var accessToken = token.accessToken; var args = toQueryString({ // eslint-disable-next-line camelcase - token_type_hint: 'access_token', - token: accessToken + token_type_hint: refreshToken ? 'refresh_token' : 'access_token', + token: refreshToken || accessToken, }).slice(1); var creds = btoa(clientId); return http.post(sdk, revokeUrl, args, { diff --git a/lib/types/Token.ts b/lib/types/Token.ts index df6162bc9..ef886e9a0 100644 --- a/lib/types/Token.ts +++ b/lib/types/Token.ts @@ -39,6 +39,7 @@ export interface IDToken extends AbstractToken { } export type Token = AccessToken | IDToken | RefreshToken; +export type RevocableToken = AccessToken | RefreshToken; export type TokenType = 'accessToken' | 'idToken' | 'refreshToken'; diff --git a/lib/types/api.ts b/lib/types/api.ts index cbc9dce55..26be2bf45 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -11,7 +11,7 @@ */ import { AuthTransaction } from '../tx/AuthTransaction'; -import { Token, AccessToken, IDToken, RefreshToken } from './Token'; +import { Token, RevocableToken, AccessToken, IDToken, RefreshToken } from './Token'; import { JWTObject } from './JWT'; import { UserClaims } from './UserClaims'; import { CustomUrls, OktaAuthOptions } from './OktaAuthOptions'; @@ -132,7 +132,7 @@ export interface TokenAPI { getWithoutPrompt(params?: TokenParams): Promise; getWithPopup(params?: TokenParams): Promise; decode(token: string): JWTObject; - revoke(token: AccessToken): Promise; + revoke(token: RevocableToken): Promise; renew(token: Token): Promise; renewTokens(): Promise; verify(token: IDToken, params?: object): Promise; diff --git a/samples/templates/partials/spa/app.js b/samples/templates/partials/spa/app.js index 8b574286c..8baff256d 100644 --- a/samples/templates/partials/spa/app.js +++ b/samples/templates/partials/spa/app.js @@ -288,7 +288,6 @@ function redirectToLogin(additionalParams) { // Redirect to Okta and show the signin widget if there is no active session authClient.token.getWithRedirect(Object.assign({ state: JSON.stringify(config.state), - // scopes: config.state.scopes.split(/\s+/) || config.scopes, // getWithRedirect doesn't obey scopes in constructor yet }, additionalParams)); } From 54b3ac982cc4a73ffcd955b26deb9d74b8985ae2 Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Mon, 2 Nov 2020 12:32:08 -0800 Subject: [PATCH 06/17] Updates existing tests to pass --- lib/browser/browser.ts | 4 ++-- test/spec/browser.js | 38 ++++++++++++++++++++++++++++---------- test/spec/token.ts | 4 ++-- test/spec/tokenManager.js | 4 ++-- test/support/tokens.js | 6 ------ 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index e2ba3fa54..be8b0f762 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -360,9 +360,9 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { this.tokenManager.clear(); if (revokeTokens && refreshToken) { - await this.revokeAccessToken(accessToken); + await this.revokeRefreshToken(refreshToken); } - + if (revokeTokens && accessToken) { await this.revokeAccessToken(accessToken); } diff --git a/test/spec/browser.js b/test/spec/browser.js index fc236d0cf..ef51512ec 100644 --- a/test/spec/browser.js +++ b/test/spec/browser.js @@ -344,6 +344,7 @@ describe('Browser', function() { auth.tokenManager.getTokens = jest.fn().mockResolvedValue({ accessToken, idToken }); spyOn(auth.tokenManager, 'clear'); spyOn(auth, 'revokeAccessToken').and.returnValue(Promise.resolve()); + spyOn(auth, 'revokeRefreshToken').and.returnValue(Promise.resolve()); spyOn(auth, 'closeSession').and.returnValue(Promise.resolve()); } @@ -353,11 +354,27 @@ describe('Browser', function() { initSpies(); }); - it('Default options: will revokeAccessToken and use window.location.origin for postLogoutRedirectUri', function() { + it('Default options when no refreshToken: will revokeAccessToken and use window.location.origin for postLogoutRedirectUri', function() { return auth.signOut() .then(function() { - expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(2); + expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(3); + expect(auth.revokeRefreshToken).not.toHaveBeenCalled(); + expect(auth.revokeAccessToken).toHaveBeenCalledWith(accessToken); + expect(auth.tokenManager.clear).toHaveBeenCalled(); + expect(auth.closeSession).not.toHaveBeenCalled(); + expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); + }); + }); + + it('Default options when refreshToken present: will revokeRefreshToken and use window.location.origin for postLogoutRedirectUri', function() { + const refreshToken = { refreshToken: 'fake'}; + auth.tokenManager.getTokens = jest.fn().mockResolvedValue({ accessToken, idToken, refreshToken }); + + return auth.signOut() + .then(function() { + expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(3); expect(auth.revokeAccessToken).toHaveBeenCalledWith(accessToken); + expect(auth.revokeRefreshToken).toHaveBeenCalledWith(refreshToken); expect(auth.tokenManager.clear).toHaveBeenCalled(); expect(auth.closeSession).not.toHaveBeenCalled(); expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); @@ -381,7 +398,7 @@ describe('Browser', function() { var customToken = { idToken: 'fake-custom' }; return auth.signOut({ idToken: customToken }) .then(function() { - expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(1); + expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(2); expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${customToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); }); }); @@ -389,7 +406,7 @@ describe('Browser', function() { it('if idToken=false will skip token manager read and call closeSession', function() { return auth.signOut({ idToken: false }) .then(function() { - expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(1); + expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(2); expect(auth.closeSession).toHaveBeenCalled(); expect(window.location.assign).toHaveBeenCalledWith(window.location.origin); }); @@ -399,7 +416,7 @@ describe('Browser', function() { global.window.location.href = origin; return auth.signOut({ idToken: false }) .then(function() { - expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(1); + expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(2); expect(auth.closeSession).toHaveBeenCalled(); expect(window.location.reload).toHaveBeenCalled(); }); @@ -439,11 +456,12 @@ describe('Browser', function() { }); }); - it('Can pass a "revokeAccessToken=false" to skip accessToken logic', function() { - return auth.signOut({ revokeAccessToken: false }) + it('Can pass a "revokeTokens=false" to skip revoke logic', function() { + return auth.signOut({ revokeTokens: false }) .then(function() { expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(1); expect(auth.revokeAccessToken).not.toHaveBeenCalled(); + expect(auth.revokeRefreshToken).not.toHaveBeenCalled(); expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); }); }); @@ -451,7 +469,7 @@ describe('Browser', function() { it('Can pass a "accessToken=false" to skip accessToken logic', function() { return auth.signOut({ accessToken: false }) .then(function() { - expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(1); + expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(2); expect(auth.revokeAccessToken).not.toHaveBeenCalled(); expect(window.location.assign).toHaveBeenCalledWith(`${issuer}/oauth2/v1/logout?id_token_hint=${idToken.idToken}&post_logout_redirect_uri=${encodedOrigin}`); }); @@ -472,7 +490,7 @@ describe('Browser', function() { spyOn(auth, 'closeSession').and.returnValue(Promise.resolve()); return auth.signOut() .then(function() { - expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(2); + expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(3); expect(auth.revokeAccessToken).toHaveBeenCalledWith(accessToken); expect(auth.tokenManager.clear).toHaveBeenCalled(); expect(auth.closeSession).toHaveBeenCalled(); @@ -485,7 +503,7 @@ describe('Browser', function() { global.window.location.href = origin; return auth.signOut() .then(function() { - expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(2); + expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(3); expect(auth.revokeAccessToken).toHaveBeenCalledWith(accessToken); expect(auth.tokenManager.clear).toHaveBeenCalled(); expect(auth.closeSession).toHaveBeenCalled(); diff --git a/test/spec/token.ts b/test/spec/token.ts index 88224a385..fd6199611 100644 --- a/test/spec/token.ts +++ b/test/spec/token.ts @@ -67,7 +67,7 @@ describe('token.revoke', function() { var oa = setupSync(); return oa.token.revoke(undefined as AccessToken) .catch(function(err) { - util.assertAuthSdkError(err, 'A valid access token object is required'); + util.assertAuthSdkError(err, 'A valid access or refresh token object is required'); }); }); it('throws if invalid token is passed', function() { @@ -75,7 +75,7 @@ describe('token.revoke', function() { var accessToken: unknown = { foo: 'bar' }; return oa.token.revoke(accessToken as AccessToken) .catch(function(err) { - util.assertAuthSdkError(err, 'A valid access token object is required'); + util.assertAuthSdkError(err, 'A valid access or refresh token object is required'); }); }); it('throws if clientId is not set', function() { diff --git a/test/spec/tokenManager.js b/test/spec/tokenManager.js index c6e6d0481..0571b16c0 100644 --- a/test/spec/tokenManager.js +++ b/test/spec/tokenManager.js @@ -272,9 +272,9 @@ describe('TokenManager', function() { } catch (e) { util.expectErrorToEqual(e, { name: 'AuthSdkError', - message: 'Token must be an Object with scopes, expiresAt, and an idToken or accessToken properties', + message: 'Token must be an Object with scopes, expiresAt, and one of: an idToken, accessToken, or refreshToken property', errorCode: 'INTERNAL', - errorSummary: 'Token must be an Object with scopes, expiresAt, and an idToken or accessToken properties', + errorSummary: 'Token must be an Object with scopes, expiresAt, and one of: an idToken, accessToken, or refreshToken property', errorLink: 'INTERNAL', errorId: 'INTERNAL', errorCauses: [] diff --git a/test/support/tokens.js b/test/support/tokens.js index 87f19bd44..36f3de260 100644 --- a/test/support/tokens.js +++ b/test/support/tokens.js @@ -60,7 +60,6 @@ tokens.standardIdTokenClaims = { }; tokens.standardIdTokenParsed = { - value: tokens.standardIdToken, idToken: tokens.standardIdToken, claims: tokens.standardIdTokenClaims, expiresAt: 1449699930, @@ -106,7 +105,6 @@ tokens.standardIdToken2Claims = { }; tokens.standardIdToken2Parsed = { - value: tokens.standardIdToken2, idToken: tokens.standardIdToken2, claims: tokens.standardIdToken2Claims, expiresAt: 1449699930, @@ -151,7 +149,6 @@ tokens.expiredBeforeIssuedIdTokenClaims = { }; tokens.expiredBeforeIssuedIdTokenParsed = { - value: tokens.expiredBeforeIssuedIdToken, idToken: tokens.expiredBeforeIssuedIdToken, claims: tokens.expiredBeforeIssuedIdTokenClaims, expiresAt: 1449690000, @@ -189,7 +186,6 @@ tokens.authServerIdTokenClaims = { }; tokens.authServerIdTokenParsed = { - value: tokens.authServerIdToken, idToken: tokens.authServerIdToken, claims: tokens.authServerIdTokenClaims, expiresAt: 1449699930, @@ -248,7 +244,6 @@ tokens.standardAccessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOj' + 'EQ9-Ua9rPOMaO0pFC6h2lfB_HfzGifXATKsN-wLdxk6cgA'; tokens.standardAccessTokenParsed = { - value: tokens.standardAccessToken, accessToken: tokens.standardAccessToken, expiresAt: 1449703529, // assuming time = 1449699929 scopes: ['openid', 'email'], @@ -271,7 +266,6 @@ tokens.authServerAccessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOjE 'h9gY9Z3xd92ac407ZIOHkabLvZ0-45ANM3Gm0LC0c'; tokens.authServerAccessTokenParsed = { - value: tokens.authServerAccessToken, accessToken: tokens.authServerAccessToken, expiresAt: 1449703529, // assuming time = 1449699929 scopes: ['openid', 'email'], From 47544ae958e9e13ae41390c6be16dfed4eeee200 Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Tue, 3 Nov 2020 09:03:08 -0800 Subject: [PATCH 07/17] updates docs a bit --- CHANGELOG.md | 3 +-- README.md | 14 +++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 418189720..fd6bcb82f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features - Adding the ability to use refresh tokens with single page applications (SPA) (Early Access feature - reach out to our support team) - - `scopes` configuration option now handles 'offline_access' as an option, which will use refresh tokens IF the app of the clientId being used is configured to do so in the Okta settings + - `scopes` configuration option now handles 'offline_access' as an option, which will use refresh tokens IF your client app is configured to do so in the Okta settings - If you already have tokens (from a separate instance of auth-js or the okta-signin-widget) those tokens must already include a refresh token and have the 'offline_access' scope - 'offline_access' is not requested by default. Anyone using the default `scopes` and wishing to add 'offline_access' should pass `scopes: ['openid', 'email', 'offline_access']` to their constructor - `renewTokens()` will now use an XHR call to replace tokens if the app has a refresh token. This does not rely on "3rd party cookies" @@ -24,7 +24,6 @@ - [#535](https://github.com/okta/okta-auth-js/pull/535) Respects `scopes` that are set in the constructor - ## 4.1.0 ### Features diff --git a/README.md b/README.md index 964cc2758..20e36f94d 100644 --- a/README.md +++ b/README.md @@ -626,6 +626,7 @@ var config = { * [signOut](#signout) * [closeSession](#closesession) * [revokeAccessToken](#revokeaccesstokenaccesstoken) +* [revokeRefreshToken](#revokerefreshtokenrefreshtoken) * [forgotPassword](#forgotpasswordoptions) * [unlockAccount](#unlockaccountoptions) * [verifyRecoveryToken](#verifyrecoverytokenoptions) @@ -739,7 +740,7 @@ if (authClient.isLoginRedirect()) { > :hourglass: async -Signs the user out of their current [Okta session](https://developer.okta.com/docs/api/resources/sessions) and clears all tokens stored locally in the `TokenManager`. By default, the access token is revoked so it can no longer be used. Some points to consider: +Signs the user out of their current [Okta session](https://developer.okta.com/docs/api/resources/sessions) and clears all tokens stored locally in the `TokenManager`. By default, the refresh token (if any) and access token are revoked so they can no longer be used. Some points to consider: * Will redirect to an Okta-hosted page before returning to your app. * If a `postLogoutRedirectUri` has not been specified or configured, `window.location.origin` will be used as the return URI. This URI must be listed in the Okta application's [Login redirect URIs](#login-redirect-uris). If the URI is unknown or invalid the redirect will end on a 400 error page from Okta. This error will be visible to the user and cannot be handled by the app. @@ -751,8 +752,8 @@ Signs the user out of their current [Okta session](https://developer.okta.com/do * `postLogoutRedirectUri` - Setting a value will override the `postLogoutRedirectUri` configured on the SDK. * `state` - An optional value, used along with `postLogoutRedirectUri`. If set, this value will be returned as a query parameter during the redirect to the `postLogoutRedirectUri` * `idToken` - Specifies the ID token object. By default, `signOut` will look for a token object named `idToken` within the `TokenManager`. If you have stored the id token object in a different location, you should retrieve it first and then pass it here. -* `revokeAccessToken` - If `false`, the access token will not be revoked. Use this option with care: not revoking the access token may pose a security risk if the token has been leaked outside the application. -* `accessToken` - Specifies the access token object. By default, `signOut` will look for a token object named `accessToken` within the `TokenManager`. If you have stored the access token object in a different location, you should retrieve it first and then pass it here. This options is ignored if the `revokeAccessToken` option is `false`. +* `revokeTokens` - If `false` (default: `true`), neither the access token nor the refresh token (if any) will be revoked. Use this option with care: not revoking tokens may pose a security risk if tokens have been leaked outside the application. +* `accessToken` - Specifies the access token object. By default, `signOut` will look for a token object named `accessToken` within the `TokenManager`. If you have stored the access token object in a different location, you should retrieve it first and then pass it here. This options is ignored if the `revokeTokens` option is `false`. ```javascript // Sign out using the default options @@ -814,6 +815,13 @@ authClient.closeSession() Revokes the access token for this application so it can no longer be used to authenticate API requests. The `accessToken` parameter is optional. By default, `revokeAccessToken` will look for a token object named `accessToken` within the `TokenManager`. If you have stored the access token object in a different location, you should retrieve it first and then pass it here. Returns a promise that resolves when the operation has completed. This method will succeed even if the access token has already been revoked or removed. +### `revokeRefreshToken(refreshToken)` + +> :hourglass: async + +Revokes the refresh token (if any) for this application so it can no longer be used to mint new tokens. The `refreshToken` parameter is optional. By default, `revokeRefreshToken` will look for a token object named `refreshToken` within the `TokenManager`. If you have stored the refresh token object in a different location, you should retrieve it first and then pass it here. Returns a promise that resolves when the operation has completed. This method will succeed even if the refresh token has already been revoked or removed. + + ### `forgotPassword(options)` > :hourglass: async From 00ea7c75d1a9cb712a557bf8eff539bb186dd919 Mon Sep 17 00:00:00 2001 From: Shuo Wu Date: Thu, 29 Oct 2020 17:36:18 -0400 Subject: [PATCH 08/17] feat: add method handleLoginRedirect (#528) --- test/spec/browser.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/spec/browser.js b/test/spec/browser.js index ef51512ec..02b48033a 100644 --- a/test/spec/browser.js +++ b/test/spec/browser.js @@ -897,4 +897,5 @@ describe('Browser', function() { expect(auth.isAuthorizationCodeFlow()).toBe(true); }); }); + }); From 94b9bf99ca96a92efe987ee5bcd2ee02a5690101 Mon Sep 17 00:00:00 2001 From: Aaron Granick Date: Fri, 30 Oct 2020 09:22:46 -0700 Subject: [PATCH 09/17] accept responseType as a constructor arg (#525) add methods: isPKCE, hasResponseType --- lib/token.ts | 10 +++++----- test/spec/browser.js | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/token.ts b/lib/token.ts index bdaaebef9..0c8e9f5a1 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -346,11 +346,11 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth function getDefaultTokenParams(sdk: OktaAuth): TokenParams { const { pkce, clientId, redirectUri, responseType, responseMode, scopes, ignoreSignature } = sdk.options; return { - pkce, - clientId, - redirectUri: redirectUri || window.location.href, - responseType: responseType || ['token', 'id_token'], - responseMode, + pkce: sdk.options.pkce, + clientId: sdk.options.clientId, + redirectUri: sdk.options.redirectUri || window.location.href, + responseType: sdk.options.responseType || ['token', 'id_token'], + responseMode: sdk.options.responseMode, state: generateState(), nonce: generateNonce(), scopes: scopes || ['openid', 'email'], diff --git a/test/spec/browser.js b/test/spec/browser.js index 02b48033a..ef51512ec 100644 --- a/test/spec/browser.js +++ b/test/spec/browser.js @@ -897,5 +897,4 @@ describe('Browser', function() { expect(auth.isAuthorizationCodeFlow()).toBe(true); }); }); - }); From e3cdb3aa14b65db75e33a76c12f9882c6f2f0a7a Mon Sep 17 00:00:00 2001 From: Aaron Granick Date: Sun, 1 Nov 2020 22:19:29 -0800 Subject: [PATCH 10/17] update samples to use authStateManager (#506) --- samples/generated/static-spa/public/app.js | 2 +- samples/generated/webpack-spa/src/index.js | 2 +- samples/templates/partials/spa/app.js | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/samples/generated/static-spa/public/app.js b/samples/generated/static-spa/public/app.js index cb6bc447e..c5dbde592 100644 --- a/samples/generated/static-spa/public/app.js +++ b/samples/generated/static-spa/public/app.js @@ -145,6 +145,7 @@ function handleLoginRedirect() { }); } +// called when the "get user info" link is clicked function getUserInfo() { return authClient.token.getUserInfo() .then(function(value) { @@ -283,7 +284,6 @@ function redirectToLogin(additionalParams) { // Redirect to Okta and show the signin widget if there is no active session authClient.token.getWithRedirect(Object.assign({ state: JSON.stringify(config.state), - // scopes: config.state.scopes.split(/\s+/) || config.scopes, // getWithRedirect doesn't obey scopes in constructor yet }, additionalParams)); } diff --git a/samples/generated/webpack-spa/src/index.js b/samples/generated/webpack-spa/src/index.js index f1947cbb9..aeb8c838c 100644 --- a/samples/generated/webpack-spa/src/index.js +++ b/samples/generated/webpack-spa/src/index.js @@ -144,6 +144,7 @@ function handleLoginRedirect() { }); } +// called when the "get user info" link is clicked function getUserInfo() { return authClient.token.getUserInfo() .then(function(value) { @@ -282,7 +283,6 @@ function redirectToLogin(additionalParams) { // Redirect to Okta and show the signin widget if there is no active session authClient.token.getWithRedirect(Object.assign({ state: JSON.stringify(config.state), - // scopes: config.state.scopes.split(/\s+/) || config.scopes, // getWithRedirect doesn't obey scopes in constructor yet }, additionalParams)); } diff --git a/samples/templates/partials/spa/app.js b/samples/templates/partials/spa/app.js index 8baff256d..2e29b3792 100644 --- a/samples/templates/partials/spa/app.js +++ b/samples/templates/partials/spa/app.js @@ -142,6 +142,7 @@ function handleLoginRedirect() { }); } +// called when the "get user info" link is clicked function getUserInfo() { return authClient.token.getUserInfo() .then(function(value) { From 8bcfdd5f167aec2257c4d07f424fed7538bb6cf8 Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Tue, 3 Nov 2020 15:07:41 -0800 Subject: [PATCH 11/17] revised to try for non-breaking --- README.md | 5 +++-- lib/TokenManager.ts | 7 +++---- lib/browser/browser.ts | 12 +++++++----- lib/token.ts | 3 +++ lib/types/Token.ts | 3 ++- test/support/tokens.js | 6 ++++++ 6 files changed, 24 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 20e36f94d..e62de4a97 100644 --- a/README.md +++ b/README.md @@ -752,8 +752,9 @@ Signs the user out of their current [Okta session](https://developer.okta.com/do * `postLogoutRedirectUri` - Setting a value will override the `postLogoutRedirectUri` configured on the SDK. * `state` - An optional value, used along with `postLogoutRedirectUri`. If set, this value will be returned as a query parameter during the redirect to the `postLogoutRedirectUri` * `idToken` - Specifies the ID token object. By default, `signOut` will look for a token object named `idToken` within the `TokenManager`. If you have stored the id token object in a different location, you should retrieve it first and then pass it here. -* `revokeTokens` - If `false` (default: `true`), neither the access token nor the refresh token (if any) will be revoked. Use this option with care: not revoking tokens may pose a security risk if tokens have been leaked outside the application. -* `accessToken` - Specifies the access token object. By default, `signOut` will look for a token object named `accessToken` within the `TokenManager`. If you have stored the access token object in a different location, you should retrieve it first and then pass it here. This options is ignored if the `revokeTokens` option is `false`. +* `revokeAccessToken` - If `false` (default: `true`) the access token will not be revoked. Use this option with care: not revoking tokens may pose a security risk if tokens have been leaked outside the application. +* `revokeRefreshToken` - If `false` (default: `true`) the refresh token will not be revoked. Use this option with care: not revoking tokens may pose a security risk if tokens have been leaked outside the application. Revoking a refersh token will revoke any access tokens minted by it, even if `revokeAccessToken` is `false`. +* `accessToken` - Specifies the access token object. By default, `signOut` will look for a token object named `accessToken` within the `TokenManager`. If you have stored the access token object in a different location, you should retrieve it first and then pass it here. This options is ignored if the `revokeAccessToken` option is `false`. ```javascript // Sign out using the default options diff --git a/lib/TokenManager.ts b/lib/TokenManager.ts index f06899c89..9a9ef91f3 100644 --- a/lib/TokenManager.ts +++ b/lib/TokenManager.ts @@ -316,14 +316,12 @@ function renew(sdk, tokenMgmtRef, storage, key) { // Remove existing autoRenew timeouts clearExpireEventTimeoutAll(tokenMgmtRef); - // A refresh token means a refresh instead of renewal - // TODO: XXX + // A refresh token means a replace instead of renewal // Store the renew promise state, to avoid renewing again // Renew/refresh all tokens in one process tokenMgmtRef.renewPromise[key] = sdk.token.renewTokens({ scopes: token.scopes, - refreshToken: storage.getStorage() }) .then(function(freshTokens) { // store and emit events for freshTokens @@ -336,7 +334,8 @@ function renew(sdk, tokenMgmtRef, storage, key) { (accessTokenKey, accessToken) => emitRenewed(tokenMgmtRef, accessTokenKey, accessToken, oldTokenStorage[accessTokenKey]), (idTokenKey, idToken) => - emitRenewed(tokenMgmtRef, idTokenKey, idToken, oldTokenStorage[idTokenKey]) + emitRenewed(tokenMgmtRef, idTokenKey, idToken, oldTokenStorage[idTokenKey]), + // not emitting refresh token as an internal detail, not a usable token ); // return freshToken by key diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index be8b0f762..41c8c2961 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -339,7 +339,8 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { var accessToken = options.accessToken; var refreshToken = options.refreshToken; - var revokeTokens = options.revokeTokens !== false; + var revokeAccessToken = options.revokeAccessToken !== false; + var revokeRefreshToken = options.revokeRefreshToken !== false; var idToken = options.idToken; var logoutUrl = getOAuthUrls(this).logoutUrl; @@ -348,22 +349,23 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { idToken = (await this.tokenManager.getTokens()).idToken as IDToken; } - if (revokeTokens && typeof refreshToken === 'undefined') { + + if (revokeRefreshToken && typeof refreshToken === 'undefined') { refreshToken = (await this.tokenManager.getTokens()).refreshToken as RefreshToken; } - if (revokeTokens && typeof accessToken === 'undefined') { + if (revokeAccessToken && typeof accessToken === 'undefined') { accessToken = (await this.tokenManager.getTokens()).accessToken as AccessToken; } // Clear all local tokens this.tokenManager.clear(); - if (revokeTokens && refreshToken) { + if (revokeRefreshToken && refreshToken) { await this.revokeRefreshToken(refreshToken); } - if (revokeTokens && accessToken) { + if (revokeAccessToken && accessToken) { await this.revokeAccessToken(accessToken); } diff --git a/lib/token.ts b/lib/token.ts index 0c8e9f5a1..e656891e3 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -271,6 +271,7 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth if (accessToken) { tokenDict.accessToken = { + value: accessToken, accessToken: accessToken, expiresAt: Number(expiresIn) + Math.floor(Date.now() / 1000), tokenType: tokenType, @@ -282,6 +283,7 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth if (refreshToken) { tokenDict.refreshToken = { + value: refreshToken, refreshToken: refreshToken, expiresAt: Number(expiresIn) + Math.floor(Date.now() / 1000), scopes: scopes, @@ -295,6 +297,7 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth var jwt = sdk.token.decode(idToken); var idTokenObj: IDToken = { + value: idToken, idToken: idToken, claims: jwt.payload, expiresAt: jwt.payload.exp, diff --git a/lib/types/Token.ts b/lib/types/Token.ts index ef886e9a0..0f283a5c7 100644 --- a/lib/types/Token.ts +++ b/lib/types/Token.ts @@ -13,8 +13,9 @@ import { UserClaims } from './UserClaims'; export interface AbstractToken { - authorizeUrl: string; expiresAt: number; + value: string; + authorizeUrl: string; scopes: string[]; } diff --git a/test/support/tokens.js b/test/support/tokens.js index 36f3de260..87f19bd44 100644 --- a/test/support/tokens.js +++ b/test/support/tokens.js @@ -60,6 +60,7 @@ tokens.standardIdTokenClaims = { }; tokens.standardIdTokenParsed = { + value: tokens.standardIdToken, idToken: tokens.standardIdToken, claims: tokens.standardIdTokenClaims, expiresAt: 1449699930, @@ -105,6 +106,7 @@ tokens.standardIdToken2Claims = { }; tokens.standardIdToken2Parsed = { + value: tokens.standardIdToken2, idToken: tokens.standardIdToken2, claims: tokens.standardIdToken2Claims, expiresAt: 1449699930, @@ -149,6 +151,7 @@ tokens.expiredBeforeIssuedIdTokenClaims = { }; tokens.expiredBeforeIssuedIdTokenParsed = { + value: tokens.expiredBeforeIssuedIdToken, idToken: tokens.expiredBeforeIssuedIdToken, claims: tokens.expiredBeforeIssuedIdTokenClaims, expiresAt: 1449690000, @@ -186,6 +189,7 @@ tokens.authServerIdTokenClaims = { }; tokens.authServerIdTokenParsed = { + value: tokens.authServerIdToken, idToken: tokens.authServerIdToken, claims: tokens.authServerIdTokenClaims, expiresAt: 1449699930, @@ -244,6 +248,7 @@ tokens.standardAccessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOj' + 'EQ9-Ua9rPOMaO0pFC6h2lfB_HfzGifXATKsN-wLdxk6cgA'; tokens.standardAccessTokenParsed = { + value: tokens.standardAccessToken, accessToken: tokens.standardAccessToken, expiresAt: 1449703529, // assuming time = 1449699929 scopes: ['openid', 'email'], @@ -266,6 +271,7 @@ tokens.authServerAccessToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOjE 'h9gY9Z3xd92ac407ZIOHkabLvZ0-45ANM3Gm0LC0c'; tokens.authServerAccessTokenParsed = { + value: tokens.authServerAccessToken, accessToken: tokens.authServerAccessToken, expiresAt: 1449703529, // assuming time = 1449699929 scopes: ['openid', 'email'], From 449787468769dbb8c1da2368c6b9cf4aaa62849a Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Thu, 5 Nov 2020 15:17:04 -0800 Subject: [PATCH 12/17] Fix for lack of AuthState --- lib/browser/browser.ts | 4 ++-- lib/token.ts | 44 +++++++++++++++++++++++------------------ test/app/src/config.ts | 4 ++-- test/app/src/testApp.ts | 23 +++++++++++++++++---- test/app/src/tokens.ts | 6 +++++- test/spec/browser.js | 4 ++-- 6 files changed, 55 insertions(+), 30 deletions(-) diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index 41c8c2961..2d55eabda 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -350,7 +350,7 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { } - if (revokeRefreshToken && typeof refreshToken === 'undefined') { + if (revokeAccessToken && typeof refreshToken === 'undefined') { refreshToken = (await this.tokenManager.getTokens()).refreshToken as RefreshToken; } @@ -361,7 +361,7 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { // Clear all local tokens this.tokenManager.clear(); - if (revokeRefreshToken && refreshToken) { + if (revokeAccessToken && refreshToken) { await this.revokeRefreshToken(refreshToken); } diff --git a/lib/token.ts b/lib/token.ts index e656891e3..51c9f8f9e 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -790,28 +790,34 @@ async function renewTokensWithRefresh(sdk: OktaAuth, options: TokenParams, refre } function renewTokens(sdk, options: TokenParams): Promise { - // If we have a refresh token, renew in a different way - var refreshTokenObject = sdk.authStateManager.getAuthState().refreshToken as RefreshToken; + + // If we have a refresh token, renew using that, otherwise getWithoutPrompt - if (refreshTokenObject) { - return renewTokensWithRefresh(sdk, options, refreshTokenObject); - } + // Calling via async as auth-js doesn't yet (as of 4.2) ensure that updateAuthState() was ever called + this.tokenManager.getTokens() + .then(tokens => tokens.refreshToken as RefreshToken) + .then(refreshTokenObject => { - options = Object.assign({ - scopes: sdk.options.scopes, - authorizeUrl: sdk.options.authorizeUrl, - userinfoUrl: sdk.options.userinfoUrl, - issuer: sdk.options.issuer - }, options); + if (refreshTokenObject) { + return renewTokensWithRefresh(sdk, options, refreshTokenObject); + } - if (sdk.options.pkce) { - options.responseType = 'code'; - } else { - options.responseType = ['token', 'id_token']; - } + options = Object.assign({ + scopes: sdk.options.scopes, + authorizeUrl: sdk.options.authorizeUrl, + userinfoUrl: sdk.options.userinfoUrl, + issuer: sdk.options.issuer + }, options); - return getWithoutPrompt(sdk, options) - .then(res => res.tokens); + if (sdk.options.pkce) { + options.responseType = 'code'; + } else { + options.responseType = ['token', 'id_token']; + } + + return getWithoutPrompt(sdk, options) + .then(res => res.tokens); + }); } function removeHash(sdk) { @@ -970,5 +976,5 @@ export { handleOAuthResponse, prepareTokenParams, _addOAuthParamsToStorage, // export for testing purpose - _getOAuthParamsStrFromStorage // export for testing purpose + _getOAuthParamsStrFromStorage, // export for testing purpose }; diff --git a/test/app/src/config.ts b/test/app/src/config.ts index 70a491340..258f8f7b3 100644 --- a/test/app/src/config.ts +++ b/test/app/src/config.ts @@ -34,7 +34,7 @@ function getDefaultConfig(): Config { issuer: ISSUER, clientId: CLIENT_ID, responseType: ['token', 'id_token'], - scopes: ['openid', 'email'], + scopes: ['openid', 'email', 'offline_access'], _defaultScopes: false, pkce: true, cookies: { @@ -52,7 +52,7 @@ function getConfigFromUrl(): Config { const clientId = url.searchParams.get('clientId'); const pkce = url.searchParams.get('pkce') !== 'false'; // On by default const _defaultScopes = url.searchParams.get('_defaultScopes') === 'true'; - const scopes = (url.searchParams.get('scopes') || 'openid,email').split(','); + const scopes = (url.searchParams.get('scopes') || 'openid,email,offline_access').split(','); const responseType = (url.searchParams.get('responseType') || 'id_token,token').split(','); const responseMode = url.searchParams.get('responseMode') || undefined; const storage = url.searchParams.get('storage') || undefined; diff --git a/test/app/src/testApp.ts b/test/app/src/testApp.ts index 97e93bde0..f03f4792c 100644 --- a/test/app/src/testApp.ts +++ b/test/app/src/testApp.ts @@ -96,6 +96,7 @@ function bindFunctions(testApp: TestApp, window: Window): void { logoutApp: testApp.logoutApp.bind(testApp), refreshSession: testApp.refreshSession.bind(testApp), renewToken: testApp.renewToken.bind(testApp), + renewTokens: testApp.renewTokens.bind(testApp), revokeToken: testApp.revokeToken.bind(testApp), handleCallback: testApp.handleCallback.bind(testApp), getUserInfo: testApp.getUserInfo.bind(testApp), @@ -218,13 +219,17 @@ class TestApp { signIn.showSignInToGetTokens({ clientId: config.clientId, - redirectUri: config.redirectUri, + redirectUri: config.redirectUri, + scope: ['openid', 'email', 'offline_access'], // Return an access token from the authorization server getAccessToken: true, // Return an ID token from the authorization server getIdToken: true, + + // Return a Refresh token from the authorization server + getRefreshToken: true }); } @@ -302,6 +307,13 @@ class TestApp { }); } + async renewTokens(): Promise { + return this.oktaAuth.token.renewTokens() + .then(() => { + this.render(); + }); + } + logoutRedirect(): void { this.oktaAuth.signOut() .catch(e => { @@ -351,7 +363,7 @@ class TestApp { } async getUserInfo(): Promise { - const { accessToken, idToken } = await this.oktaAuth.tokenManager.getTokens(); + const { accessToken, idToken, refreshToken } = await this.oktaAuth.tokenManager.getTokens(); if (accessToken && idToken) { return this.oktaAuth.token.getUserInfo(accessToken as AccessToken) .catch(error => { @@ -435,7 +447,7 @@ class TestApp { } appHTML(props: Tokens): string { - const { idToken, accessToken } = props || {}; + const { idToken, accessToken, refreshToken } = props || {}; if (idToken || accessToken) { // Authenticated user home page return ` @@ -450,6 +462,9 @@ class TestApp {
  • Renew Token
  • +
  • + Renew Tokens +
  • Get Token (without prompt)
  • @@ -471,7 +486,7 @@ class TestApp {

    - ${ tokensHTML({idToken, accessToken})} + ${ tokensHTML({idToken, accessToken, refreshToken})} `; } diff --git a/test/app/src/tokens.ts b/test/app/src/tokens.ts index 1c42a5d98..f3f384b0e 100644 --- a/test/app/src/tokens.ts +++ b/test/app/src/tokens.ts @@ -2,7 +2,7 @@ import { htmlString } from './util'; import { Tokens, IDToken, UserClaims } from '@okta/okta-auth-js'; function tokensHTML(tokens: Tokens): string { - const { idToken, accessToken } = tokens; + const { idToken, accessToken, refreshToken } = tokens; const claims: UserClaims = idToken ? (idToken as IDToken).claims : {} as UserClaims; const html = ` @@ -29,6 +29,10 @@ function tokensHTML(tokens: Tokens): string { ID Token
    ${ idToken ? htmlString(idToken) : 'N/A' }
    +
    + Refresh Token
    +
    ${ refreshToken ? htmlString(refreshToken) : 'N/A' }
    +
    `; return html; diff --git a/test/spec/browser.js b/test/spec/browser.js index ef51512ec..5c2f05d4c 100644 --- a/test/spec/browser.js +++ b/test/spec/browser.js @@ -456,8 +456,8 @@ describe('Browser', function() { }); }); - it('Can pass a "revokeTokens=false" to skip revoke logic', function() { - return auth.signOut({ revokeTokens: false }) + it('Can pass a "revokeAccessToken=false" to skip revoke logic', function() { + return auth.signOut({ revokeAccessToken: false }) .then(function() { expect(auth.tokenManager.getTokens).toHaveBeenCalledTimes(1); expect(auth.revokeAccessToken).not.toHaveBeenCalled(); From 1a7b909fea9b78e3f40393c27e7f6337f6f823fd Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Thu, 5 Nov 2020 16:23:07 -0800 Subject: [PATCH 13/17] Fix return val for renewTokens --- lib/token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/token.ts b/lib/token.ts index 51c9f8f9e..93497a7fb 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -794,7 +794,7 @@ function renewTokens(sdk, options: TokenParams): Promise { // If we have a refresh token, renew using that, otherwise getWithoutPrompt // Calling via async as auth-js doesn't yet (as of 4.2) ensure that updateAuthState() was ever called - this.tokenManager.getTokens() + return this.tokenManager.getTokens() .then(tokens => tokens.refreshToken as RefreshToken) .then(refreshTokenObject => { From b135f3b3d979b8615c5db2f355e5389aba835702 Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Fri, 6 Nov 2020 12:06:26 -0800 Subject: [PATCH 14/17] Fix responseType on renew error --- lib/token.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/token.ts b/lib/token.ts index 93497a7fb..4a4527236 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -234,6 +234,7 @@ function validateResponse(res: OAuthResponse, oauthParams: TokenParams) { // eslint-disable-next-line max-len function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuthResponse, urls: CustomUrls): Promise { urls = urls || {}; + tokenParams = tokenParams || {}; var responseType = tokenParams.responseType; if (!Array.isArray(responseType)) { @@ -794,7 +795,7 @@ function renewTokens(sdk, options: TokenParams): Promise { // If we have a refresh token, renew using that, otherwise getWithoutPrompt // Calling via async as auth-js doesn't yet (as of 4.2) ensure that updateAuthState() was ever called - return this.tokenManager.getTokens() + return sdk.tokenManager.getTokens() .then(tokens => tokens.refreshToken as RefreshToken) .then(refreshTokenObject => { From 168e41dbf6f06441b73f2617c07b214f85d66a42 Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Wed, 11 Nov 2020 14:33:00 -0800 Subject: [PATCH 15/17] Fix urls passed for renew --- lib/TokenManager.ts | 4 ++- lib/browser/browser.ts | 1 - lib/token.ts | 33 +++++++++++----------- samples/generated/static-spa/README.md | 1 + samples/generated/static-spa/public/app.js | 4 +-- samples/generated/webpack-spa/src/index.js | 4 +-- samples/templates/partials/spa/app.js | 4 +-- test/app/src/testApp.ts | 2 +- 8 files changed, 28 insertions(+), 25 deletions(-) diff --git a/lib/TokenManager.ts b/lib/TokenManager.ts index 9a9ef91f3..5948fa55a 100644 --- a/lib/TokenManager.ts +++ b/lib/TokenManager.ts @@ -155,7 +155,9 @@ function validateToken(token: Token) { !token.scopes || (!token.expiresAt && token.expiresAt !== 0) || (!isIDToken(token) && !isAccessToken(token) && !isRefreshToken(token))) { - throw new AuthSdkError('Token must be an Object with scopes, expiresAt, and one of: an idToken, accessToken, or refreshToken property'); + throw new AuthSdkError( + 'Token must be an Object with scopes, expiresAt, and one of: an idToken, accessToken, or refreshToken property' + ); } } diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index 2d55eabda..bb7ba7ead 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -340,7 +340,6 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI { var accessToken = options.accessToken; var refreshToken = options.refreshToken; var revokeAccessToken = options.revokeAccessToken !== false; - var revokeRefreshToken = options.revokeRefreshToken !== false; var idToken = options.idToken; var logoutUrl = getOAuthUrls(this).logoutUrl; diff --git a/lib/token.ts b/lib/token.ts index 4a4527236..4ece25930 100644 --- a/lib/token.ts +++ b/lib/token.ts @@ -350,11 +350,11 @@ function handleOAuthResponse(sdk: OktaAuth, tokenParams: TokenParams, res: OAuth function getDefaultTokenParams(sdk: OktaAuth): TokenParams { const { pkce, clientId, redirectUri, responseType, responseMode, scopes, ignoreSignature } = sdk.options; return { - pkce: sdk.options.pkce, - clientId: sdk.options.clientId, - redirectUri: sdk.options.redirectUri || window.location.href, - responseType: sdk.options.responseType || ['token', 'id_token'], - responseMode: sdk.options.responseMode, + pkce, + clientId, + redirectUri: redirectUri || window.location.href, + responseType: responseType || ['token', 'id_token'], + responseMode, state: generateState(), nonce: generateNonce(), scopes: scopes || ['openid', 'email'], @@ -755,17 +755,17 @@ function renewToken(sdk: OktaAuth, token: Token): Promise { }); } -async function renewTokensWithRefresh(sdk: OktaAuth, options: TokenParams, refreshTokenObject: RefreshToken): Promise { +async function renewTokensWithRefresh( + sdk: OktaAuth, + tokenParams: TokenParams, + refreshTokenObject: RefreshToken +): Promise { var clientId = sdk.options.clientId; if (!clientId) { throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to revoke a token'); } - var urls = { - issuer: refreshTokenObject.issuer, - authorizeUrl: refreshTokenObject.authorizeUrl, - tokenUrl: refreshTokenObject.tokenUrl, - }; + var urls = getOAuthUrls(sdk, tokenParams); try { const response = await http.httpRequest(sdk, { @@ -775,16 +775,17 @@ async function renewTokensWithRefresh(sdk: OktaAuth, options: TokenParams, refre headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, + args: Object.entries({ - client_id: clientId, - grant_type: 'refresh_token', + client_id: clientId, // eslint-disable-line camelcase + grant_type: 'refresh_token', // eslint-disable-line camelcase scope: refreshTokenObject.scopes.join(' '), - refresh_token: refreshTokenObject.refreshToken, + refresh_token: refreshTokenObject.refreshToken, // eslint-disable-line camelcase }).map(function ([name, value]) { - return name + "=" + encodeURIComponent(value); + return name + '=' + encodeURIComponent(value); }).join('&'), }); - return handleOAuthResponse(sdk, options, response, urls).then(res => res.tokens); + return handleOAuthResponse(sdk, tokenParams, response, urls).then(res => res.tokens); } catch (err) { console.log({ err }); } diff --git a/samples/generated/static-spa/README.md b/samples/generated/static-spa/README.md index f583fd490..907c223dd 100644 --- a/samples/generated/static-spa/README.md +++ b/samples/generated/static-spa/README.md @@ -29,6 +29,7 @@ The following parameters are accepted by this app: * `storage` - ("memory"|"sessionStorage"|"localStorage"|"cookie") - set the `storage` option for the `TokenManager` token storage * `requireUserSession` - (true|false) - by default, a user will be considered authenticated if there are tokens in storage. This check does not require a network request. If the `requireUserSession` option is set to `true`, an additional check will be done to verify that the user has a valid Okta SSO * `flow` - ("redirect"|"form"|"widget") - set the authorization flow +* `scopes` - (space-separated string) - defaults to "openid profile". Sets the scopes that will be used to request tokens ## Authorization flows diff --git a/samples/generated/static-spa/public/app.js b/samples/generated/static-spa/public/app.js index c5dbde592..17f6e57e8 100644 --- a/samples/generated/static-spa/public/app.js +++ b/samples/generated/static-spa/public/app.js @@ -11,7 +11,7 @@ var config = { clientId: '', scopes: 'openid email', storage: 'sessionStorage', - requireUserSession: true, + requireUserSession: 'true', flow: 'redirect' }; @@ -361,7 +361,7 @@ function showError(error) { document.getElementById('error').appendChild(node); } -/* eslint-disable max-statements */ +/* eslint-disable max-statements,complexity */ function loadConfig() { // Read all config from the URL var url = new URL(window.location.href); diff --git a/samples/generated/webpack-spa/src/index.js b/samples/generated/webpack-spa/src/index.js index aeb8c838c..0daa08b7c 100644 --- a/samples/generated/webpack-spa/src/index.js +++ b/samples/generated/webpack-spa/src/index.js @@ -10,7 +10,7 @@ var config = { clientId: '', scopes: 'openid email', storage: 'sessionStorage', - requireUserSession: true, + requireUserSession: 'true', flow: 'redirect' }; @@ -360,7 +360,7 @@ function showError(error) { document.getElementById('error').appendChild(node); } -/* eslint-disable max-statements */ +/* eslint-disable max-statements,complexity */ function loadConfig() { // Read all config from the URL var url = new URL(window.location.href); diff --git a/samples/templates/partials/spa/app.js b/samples/templates/partials/spa/app.js index 2e29b3792..1bdfcbd75 100644 --- a/samples/templates/partials/spa/app.js +++ b/samples/templates/partials/spa/app.js @@ -6,7 +6,7 @@ var config = { clientId: '', scopes: '{{ scopes }}', storage: '{{ storage }}', - requireUserSession: {{ requireUserSession }}, + requireUserSession: '{{ requireUserSession }}', flow: '{{ flow }}' }; @@ -366,7 +366,7 @@ function showError(error) { document.getElementById('error').appendChild(node); } -/* eslint-disable max-statements */ +/* eslint-disable max-statements,complexity */ function loadConfig() { // Read all config from the URL var url = new URL(window.location.href); diff --git a/test/app/src/testApp.ts b/test/app/src/testApp.ts index f03f4792c..a83c7f4a9 100644 --- a/test/app/src/testApp.ts +++ b/test/app/src/testApp.ts @@ -363,7 +363,7 @@ class TestApp { } async getUserInfo(): Promise { - const { accessToken, idToken, refreshToken } = await this.oktaAuth.tokenManager.getTokens(); + const { accessToken, idToken } = await this.oktaAuth.tokenManager.getTokens(); if (accessToken && idToken) { return this.oktaAuth.token.getUserInfo(accessToken as AccessToken) .catch(error => { From f74cb6ff55246c606bb28ee3a15c65ae7580de38 Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Fri, 13 Nov 2020 14:53:10 -0800 Subject: [PATCH 16/17] Run e2e tests w/o refresh token for now --- samples/generated/static-spa/README.md | 1 - test/app/src/config.ts | 4 ++-- test/app/src/testApp.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/samples/generated/static-spa/README.md b/samples/generated/static-spa/README.md index 907c223dd..f583fd490 100644 --- a/samples/generated/static-spa/README.md +++ b/samples/generated/static-spa/README.md @@ -29,7 +29,6 @@ The following parameters are accepted by this app: * `storage` - ("memory"|"sessionStorage"|"localStorage"|"cookie") - set the `storage` option for the `TokenManager` token storage * `requireUserSession` - (true|false) - by default, a user will be considered authenticated if there are tokens in storage. This check does not require a network request. If the `requireUserSession` option is set to `true`, an additional check will be done to verify that the user has a valid Okta SSO * `flow` - ("redirect"|"form"|"widget") - set the authorization flow -* `scopes` - (space-separated string) - defaults to "openid profile". Sets the scopes that will be used to request tokens ## Authorization flows diff --git a/test/app/src/config.ts b/test/app/src/config.ts index 258f8f7b3..70a491340 100644 --- a/test/app/src/config.ts +++ b/test/app/src/config.ts @@ -34,7 +34,7 @@ function getDefaultConfig(): Config { issuer: ISSUER, clientId: CLIENT_ID, responseType: ['token', 'id_token'], - scopes: ['openid', 'email', 'offline_access'], + scopes: ['openid', 'email'], _defaultScopes: false, pkce: true, cookies: { @@ -52,7 +52,7 @@ function getConfigFromUrl(): Config { const clientId = url.searchParams.get('clientId'); const pkce = url.searchParams.get('pkce') !== 'false'; // On by default const _defaultScopes = url.searchParams.get('_defaultScopes') === 'true'; - const scopes = (url.searchParams.get('scopes') || 'openid,email,offline_access').split(','); + const scopes = (url.searchParams.get('scopes') || 'openid,email').split(','); const responseType = (url.searchParams.get('responseType') || 'id_token,token').split(','); const responseMode = url.searchParams.get('responseMode') || undefined; const storage = url.searchParams.get('storage') || undefined; diff --git a/test/app/src/testApp.ts b/test/app/src/testApp.ts index a83c7f4a9..308ab0e8f 100644 --- a/test/app/src/testApp.ts +++ b/test/app/src/testApp.ts @@ -220,7 +220,7 @@ class TestApp { signIn.showSignInToGetTokens({ clientId: config.clientId, redirectUri: config.redirectUri, - scope: ['openid', 'email', 'offline_access'], + scope: ['openid', 'email'], // Return an access token from the authorization server getAccessToken: true, From 8f7dfb671bdac1b870d7cff5d8b580285ced59f4 Mon Sep 17 00:00:00 2001 From: Brett Ritter Date: Mon, 16 Nov 2020 13:46:41 -0800 Subject: [PATCH 17/17] prep for 4.2.0 release --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd6bcb82f..1e11de927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## PENDING +## 4.2.0 ### Features - Adding the ability to use refresh tokens with single page applications (SPA) (Early Access feature - reach out to our support team) @@ -10,7 +10,7 @@ - `renewTokens()` will now use an XHR call to replace tokens if the app has a refresh token. This does not rely on "3rd party cookies" - The `autoRenew` option (defaults to `true`) already calls `renewTokens()` shortly before tokens expire. The `autoRenew` feature will now automatically make use of the refresh token if present - `signOut()` now revokes the refresh token (if present) by default, which in turn will revoke all tokens minted with that refresh token - - The revoke calls by `signOut()` follow the existing `revokeAccessToken` parameter - when `true` (the default) any refreshToken will be also be revoked, and when `false`, any tokens are not explicitly revoked. + - The revoke calls by `signOut()` follow the existing `revokeAccessToken` parameter - when `true` (the default) any refreshToken will be also be revoked, and when `false`, any tokens are not explicitly revoked. This parameter name becomes slightly misleading (as it controls both access AND refresh token revocation) and will change in a future version. ## 4.1.2