Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chubby-chairs-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/oidc-client': minor
---

Implement token `revoke` method
6 changes: 3 additions & 3 deletions e2e/oidc-app/src/ping-am/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
<title>E2E Test | Ping Identity JavaScript SDK</title>

<style>
#logout,
#userinfo {
#logout {
display: none;
}
</style>
Expand All @@ -19,7 +18,8 @@ <h1>OIDC App | PingAM Login</h1>
<button id="get-tokens">Get Tokens</button>
<button id="renew-tokens">Renew Tokens</button>
<button id="logout">Logout</button>
<button id="userinfo">User Info</button>
<button id="user-info-btn">User Info</button>
<button id="revoke">Revoke Token</button>
<a href="/ping-am/">Start Over</a>
</div>
<script type="module" src="./main.ts"></script>
Expand Down
6 changes: 3 additions & 3 deletions e2e/oidc-app/src/ping-one/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
<title>E2E Test | Ping Identity JavaScript SDK</title>

<style>
#logout,
#userinfo {
#logout {
display: none;
}
</style>
Expand All @@ -19,7 +18,8 @@ <h1>OIDC App | P1 Login</h1>
<button id="get-tokens">Get Tokens</button>
<button id="renew-tokens">Renew Tokens</button>
<button id="logout">Logout</button>
<button id="userinfo">User Info</button>
<button id="user-info-btn">User Info</button>
<button id="revoke">Revoke Token</button>
<a href="/ping-one/">Start Over</a>
</div>
<script type="module" src="./main.ts"></script>
Expand Down
24 changes: 19 additions & 5 deletions e2e/oidc-app/src/utils/oidc-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand All @@ -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 = `<p>Token successfully revoked</p>`;
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);
Expand Down
5 changes: 0 additions & 5 deletions e2e/oidc-suites/src/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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 }) => {
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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');
});
});

Expand Down
83 changes: 68 additions & 15 deletions e2e/oidc-suites/src/token.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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/');
Expand Down Expand Up @@ -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();
Expand All @@ -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/');
Expand All @@ -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"`);
});
23 changes: 19 additions & 4 deletions e2e/oidc-suites/src/user.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think networkidle is an anti-pattern or discouraged.

Copy link
Collaborator

@ryanbas21 ryanbas21 Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'networkidle' - DISCOURAGED consider operation to be finished when there are no network connections for at least 500 ms. Don't use this method for testing, rely on web assertions to assess readiness instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This true but we use it a lot in our tests rights now. We have a ticket to do a bunch of Playwright updates later on.
https://pingidentity.atlassian.net/browse/SDKS-4331

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do use networkidle in more places, let's just note it in that Playwright improvement story and leave it for then. Ultimately, I'd like to create a utility function that does this since "clicking a button and waiting for a response" as it is a super common pattern. That was the purpose of the old util file, and I'd like to keep to that goal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the ticket

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('[email protected]')).toBeVisible();
Expand All @@ -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('[email protected]')).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"`);
});
});
Loading
Loading