diff --git a/.changeset/chubby-chairs-dream.md b/.changeset/chubby-chairs-dream.md
new file mode 100644
index 000000000..5d99e20cb
--- /dev/null
+++ b/.changeset/chubby-chairs-dream.md
@@ -0,0 +1,5 @@
+---
+'@forgerock/oidc-client': minor
+---
+
+Implement token `revoke` method
diff --git a/e2e/oidc-app/src/ping-am/index.html b/e2e/oidc-app/src/ping-am/index.html
index 9ae63641e..f1021f29e 100644
--- a/e2e/oidc-app/src/ping-am/index.html
+++ b/e2e/oidc-app/src/ping-am/index.html
@@ -4,8 +4,7 @@
E2E Test | Ping Identity JavaScript SDK
@@ -19,7 +18,8 @@ OIDC App | PingAM Login
-
+
+
Start Over
diff --git a/e2e/oidc-app/src/ping-one/index.html b/e2e/oidc-app/src/ping-one/index.html
index bdcc56f70..f1a50104e 100644
--- a/e2e/oidc-app/src/ping-one/index.html
+++ b/e2e/oidc-app/src/ping-one/index.html
@@ -4,8 +4,7 @@
E2E Test | Ping Identity JavaScript SDK
@@ -19,7 +18,8 @@ OIDC App | P1 Login
-
+
+
Start Over
diff --git a/e2e/oidc-app/src/utils/oidc-app.ts b/e2e/oidc-app/src/utils/oidc-app.ts
index a03e00729..eaa099bff 100644
--- a/e2e/oidc-app/src/utils/oidc-app.ts
+++ b/e2e/oidc-app/src/utils/oidc-app.ts
@@ -27,13 +27,13 @@ function displayTokenResponse(
response: OauthTokens | TokenExchangeErrorResponse | GenericError | AuthorizationError,
) {
const appEl = document.getElementById('app');
- if ('error' in response) {
+ if ('error' in response || !('accessToken' in response)) {
console.error('Token Error:', response);
displayError(response);
} else {
console.log('Token Response:', response);
document.getElementById('logout').style.display = 'block';
- document.getElementById('userinfo').style.display = 'block';
+ document.getElementById('user-info-btn').style.display = 'block';
document.getElementById('login-background').style.display = 'none';
document.getElementById('login-redirect').style.display = 'none';
@@ -108,7 +108,7 @@ export async function oidcApp({ config, urlParams }) {
displayTokenResponse(response);
});
- document.getElementById('userinfo').addEventListener('click', async () => {
+ document.getElementById('user-info-btn').addEventListener('click', async () => {
const userInfo = await oidcClient.user.info();
if ('error' in userInfo) {
@@ -124,16 +124,30 @@ export async function oidcApp({ config, urlParams }) {
}
});
+ document.getElementById('revoke').addEventListener('click', async () => {
+ const response = await oidcClient.token.revoke();
+
+ if ('error' in response) {
+ console.error('Token Revocation Error:', response);
+ displayError(response);
+ } else {
+ const appEl = document.getElementById('app');
+ const userInfoEl = document.createElement('div');
+ userInfoEl.innerHTML = `Token successfully revoked
`;
+ appEl.appendChild(userInfoEl);
+ }
+ });
+
document.getElementById('logout').addEventListener('click', async () => {
const response = await oidcClient.user.logout();
- if (response && 'error' in response) {
+ if ('error' in response) {
console.error('Logout Error:', response);
displayError(response);
} else {
console.log('Logout successful');
document.getElementById('logout').style.display = 'none';
- document.getElementById('userinfo').style.display = 'none';
+ document.getElementById('user-info-btn').style.display = 'none';
document.getElementById('login-background').style.display = 'block';
document.getElementById('login-redirect').style.display = 'block';
window.location.assign(window.location.origin + window.location.pathname);
diff --git a/e2e/oidc-suites/src/login.spec.ts b/e2e/oidc-suites/src/login.spec.ts
index 26c212e1e..1443be4c3 100644
--- a/e2e/oidc-suites/src/login.spec.ts
+++ b/e2e/oidc-suites/src/login.spec.ts
@@ -30,7 +30,6 @@ test.describe('PingAM login and get token tests', () => {
expect(page.url()).toContain('code');
expect(page.url()).toContain('state');
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
- await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
});
test('redirect login with valid credentials', async ({ page }) => {
@@ -47,7 +46,6 @@ test.describe('PingAM login and get token tests', () => {
expect(page.url()).toContain('code');
expect(page.url()).toContain('state');
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
- await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
});
test('background login with invalid client id fails', async ({ page }) => {
@@ -81,7 +79,6 @@ test.describe('PingOne login and get token tests', () => {
expect(page.url()).toContain('code');
expect(page.url()).toContain('state');
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
- await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
});
test('redirect login with valid credentials', async ({ page }) => {
@@ -99,7 +96,6 @@ test.describe('PingOne login and get token tests', () => {
expect(page.url()).toContain('code');
expect(page.url()).toContain('state');
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
- await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
});
test('login with invalid client id fails', async ({ page }) => {
@@ -140,7 +136,6 @@ test.describe('PingOne login and get token tests', () => {
expect(page.url()).toContain('code');
expect(page.url()).toContain('state');
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
- await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
});
});
diff --git a/e2e/oidc-suites/src/token.spec.ts b/e2e/oidc-suites/src/token.spec.ts
index bfd5d7f5d..7758ff223 100644
--- a/e2e/oidc-suites/src/token.spec.ts
+++ b/e2e/oidc-suites/src/token.spec.ts
@@ -15,17 +15,6 @@ import {
} from './utils/demo-users.js';
import { asyncEvents } from './utils/async-events.js';
-test('get tokens without logging in should error', async ({ page }) => {
- const { navigate } = asyncEvents(page);
- await navigate('/ping-am/');
- expect(page.url()).toBe('http://localhost:8443/ping-am/');
-
- await page.getByRole('button', { name: 'Get Tokens' }).click();
-
- await expect(page.locator('.error')).toContainText(`"error": "No tokens found"`);
- await expect(page.locator('.error')).toContainText(`"type": "state_error"`);
-});
-
test.describe('PingAM tokens', () => {
test('login and get tokens', async ({ page }) => {
const { navigate, clickButton } = asyncEvents(page);
@@ -44,7 +33,6 @@ test.describe('PingAM tokens', () => {
await page.getByRole('button', { name: 'Get Tokens' }).click();
await expect(page.locator('#accessToken-1')).not.toBeEmpty();
- await expect(page.locator('#accessToken-1')).not.toHaveText('undefined');
const accessToken0 = await page.locator('#accessToken-0').textContent();
const accessToken1 = await page.locator('#accessToken-1').textContent();
@@ -70,13 +58,35 @@ test.describe('PingAM tokens', () => {
await page.getByRole('button', { name: 'Renew Tokens' }).click();
await expect(page.locator('#accessToken-1')).not.toBeEmpty();
- await expect(page.locator('#accessToken-1')).not.toHaveText('undefined');
const accessToken0 = await page.locator('#accessToken-0').textContent();
const accessToken1 = await page.locator('#accessToken-1').textContent();
await expect(accessToken0).not.toBe(accessToken1);
});
+ test('login and revoke tokens', async ({ page }) => {
+ const { navigate, clickButton } = asyncEvents(page);
+ await navigate('/ping-am/');
+ expect(page.url()).toBe('http://localhost:8443/ping-am/');
+
+ await clickButton('Login (Background)', 'https://openam-sdks.forgeblocks.com/');
+
+ await page.getByLabel('User Name').fill(pingAmUsername);
+ await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword);
+ await page.getByRole('button', { name: 'Next' }).click();
+
+ await page.waitForURL('http://localhost:8443/ping-am/**', { waitUntil: 'networkidle' });
+ expect(page.url()).toContain('code');
+ expect(page.url()).toContain('state');
+
+ await expect(page.locator('#accessToken-0')).not.toBeEmpty();
+
+ await page.getByRole('button', { name: 'Revoke Token' }).click();
+ await expect(page.getByText('Token successfully revoked')).toBeVisible();
+ const token = await page.evaluate(() => localStorage.getItem('pic-oidcTokens'));
+ await expect(token).toBeFalsy();
+ });
+
test('renew tokens without logging in should error', async ({ page }) => {
const { navigate } = asyncEvents(page);
await navigate('/ping-am/');
@@ -110,7 +120,6 @@ test.describe('PingOne tokens', () => {
await page.getByRole('button', { name: 'Get Tokens' }).click();
await expect(page.locator('#accessToken-1')).not.toBeEmpty();
- await expect(page.locator('#accessToken-1')).not.toHaveText('undefined');
const accessToken0 = await page.locator('#accessToken-0').textContent();
const accessToken1 = await page.locator('#accessToken-1').textContent();
@@ -136,13 +145,35 @@ test.describe('PingOne tokens', () => {
await page.getByRole('button', { name: 'Renew Tokens' }).click();
await expect(page.locator('#accessToken-1')).not.toBeEmpty();
- await expect(page.locator('#accessToken-1')).not.toHaveText('undefined');
const accessToken0 = await page.locator('#accessToken-0').textContent();
const accessToken1 = await page.locator('#accessToken-1').textContent();
await expect(accessToken0).not.toBe(accessToken1);
});
+ test('login and revoke tokens', async ({ page }) => {
+ const { navigate, clickButton } = asyncEvents(page);
+ await navigate('/ping-one/');
+ expect(page.url()).toBe('http://localhost:8443/ping-one/');
+
+ await clickButton('Login (Background)', 'https://apps.pingone.ca/');
+
+ await page.getByLabel('Username').fill(pingOneUsername);
+ await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword);
+ await page.getByRole('button', { name: 'Sign On' }).click();
+
+ await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' });
+ expect(page.url()).toContain('code');
+ expect(page.url()).toContain('state');
+
+ await expect(page.locator('#accessToken-0')).not.toBeEmpty();
+
+ await page.getByRole('button', { name: 'Revoke Token' }).click();
+ await expect(page.getByText('Token successfully revoked')).toBeVisible();
+ const token = await page.evaluate(() => localStorage.getItem('pic-oidcTokens'));
+ await expect(token).toBeFalsy();
+ });
+
test('renew tokens without logging in should error', async ({ page }) => {
const { navigate } = asyncEvents(page);
await navigate('/ping-one/');
@@ -155,3 +186,25 @@ test.describe('PingOne tokens', () => {
await expect(page.locator('.error')).toContainText('User authentication is required');
});
});
+
+test('get tokens without logging in should error', async ({ page }) => {
+ const { navigate } = asyncEvents(page);
+ await navigate('/ping-am/');
+ expect(page.url()).toBe('http://localhost:8443/ping-am/');
+
+ await page.getByRole('button', { name: 'Get Tokens' }).click();
+
+ await expect(page.locator('.error')).toContainText(`"error": "No tokens found"`);
+ await expect(page.locator('.error')).toContainText(`"type": "state_error"`);
+});
+
+test('revoke tokens should error with missing token', async ({ page }) => {
+ const { navigate } = asyncEvents(page);
+ await navigate('/ping-am/');
+ expect(page.url()).toBe('http://localhost:8443/ping-am/');
+
+ await page.getByRole('button', { name: 'Revoke Token' }).click();
+
+ await expect(page.locator('.error')).toContainText(`"error": "No access token found"`);
+ await expect(page.locator('.error')).toContainText(`"type": "state_error"`);
+});
diff --git a/e2e/oidc-suites/src/user.spec.ts b/e2e/oidc-suites/src/user.spec.ts
index 26978290a..5012b49f4 100644
--- a/e2e/oidc-suites/src/user.spec.ts
+++ b/e2e/oidc-suites/src/user.spec.ts
@@ -27,11 +27,11 @@ test.describe('User tests', () => {
await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword);
await page.getByRole('button', { name: 'Next' }).click();
- await page.waitForURL('http://localhost:8443/ping-am/**');
+ await page.waitForURL('http://localhost:8443/ping-am/**', { waitUntil: 'networkidle' });
expect(page.url()).toContain('code');
expect(page.url()).toContain('state');
- await page.getByRole('button', { name: 'User Info' }).click();
+ await clickButton('User Info', 'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/userinfo');
await expect(page.locator('#userInfo')).not.toBeEmpty();
await expect(page.getByText('Sdk User')).toBeVisible();
await expect(page.getByText('sdkuser@example.com')).toBeVisible();
@@ -48,13 +48,28 @@ test.describe('User tests', () => {
await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword);
await page.getByRole('button', { name: 'Sign On' }).click();
- await page.waitForURL('http://localhost:8443/ping-one/**');
+ await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' });
expect(page.url()).toContain('code');
expect(page.url()).toContain('state');
- await page.getByRole('button', { name: 'User Info' }).click();
+ await clickButton(
+ 'User Info',
+ 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo',
+ );
await expect(page.locator('#userInfo')).not.toBeEmpty();
await expect(page.getByText('demouser')).toBeVisible();
await expect(page.getByText('demouser@user.com')).toBeVisible();
});
+
+ test('get user info should error with missing token', async ({ page }) => {
+ const { navigate } = asyncEvents(page);
+ await navigate('/ping-am/');
+ expect(page.url()).toBe('http://localhost:8443/ping-am/');
+
+ await page.getByRole('button', { name: 'User Info' }).click();
+
+ await expect(page.locator('#userInfo')).not.toBeVisible();
+ await expect(page.locator('.error')).toContainText(`"error": "No access token found"`);
+ await expect(page.locator('.error')).toContainText(`"type": "auth_error"`);
+ });
});
diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts
index d7b50f209..675815fb4 100644
--- a/packages/oidc-client/src/lib/client.store.ts
+++ b/packages/oidc-client/src/lib/client.store.ts
@@ -19,10 +19,17 @@ import { wellknownApi, wellknownSelector } from './wellknown.api.js';
import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware';
import type { GenericError, GetAuthorizationUrlOptions } from '@forgerock/sdk-types';
-import type { GetTokensOptions, LogoutResult } from './client.types.js';
+import type {
+ GetTokensOptions,
+ LogoutErrorResult,
+ LogoutSuccessResult,
+ RevokeErrorResult,
+ RevokeSuccessResult,
+ UserInfoResponse,
+} from './client.types.js';
import type { OauthTokens, OidcConfig } from './config.types.js';
import type { AuthorizationError, AuthorizationSuccess } from './authorize.request.types.js';
-import type { TokenExchangeErrorResponse, TokenExchangeResponse } from './exchange.types.js';
+import type { TokenExchangeErrorResponse } from './exchange.types.js';
import { isExpiryWithinThreshold } from './token.utils.js';
import { logoutµ } from './logout.request.js';
@@ -155,7 +162,7 @@ export async function oidc({
},
},
/**
- * An object containing methods for token exchange
+ * An object containing methods for token management
*/
token: {
/**
@@ -295,6 +302,106 @@ export async function oidc({
};
}
},
+ /**
+ * @method revoke
+ * @description Revokes an access token using the revocation endpoint from the wellknown configuration.
+ * It requires an access token stored in the configured storage.
+ * @returns {Promise} - Returns a promise that resolves to the revoke response or an error response.
+ */
+ revoke: async (): Promise => {
+ const state = store.getState();
+ const wellknown = wellknownSelector(wellknownUrl, state);
+
+ if (!wellknown?.revocation_endpoint) {
+ return {
+ error: 'Wellknown missing revocation endpoint',
+ type: 'wellknown_error',
+ };
+ }
+
+ const tokens = await storageClient.get();
+
+ if (!tokens || !('accessToken' in tokens)) {
+ return {
+ error: 'No access token found',
+ type: 'state_error',
+ };
+ }
+
+ const revokeµ = Micro.promise(() =>
+ store.dispatch(
+ oidcApi.endpoints.revoke.initiate({
+ accessToken: tokens.accessToken,
+ clientId: config.clientId,
+ endpoint: wellknown.revocation_endpoint,
+ }),
+ ),
+ ).pipe(
+ Micro.map(({ error }) => {
+ if (error) {
+ let message = 'An error occurred while revoking the token';
+ let status: number | string = 'unknown';
+ if ('message' in error && error.message) {
+ message = error.message;
+ }
+ if ('status' in error) {
+ status = error.status;
+ }
+ return {
+ error: 'Token revocation failure',
+ message,
+ type: 'auth_error',
+ status,
+ } as GenericError;
+ }
+
+ return null;
+ }),
+ // Delete local token and return combined results
+ Micro.flatMap((revokeResponse) =>
+ Micro.promise(() => storageClient.remove()).pipe(
+ Micro.flatMap((deleteRes) => {
+ const deleteResponse = typeof deleteRes === 'undefined' ? null : deleteRes;
+
+ const isInnerRequestError =
+ (revokeResponse && 'error' in revokeResponse) ||
+ (deleteResponse &&
+ typeof deleteResponse === 'object' &&
+ 'error' in deleteResponse);
+
+ if (isInnerRequestError) {
+ const result: RevokeErrorResult = {
+ error: 'Inner request error',
+ revokeResponse,
+ deleteResponse,
+ };
+ return Micro.fail(result);
+ } else {
+ const result: RevokeSuccessResult = {
+ revokeResponse: null,
+ deleteResponse: null,
+ };
+ return Micro.succeed(result);
+ }
+ }),
+ ),
+ ),
+ );
+
+ const result = await Micro.runPromiseExit(revokeµ);
+
+ if (exitIsSuccess(result)) {
+ return result.value;
+ } else if (exitIsFail(result)) {
+ return result.cause.error;
+ } else {
+ return {
+ error: 'Token revocation failure',
+ message: result.cause.message,
+ type: 'auth_error',
+ };
+ }
+ },
},
/**
@@ -305,9 +412,9 @@ export async function oidc({
* @method info
* @description Retrieves user information using the userinfo endpoint from the wellknown configuration.
* It requires an access token stored in the configured storage.
- * @returns {Promise} - Returns a promise that resolves to user information or an error response.
+ * @returns {Promise} - Returns a promise that resolves to user information or an error response.
*/
- info: async (): Promise => {
+ info: async (): Promise => {
const state = store.getState();
const wellknown = wellknownSelector(wellknownUrl, state);
@@ -375,9 +482,9 @@ export async function oidc({
* @method logout
* @description Logs out the user by revoking tokens and clearing the storage.
* It uses the end session endpoint from the wellknown configuration.
- * @returns {Promise} - Returns a promise that resolves to the logout response or an error.
+ * @returns {Promise} - Returns a promise that resolves to the logout response or an error.
*/
- logout: async (): Promise => {
+ logout: async (): Promise => {
const state = store.getState();
const wellknown = wellknownSelector(wellknownUrl, state);
@@ -388,6 +495,13 @@ export async function oidc({
};
}
+ if (!wellknown?.revocation_endpoint) {
+ return {
+ error: 'Wellknown missing revocation endpoint',
+ type: 'wellknown_error',
+ };
+ }
+
const tokens = await storageClient.get();
if (!tokens) {
diff --git a/packages/oidc-client/src/lib/client.types.ts b/packages/oidc-client/src/lib/client.types.ts
index a7c3f2a31..995279438 100644
--- a/packages/oidc-client/src/lib/client.types.ts
+++ b/packages/oidc-client/src/lib/client.types.ts
@@ -7,11 +7,24 @@ export interface GetTokensOptions {
storageOptions?: Partial;
}
-export type LogoutResult = Promise<
- | GenericError
- | {
- sessionResponse: GenericError | null;
- revokeResponse: GenericError | null;
- deleteResponse: GenericError | null;
- }
->;
+export type RevokeSuccessResult = {
+ revokeResponse: GenericError | null;
+ deleteResponse: GenericError | null;
+};
+
+export type RevokeErrorResult = RevokeSuccessResult & {
+ error: string;
+};
+
+export type LogoutSuccessResult = RevokeSuccessResult & {
+ sessionResponse: GenericError | null;
+};
+
+export type LogoutErrorResult = LogoutSuccessResult & {
+ error: string;
+};
+
+export type UserInfoResponse = {
+ sub: string;
+ [key: string]: unknown;
+};
diff --git a/packages/oidc-client/src/lib/logout.request.test.ts b/packages/oidc-client/src/lib/logout.request.test.ts
index 5f8388e3c..39beb1765 100644
--- a/packages/oidc-client/src/lib/logout.request.test.ts
+++ b/packages/oidc-client/src/lib/logout.request.test.ts
@@ -6,6 +6,7 @@
*/
import { it, expect, describe } from '@effect/vitest';
import { Micro } from 'effect';
+import { deepStrictEqual } from 'node:assert';
import { setupServer } from 'msw/node';
import { logoutµ } from './logout.request.js';
import { OauthTokens, OidcConfig } from './config.types.js';
@@ -127,28 +128,34 @@ describe('Ping AM', () => {
const end_session_endpoint = 'https://example.com/am/oauth2/fake-realm/connect/endSession';
const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke';
- const result = yield* logoutµ({
- tokens,
- config,
- wellknown: {
- ...partialWellknown,
- end_session_endpoint,
- revocation_endpoint,
- },
- store,
- storageClient,
- });
-
- expect(result).toStrictEqual({
- sessionResponse: {
- error: 'End Session failure',
- message: 'An error occurred while ending the session',
- type: 'auth_error',
- status: 400,
- },
- revokeResponse: null,
- deleteResponse: null,
- });
+ const result = yield* Micro.exit(
+ logoutµ({
+ tokens,
+ config,
+ wellknown: {
+ ...partialWellknown,
+ end_session_endpoint,
+ revocation_endpoint,
+ },
+ store,
+ storageClient,
+ }),
+ );
+
+ deepStrictEqual(
+ result,
+ Micro.exitFail({
+ error: 'Inner request error',
+ sessionResponse: {
+ error: 'End Session failure',
+ message: 'An error occurred while ending the session',
+ type: 'auth_error',
+ status: 400,
+ },
+ revokeResponse: null,
+ deleteResponse: null,
+ }),
+ );
}),
);
@@ -157,28 +164,34 @@ describe('Ping AM', () => {
const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession';
const revocation_endpoint = 'https://example.com/am/oauth2/fake-realm/token/revoke';
- const result = yield* logoutµ({
- tokens,
- config,
- wellknown: {
- ...partialWellknown,
- end_session_endpoint,
- revocation_endpoint,
- },
- store,
- storageClient,
- });
-
- expect(result).toStrictEqual({
- sessionResponse: null,
- revokeResponse: {
- error: 'End Session failure',
- message: 'An error occurred while ending the session',
- type: 'auth_error',
- status: 400,
- },
- deleteResponse: null,
- });
+ const result = yield* Micro.exit(
+ logoutµ({
+ tokens,
+ config,
+ wellknown: {
+ ...partialWellknown,
+ end_session_endpoint,
+ revocation_endpoint,
+ },
+ store,
+ storageClient,
+ }),
+ );
+
+ deepStrictEqual(
+ result,
+ Micro.exitFail({
+ error: 'Inner request error',
+ sessionResponse: null,
+ revokeResponse: {
+ error: 'End Session failure',
+ message: 'An error occurred while ending the session',
+ type: 'auth_error',
+ status: 400,
+ },
+ deleteResponse: null,
+ }),
+ );
}),
);
});
@@ -217,29 +230,35 @@ describe('PingOne', () => {
const ping_end_idp_session_endpoint = 'https://example.com/as/badIdpSignoff';
const revocation_endpoint = 'https://example.com/as/revoke';
- const result = yield* logoutµ({
- tokens,
- config,
- wellknown: {
- ...partialWellknown,
- ping_end_idp_session_endpoint,
- end_session_endpoint: fakeEndSessionEndpoint,
- revocation_endpoint,
- },
- store,
- storageClient,
- });
-
- expect(result).toStrictEqual({
- sessionResponse: {
- error: 'End Session failure',
- message: 'An error occurred while ending the session',
- type: 'auth_error',
- status: 400,
- },
- revokeResponse: null,
- deleteResponse: null,
- });
+ const result = yield* Micro.exit(
+ logoutµ({
+ tokens,
+ config,
+ wellknown: {
+ ...partialWellknown,
+ ping_end_idp_session_endpoint,
+ end_session_endpoint: fakeEndSessionEndpoint,
+ revocation_endpoint,
+ },
+ store,
+ storageClient,
+ }),
+ );
+
+ deepStrictEqual(
+ result,
+ Micro.exitFail({
+ error: 'Inner request error',
+ sessionResponse: {
+ error: 'End Session failure',
+ message: 'An error occurred while ending the session',
+ type: 'auth_error',
+ status: 400,
+ },
+ revokeResponse: null,
+ deleteResponse: null,
+ }),
+ );
}),
);
@@ -248,29 +267,35 @@ describe('PingOne', () => {
const ping_end_idp_session_endpoint = 'https://example.com/as/idpSignoff';
const revocation_endpoint = 'https://example.com/as/badRevoke';
- const result = yield* logoutµ({
- tokens,
- config,
- wellknown: {
- ...partialWellknown,
- ping_end_idp_session_endpoint,
- end_session_endpoint: fakeEndSessionEndpoint,
- revocation_endpoint,
- },
- store,
- storageClient,
- });
-
- expect(result).toStrictEqual({
- sessionResponse: null,
- revokeResponse: {
- error: 'End Session failure',
- message: 'An error occurred while ending the session',
- type: 'auth_error',
- status: 400,
- },
- deleteResponse: null,
- });
+ const result = yield* Micro.exit(
+ logoutµ({
+ tokens,
+ config,
+ wellknown: {
+ ...partialWellknown,
+ ping_end_idp_session_endpoint,
+ end_session_endpoint: fakeEndSessionEndpoint,
+ revocation_endpoint,
+ },
+ store,
+ storageClient,
+ }),
+ );
+
+ deepStrictEqual(
+ result,
+ Micro.exitFail({
+ error: 'Inner request error',
+ sessionResponse: null,
+ revokeResponse: {
+ error: 'End Session failure',
+ message: 'An error occurred while ending the session',
+ type: 'auth_error',
+ status: 400,
+ },
+ deleteResponse: null,
+ }),
+ );
}),
);
});
diff --git a/packages/oidc-client/src/lib/logout.request.ts b/packages/oidc-client/src/lib/logout.request.ts
index 7c80ad8e3..04480fb9a 100644
--- a/packages/oidc-client/src/lib/logout.request.ts
+++ b/packages/oidc-client/src/lib/logout.request.ts
@@ -10,6 +10,7 @@ import { createClientStore, createLogoutError } from './client.store.utils.js';
import { OauthTokens, OidcConfig } from './config.types.js';
import { WellKnownResponse } from '@forgerock/sdk-types';
import { createStorage } from '@forgerock/storage';
+import { LogoutErrorResult, LogoutSuccessResult } from './client.types.js';
export function logoutµ({
tokens,
@@ -48,15 +49,33 @@ export function logoutµ({
).pipe(
// Delete local token and return combined results
Micro.flatMap(([sessionResponse, revokeResponse]) =>
- Micro.promise(async () => {
- const deleteRes = await storageClient.remove();
- const deleteResponse = typeof deleteRes === 'undefined' ? null : deleteRes;
- return {
- sessionResponse,
- revokeResponse,
- deleteResponse,
- };
- }),
+ Micro.promise(() => storageClient.remove()).pipe(
+ Micro.flatMap((deleteRes) => {
+ const deleteResponse = typeof deleteRes === 'undefined' ? null : deleteRes;
+
+ const isInnerRequestError =
+ (sessionResponse && 'error' in sessionResponse) ||
+ (revokeResponse && 'error' in revokeResponse) ||
+ (deleteResponse && typeof deleteResponse === 'object' && 'error' in deleteResponse);
+
+ if (isInnerRequestError) {
+ const result: LogoutErrorResult = {
+ error: 'Inner request error',
+ sessionResponse,
+ revokeResponse,
+ deleteResponse,
+ };
+ return Micro.fail(result);
+ } else {
+ const result: LogoutSuccessResult = {
+ sessionResponse: null,
+ revokeResponse: null,
+ deleteResponse: null,
+ };
+ return Micro.succeed(result);
+ }
+ }),
+ ),
),
);
}
diff --git a/packages/oidc-client/src/lib/oidc.api.ts b/packages/oidc-client/src/lib/oidc.api.ts
index 7da87da26..8cb0b1c45 100644
--- a/packages/oidc-client/src/lib/oidc.api.ts
+++ b/packages/oidc-client/src/lib/oidc.api.ts
@@ -12,6 +12,7 @@ import {
import type { TokenExchangeResponse } from './exchange.types.js';
import { AuthorizationSuccess, AuthorizeSuccessResponse } from './authorize.request.types.js';
import { iFrameManager } from '@forgerock/iframe-manager';
+import { UserInfoResponse } from './client.types.js';
interface Extras {
requestMiddleware: RequestMiddleware[];
@@ -336,7 +337,7 @@ export const oidcApi = createApi({
return response as { data: object };
},
}),
- userInfo: builder.mutation({
+ userInfo: builder.mutation({
queryFn: async ({ accessToken, endpoint }, api, _, baseQuery) => {
const { requestMiddleware, logger } = api.extra as Extras;
@@ -375,7 +376,7 @@ export const oidcApi = createApi({
logger.debug('OIDC userInfo API response', response);
- return response as { data: TokenExchangeResponse };
+ return response as { data: UserInfoResponse };
},
}),
}),