Skip to content

Commit c48d9c8

Browse files
committed
feat(oidc-client): add token.revoke method
1 parent ba47ce8 commit c48d9c8

File tree

12 files changed

+389
-160
lines changed

12 files changed

+389
-160
lines changed

.changeset/chubby-chairs-dream.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@forgerock/oidc-client': minor
3+
---
4+
5+
Implement token `revoke` method

e2e/oidc-app/src/ping-am/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
<title>E2E Test | Ping Identity JavaScript SDK</title>
55

66
<style>
7-
#logout,
8-
#user-info-btn {
7+
#logout {
98
display: none;
109
}
1110
</style>
@@ -20,6 +19,7 @@ <h1>OIDC App | PingAM Login</h1>
2019
<button id="renew-tokens">Renew Tokens</button>
2120
<button id="logout">Logout</button>
2221
<button id="user-info-btn">User Info</button>
22+
<button id="revoke">Revoke Token</button>
2323
<a href="/ping-am/">Start Over</a>
2424
</div>
2525
<script type="module" src="./main.ts"></script>

e2e/oidc-app/src/ping-one/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
<title>E2E Test | Ping Identity JavaScript SDK</title>
55

66
<style>
7-
#logout,
8-
#user-info-btn {
7+
#logout {
98
display: none;
109
}
1110
</style>
@@ -20,6 +19,7 @@ <h1>OIDC App | P1 Login</h1>
2019
<button id="renew-tokens">Renew Tokens</button>
2120
<button id="logout">Logout</button>
2221
<button id="user-info-btn">User Info</button>
22+
<button id="revoke">Revoke Token</button>
2323
<a href="/ping-one/">Start Over</a>
2424
</div>
2525
<script type="module" src="./main.ts"></script>

