From 5e35346bfa477b560f5bde1afb2125a2c5f6e9c4 Mon Sep 17 00:00:00 2001 From: Guillaume Chervet Date: Tue, 13 Feb 2024 21:39:07 +0100 Subject: [PATCH] feat(oidc): add extras to revoke access_token and refresh_tokens (#1295) (release) --- packages/oidc-client/README.md | 5 +- packages/oidc-client/src/logout.spec.ts | 4 +- packages/oidc-client/src/logout.ts | 78 +++++++++++++++++-------- packages/oidc-client/src/oidc.ts | 2 +- packages/oidc-client/src/requests.ts | 13 ++++- 5 files changed, 75 insertions(+), 27 deletions(-) diff --git a/packages/oidc-client/README.md b/packages/oidc-client/README.md index 02a31f977..2cc14490b 100644 --- a/packages/oidc-client/README.md +++ b/packages/oidc-client/README.md @@ -312,7 +312,10 @@ export class OidcClient { /** * Starts the OIDC logout process with specified options. * @param callbackPathOrUrl The callback path or URL to use after logout. - * @param extras Additional parameters to send to the OIDC server during the logout request. {"no_reload:oidc":"true"} to avoid the page reload after logout. + * @param extras Additional parameters to send to the OIDC server during the logout request. + * {"no_reload:oidc":"true"} to avoid the page reload after logout. + * you can add extras like {"client_secret:revoke_refresh_token":"secret"} to revoke the refresh token with extra client secret. Any key ending with ":revoke_refresh_token" will be used to revoke the refresh token. + * you can add extras like {"client_secret:revoke_access_token":"secret"} to revoke the access token with extra client secret. Any key ending with ":revoke_access_token" will be used to revoke the access token. * @returns A promise resolved when the logout is completed. */ logoutAsync(callbackPathOrUrl?: string | null | undefined, extras?: StringMap): Promise; diff --git a/packages/oidc-client/src/logout.spec.ts b/packages/oidc-client/src/logout.spec.ts index 489b846dc..5d18d8705 100644 --- a/packages/oidc-client/src/logout.spec.ts +++ b/packages/oidc-client/src/logout.spec.ts @@ -12,7 +12,9 @@ describe('Logout test suite', () => { {logout_tokens_to_invalidate:['refresh_token'],extras:null, expectedResults: ["token=abdc&token_type_hint=refresh_token&client_id=interactive.public.short"], expectedFinalUrl}, {logout_tokens_to_invalidate:['access_token'],extras:null, expectedResults: ["token=abcd&token_type_hint=access_token&client_id=interactive.public.short"], expectedFinalUrl}, {logout_tokens_to_invalidate:[],extras:null, expectedResults: [], expectedFinalUrl}, - {logout_tokens_to_invalidate:[],extras: {"no_reload:oidc":"true"}, expectedResults: [], expectedFinalUrl:""}, + {logout_tokens_to_invalidate:[],extras: {"no_reload:oidc":"true"}, expectedResults: [], expectedFinalUrl:""}, + {logout_tokens_to_invalidate:['refresh_token'],extras:{"client_secret:revoke_refresh_token":"secret"}, expectedResults: ["token=abdc&token_type_hint=refresh_token&client_id=interactive.public.short&client_secret=secret"], expectedFinalUrl}, + {logout_tokens_to_invalidate:['access_token'],extras:{"client_secret:revoke_access_token":"secret"}, expectedResults: ["token=abcd&token_type_hint=access_token&client_id=interactive.public.short&client_secret=secret"], expectedFinalUrl}, ])('Logout should revoke tokens $logout_tokens_to_invalidate', async ({ logout_tokens_to_invalidate, extras =null, expectedResults, expectedFinalUrl}) => { const configuration = { diff --git a/packages/oidc-client/src/logout.ts b/packages/oidc-client/src/logout.ts index 731bac9d3..d422a9a8d 100644 --- a/packages/oidc-client/src/logout.ts +++ b/packages/oidc-client/src/logout.ts @@ -11,6 +11,33 @@ export const oidcLogoutTokens = { refresh_token: 'refresh_token', }; +const extractExtras = (extras: StringMap, postKey: string):StringMap => { + const postExtras:StringMap = {}; + if (extras) { + for (const [key, value] of Object.entries(extras)) { + if (key.endsWith(postKey)) { + const newKey = key.replace(postKey, ''); + postExtras[newKey] = value; + } + } + return postExtras; + } + return postExtras; +} + +const keepExtras = (extras: StringMap):StringMap => { + const postExtras : StringMap = {}; + if (extras) { + for (const [key, value] of Object.entries(extras)) { + if (!key.includes(':')) { + postExtras[key] = value; + } + } + return postExtras; + } + return postExtras; +} + export const destroyAsync = (oidc) => async (status) => { timer.clearTimeout(oidc.timeoutId); oidc.timeoutId = null; @@ -28,7 +55,10 @@ export const destroyAsync = (oidc) => async (status) => { oidc.userInfo = null; }; -export const logoutAsync = (oidc, oidcDatabase, fetch, console, oicLocation:ILOidcLocation) => async (callbackPathOrUrl: string | null | undefined = undefined, extras: StringMap = null) => { +export const logoutAsync = (oidc, + oidcDatabase, + fetch, + console, oicLocation:ILOidcLocation) => async (callbackPathOrUrl: string | null | undefined = undefined, extras: StringMap = null) => { const configuration = oidc.configuration; const oidcServerConfiguration = await oidc.initAsync(configuration.authority, configuration.authority_configuration); if (callbackPathOrUrl && (typeof callbackPathOrUrl !== 'string')) { @@ -49,12 +79,22 @@ export const logoutAsync = (oidc, oidcDatabase, fetch, console, oicLocation:ILOi const promises = []; const accessToken = oidc.tokens ? oidc.tokens.accessToken : null; if (accessToken && configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.access_token)) { - const revokeAccessTokenPromise = performRevocationRequestAsync(fetch)(revocationEndpoint, accessToken, TOKEN_TYPE.access_token, configuration.client_id); + const revokeAccessTokenExtras = extractExtras(extras, ':revoke_access_token'); + const revokeAccessTokenPromise = performRevocationRequestAsync(fetch)(revocationEndpoint, + accessToken, + TOKEN_TYPE.access_token, + configuration.client_id, + revokeAccessTokenExtras); promises.push(revokeAccessTokenPromise); } const refreshToken = oidc.tokens ? oidc.tokens.refreshToken : null; if (refreshToken && configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.refresh_token)) { - const revokeRefreshTokenPromise = performRevocationRequestAsync(fetch)(revocationEndpoint, refreshToken, TOKEN_TYPE.refresh_token, configuration.client_id); + const revokeAccessTokenExtras = extractExtras(extras, ':revoke_refresh_token'); + const revokeRefreshTokenPromise = performRevocationRequestAsync(fetch)(revocationEndpoint, + refreshToken, + TOKEN_TYPE.refresh_token, + configuration.client_id, + revokeAccessTokenExtras); promises.push(revokeRefreshTokenPromise); } if (promises.length > 0) { @@ -78,34 +118,26 @@ export const logoutAsync = (oidc, oidcDatabase, fetch, console, oicLocation:ILOi oidc.publishEvent(eventNames.logout_from_same_tab, {} ); } } - - let noReload = false; - if(extras) { - extras = {...extras}; - for (const [key, value] of Object.entries(extras)) { - if (key.endsWith('no_reload:oidc')) { - noReload = extras[key] == "true"; - delete extras[key]; - } - } - } + + const oidcExtras = extractExtras(extras, ':oidc'); + let noReload = oidcExtras && oidcExtras['no_reload'] === 'true'; if(noReload) { return; } + + const endPointExtras = keepExtras(extras); if (oidcServerConfiguration.endSessionEndpoint) { - if (!extras) { - extras = { - id_token_hint: idToken, - }; - if (callbackPathOrUrl !== null) { - extras.post_logout_redirect_uri = url; - } + if (!('id_token_hint' in endPointExtras)) { + endPointExtras['id_token_hint'] = idToken; + } + if (!('post_logout_redirect_uri' in endPointExtras) && callbackPathOrUrl !== null) { + endPointExtras['post_logout_redirect_uri'] = url; } let queryString = ''; - if (extras) { - for (const [key, value] of Object.entries(extras)) { + for (const [key, value] of Object.entries(endPointExtras)) { + if(value !== null && value !== undefined) { if (queryString === '') { queryString += '?'; } else { diff --git a/packages/oidc-client/src/oidc.ts b/packages/oidc-client/src/oidc.ts index bdf8dee82..afcc232bf 100644 --- a/packages/oidc-client/src/oidc.ts +++ b/packages/oidc-client/src/oidc.ts @@ -167,7 +167,7 @@ export class Oidc { const isInsideBrowser = (typeof process === 'undefined'); if (!Object.prototype.hasOwnProperty.call(oidcDatabase, name) && isInsideBrowser) { throw Error(`OIDC library does seem initialized. -Please checkout that you are using OIDC hook inside a compoment.`); +Please checkout that you are using OIDC hook inside a component.`); } return oidcDatabase[name]; } diff --git a/packages/oidc-client/src/requests.ts b/packages/oidc-client/src/requests.ts index 9f4ab259a..5321210f0 100644 --- a/packages/oidc-client/src/requests.ts +++ b/packages/oidc-client/src/requests.ts @@ -54,12 +54,23 @@ export const TOKEN_TYPE = { access_token: 'access_token', }; -export const performRevocationRequestAsync = (fetch) => async (url, token, token_type = TOKEN_TYPE.refresh_token, client_id, timeoutMs = 10000) => { +export const performRevocationRequestAsync = (fetch) => async (url, + token, + token_type = TOKEN_TYPE.refresh_token, + client_id, + extras:StringMap = {}, + timeoutMs = 10000) => { const details = { token, token_type_hint: token_type, client_id, }; + for (const [key, value] of Object.entries(extras)) { + + if (details[key] === undefined) { + details[key] = value; + } + } const formBody = []; for (const property in details) {