Skip to content

Commit

Permalink
feat(oidc): add extras to revoke access_token and refresh_tokens (#1295
Browse files Browse the repository at this point in the history
…) (release)
  • Loading branch information
guillaume-chervet committed Feb 13, 2024
1 parent 2969334 commit 5e35346
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 27 deletions.
5 changes: 4 additions & 1 deletion packages/oidc-client/README.md
Expand Up @@ -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<void>;
Expand Down
4 changes: 3 additions & 1 deletion packages/oidc-client/src/logout.spec.ts
Expand Up @@ -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 = {
Expand Down
78 changes: 55 additions & 23 deletions packages/oidc-client/src/logout.ts
Expand Up @@ -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;
Expand All @@ -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')) {
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/oidc-client/src/oidc.ts
Expand Up @@ -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 <OidcProvider configurationName="${name}"></OidcProvider> compoment.`);
Please checkout that you are using OIDC hook inside a <OidcProvider configurationName="${name}"></OidcProvider> component.`);
}
return oidcDatabase[name];
}
Expand Down
13 changes: 12 additions & 1 deletion packages/oidc-client/src/requests.ts
Expand Up @@ -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) {
Expand Down

0 comments on commit 5e35346

Please sign in to comment.