e2e/oidc-app/src/utils/oidc-app.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function displayTokenResponse(
2727
response: OauthTokens | TokenExchangeErrorResponse | GenericError | AuthorizationError,
2828
) {
2929
const appEl = document.getElementById('app');
30-
if ('error' in response) {
30+
if ('error' in response || !('accessToken' in response)) {
3131
console.error('Token Error:', response);
3232
displayError(response);
3333
} else {
@@ -111,7 +111,7 @@ export async function oidcApp({ config, urlParams }) {
111111
document.getElementById('user-info-btn').addEventListener('click', async () => {
112112
const userInfo = await oidcClient.user.info();
113113

114-
if (typeof userInfo === 'object' && 'error' in userInfo) {
114+
if ('error' in userInfo) {
115115
console.error('User Info Error:', userInfo);
116116
displayError(userInfo);
117117
} else {
@@ -124,10 +124,24 @@ export async function oidcApp({ config, urlParams }) {
124124
}
125125
});
126126

127+
document.getElementById('revoke').addEventListener('click', async () => {
128+
const response = await oidcClient.token.revoke();
129+
130+
if ('error' in response) {
131+
console.error('Token Revocation Error:', response);
132+
displayError(response);
133+
} else {
134+
const appEl = document.getElementById('app');
135+
const userInfoEl = document.createElement('div');
136+
userInfoEl.innerHTML = `<p>Token successfully revoked</p>`;
137+
appEl.appendChild(userInfoEl);
138+
}
139+
});
140+
127141
document.getElementById('logout').addEventListener('click', async () => {
128142
const response = await oidcClient.user.logout();
129143

130-
if (response && 'error' in response) {
144+
if ('error' in response) {
131145
console.error('Logout Error:', response);
132146
displayError(response);
133147
} else {

e2e/oidc-suites/src/login.spec.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ test.describe('PingAM login and get token tests', () => {
3030
expect(page.url()).toContain('code');
3131
expect(page.url()).toContain('state');
3232
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
33-
await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
3433
});
3534

3635
test('redirect login with valid credentials', async ({ page }) => {
@@ -47,7 +46,6 @@ test.describe('PingAM login and get token tests', () => {
4746
expect(page.url()).toContain('code');
4847
expect(page.url()).toContain('state');
4948
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
50-
await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
5149
});
5250

5351
test('background login with invalid client id fails', async ({ page }) => {
@@ -81,7 +79,6 @@ test.describe('PingOne login and get token tests', () => {
8179
expect(page.url()).toContain('code');
8280
expect(page.url()).toContain('state');
8381
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
84-
await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
8582
});
8683

8784
test('redirect login with valid credentials', async ({ page }) => {
@@ -99,7 +96,6 @@ test.describe('PingOne login and get token tests', () => {
9996
expect(page.url()).toContain('code');
10097
expect(page.url()).toContain('state');
10198
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
102-
await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
10399
});
104100

105101
test('login with invalid client id fails', async ({ page }) => {
@@ -140,7 +136,6 @@ test.describe('PingOne login and get token tests', () => {
140136
expect(page.url()).toContain('code');
141137
expect(page.url()).toContain('state');
142138
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
143-
await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
144139
});
145140
});
146141

e2e/oidc-suites/src/token.spec.ts

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,6 @@ import {
1515
} from './utils/demo-users.js';
1616
import { asyncEvents } from './utils/async-events.js';
1717

18-
test('get tokens without logging in should error', async ({ page }) => {
19-
const { navigate } = asyncEvents(page);
20-
await navigate('/ping-am/');
21-
expect(page.url()).toBe('http://localhost:8443/ping-am/');
22-
23-
await page.getByRole('button', { name: 'Get Tokens' }).click();
24-
25-
await expect(page.locator('.error')).toContainText(`"error": "No tokens found"`);
26-
await expect(page.locator('.error')).toContainText(`"type": "state_error"`);
27-
});
28-
2918
test.describe('PingAM tokens', () => {
3019
test('login and get tokens', async ({ page }) => {
3120
const { navigate, clickButton } = asyncEvents(page);
@@ -44,7 +33,6 @@ test.describe('PingAM tokens', () => {
4433

4534
await page.getByRole('button', { name: 'Get Tokens' }).click();
4635
await expect(page.locator('#accessToken-1')).not.toBeEmpty();
47-
await expect(page.locator('#accessToken-1')).not.toHaveText('undefined');
4836

4937
const accessToken0 = await page.locator('#accessToken-0').textContent();
5038
const accessToken1 = await page.locator('#accessToken-1').textContent();
@@ -70,13 +58,35 @@ test.describe('PingAM tokens', () => {
7058
await page.getByRole('button', { name: 'Renew Tokens' }).click();
7159

7260
await expect(page.locator('#accessToken-1')).not.toBeEmpty();
73-
await expect(page.locator('#accessToken-1')).not.toHaveText('undefined');
7461

7562
const accessToken0 = await page.locator('#accessToken-0').textContent();
7663
const accessToken1 = await page.locator('#accessToken-1').textContent();
7764
await expect(accessToken0).not.toBe(accessToken1);
7865
});
7966

67+
test('login and revoke tokens', async ({ page }) => {
68+
const { navigate, clickButton } = asyncEvents(page);
69+
await navigate('/ping-am/');
70+
expect(page.url()).toBe('http://localhost:8443/ping-am/');
71+
72+
await clickButton('Login (Background)', 'https://openam-sdks.forgeblocks.com/');
73+
74+
await page.getByLabel('User Name').fill(pingAmUsername);
75+
await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword);
76+
await page.getByRole('button', { name: 'Next' }).click();
77+
78+
await page.waitForURL('http://localhost:8443/ping-am/**', { waitUntil: 'networkidle' });
79+
expect(page.url()).toContain('code');
80+
expect(page.url()).toContain('state');
81+
82+
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
83+
84+
await page.getByRole('button', { name: 'Revoke Token' }).click();
85+
await expect(page.getByText('Token successfully revoked')).toBeVisible();
86+
const token = await page.evaluate(() => localStorage.getItem('pic-oidcTokens'));
87+
await expect(token).toBeFalsy();
88+
});
89+
8090
test('renew tokens without logging in should error', async ({ page }) => {
8191
const { navigate } = asyncEvents(page);
8292
await navigate('/ping-am/');
@@ -110,7 +120,6 @@ test.describe('PingOne tokens', () => {
110120

111121
await page.getByRole('button', { name: 'Get Tokens' }).click();
112122
await expect(page.locator('#accessToken-1')).not.toBeEmpty();
113-
await expect(page.locator('#accessToken-1')).not.toHaveText('undefined');
114123

115124
const accessToken0 = await page.locator('#accessToken-0').textContent();
116125
const accessToken1 = await page.locator('#accessToken-1').textContent();
@@ -136,13 +145,35 @@ test.describe('PingOne tokens', () => {
136145
await page.getByRole('button', { name: 'Renew Tokens' }).click();
137146

138147
await expect(page.locator('#accessToken-1')).not.toBeEmpty();
139-
await expect(page.locator('#accessToken-1')).not.toHaveText('undefined');
140148

141149
const accessToken0 = await page.locator('#accessToken-0').textContent();
142150
const accessToken1 = await page.locator('#accessToken-1').textContent();
143151
await expect(accessToken0).not.toBe(accessToken1);
144152
});
145153

154+
test('login and revoke tokens', async ({ page }) => {
155+
const { navigate, clickButton } = asyncEvents(page);
156+
await navigate('/ping-one/');
157+
expect(page.url()).toBe('http://localhost:8443/ping-one/');
158+
159+
await clickButton('Login (Background)', 'https://apps.pingone.ca/');
160+
161+
await page.getByLabel('Username').fill(pingOneUsername);
162+
await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword);
163+
await page.getByRole('button', { name: 'Sign On' }).click();
164+
165+
await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' });
166+
expect(page.url()).toContain('code');
167+
expect(page.url()).toContain('state');
168+
169+
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
170+
171+
await page.getByRole('button', { name: 'Revoke Token' }).click();
172+
await expect(page.getByText('Token successfully revoked')).toBeVisible();
173+
const token = await page.evaluate(() => localStorage.getItem('pic-oidcTokens'));
174+
await expect(token).toBeFalsy();
175+
});
176+
146177
test('renew tokens without logging in should error', async ({ page }) => {
147178
const { navigate } = asyncEvents(page);
148179
await navigate('/ping-one/');
@@ -155,3 +186,25 @@ test.describe('PingOne tokens', () => {
155186
await expect(page.locator('.error')).toContainText('User authentication is required');
156187
});
157188
});
189+
190+
test('get tokens without logging in should error', async ({ page }) => {
191+
const { navigate } = asyncEvents(page);
192+
await navigate('/ping-am/');
193+
expect(page.url()).toBe('http://localhost:8443/ping-am/');
194+
195+
await page.getByRole('button', { name: 'Get Tokens' }).click();
196+
197+
await expect(page.locator('.error')).toContainText(`"error": "No tokens found"`);
198+
await expect(page.locator('.error')).toContainText(`"type": "state_error"`);
199+
});
200+
201+
test('revoke tokens should error with missing token', async ({ page }) => {
202+
const { navigate } = asyncEvents(page);
203+
await navigate('/ping-am/');
204+
expect(page.url()).toBe('http://localhost:8443/ping-am/');
205+
206+
await page.getByRole('button', { name: 'Revoke Token' }).click();
207+
208+
await expect(page.locator('.error')).toContainText(`"error": "No access token found"`);
209+
await expect(page.locator('.error')).toContainText(`"type": "state_error"`);
210+
});

e2e/oidc-suites/src/user.spec.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ test.describe('User tests', () => {
2727
await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword);
2828
await page.getByRole('button', { name: 'Next' }).click();
2929

30-
await page.waitForURL('http://localhost:8443/ping-am/**');
30+
await page.waitForURL('http://localhost:8443/ping-am/**', { waitUntil: 'networkidle' });
3131
expect(page.url()).toContain('code');
3232
expect(page.url()).toContain('state');
3333

34-
await page.getByRole('button', { name: 'User Info' }).click();
34+
await clickButton('User Info', 'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/userinfo');
3535
await expect(page.locator('#userInfo')).not.toBeEmpty();
3636
await expect(page.getByText('Sdk User')).toBeVisible();
3737
await expect(page.getByText('[email protected]')).toBeVisible();
@@ -48,34 +48,24 @@ test.describe('User tests', () => {
4848
await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword);
4949
await page.getByRole('button', { name: 'Sign On' }).click();
5050

51-
await page.waitForURL('http://localhost:8443/ping-one/**');
51+
await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' });
5252
expect(page.url()).toContain('code');
5353
expect(page.url()).toContain('state');
5454

55-
await page.getByRole('button', { name: 'User Info' }).click();
55+
await clickButton(
56+
'User Info',
57+
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo',
58+
);
5659
await expect(page.locator('#userInfo')).not.toBeEmpty();
5760
await expect(page.getByText('demouser')).toBeVisible();
5861
await expect(page.getByText('[email protected]')).toBeVisible();
5962
});
6063

6164
test('get user info should error with missing token', async ({ page }) => {
62-
const { navigate, clickButton } = asyncEvents(page);
65+
const { navigate } = asyncEvents(page);
6366
await navigate('/ping-am/');
6467
expect(page.url()).toBe('http://localhost:8443/ping-am/');
6568

66-
await clickButton('Login (Background)', 'https://openam-sdks.forgeblocks.com/');
67-
68-
await page.getByLabel('User Name').fill(pingAmUsername);
69-
await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword);
70-
await page.getByRole('button', { name: 'Next' }).click();
71-
72-
await page.waitForURL('http://localhost:8443/ping-am/**');
73-
expect(page.url()).toContain('code');
74-
expect(page.url()).toContain('state');
75-
await expect(page.locator('#accessToken-0')).not.toBeEmpty();
76-
await expect(page.locator('#accessToken-0')).not.toHaveText('undefined');
77-
78-
await page.evaluate(() => window.localStorage.clear());
7969
await page.getByRole('button', { name: 'User Info' }).click();
8070

8171
await expect(page.locator('#userInfo')).not.toBeVisible();

0 commit comments

Comments
 (0)