-
Notifications
You must be signed in to change notification settings - Fork 3
feat(oidc-client): add-par-support #631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ryanbas21
wants to merge
2
commits into
main
Choose a base branch
from
par
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| '@forgerock/sdk-request-middleware': minor | ||
| '@forgerock/sdk-oidc': minor | ||
| '@forgerock/davinci-client': minor | ||
| '@forgerock/oidc-client': minor | ||
| 'am-mock-api': patch | ||
| --- | ||
|
|
||
| Add support for PAR in oidc-client requests for redirect flows |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| <!doctype html> | ||
| <html> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>E2E Test | Ping Identity JavaScript SDK</title> | ||
|
|
||
| <style> | ||
| #logout { | ||
| display: none; | ||
| } | ||
| #user-info-btn { | ||
| display: none; | ||
| } | ||
| fieldset { | ||
| display: inline-flex; | ||
| flex-direction: column; | ||
| gap: 0.4rem; | ||
| margin-bottom: 1rem; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div id="app"> | ||
| <a href="/">Home</a> | ||
| <h1>OIDC App | PAR Login (Pushed Authorization Request)</h1> | ||
| <p> | ||
| Client: <code>ParClient</code> — PAR enabled. Authorize params are sent via | ||
| back-channel POST to <code>/par</code> first, then a slim URL (<code | ||
| >client_id + request_uri</code | ||
| > | ||
| only) is used for the authorize redirect. | ||
| </p> | ||
|
|
||
| <h2>Step 1: Establish AM Session (Journey: Login)</h2> | ||
| <p> | ||
| Background PAR auth requires an existing AM session. Log in via the Login journey first. | ||
| </p> | ||
| <form id="journey-form"> | ||
| <fieldset> | ||
| <label for="username">User Name</label> | ||
| <input id="username" type="text" autocomplete="username" /> | ||
| <label for="password">Password</label> | ||
| <input id="password" type="password" autocomplete="current-password" /> | ||
| <button type="submit">Login (Journey)</button> | ||
| </fieldset> | ||
| </form> | ||
| <p id="journey-status"></p> | ||
|
|
||
| <h2>Step 2: PAR OAuth</h2> | ||
| <button id="login-background" disabled>Login (Background — PAR + iframe)</button> | ||
| <button id="login-redirect">Login (Redirect — PAR slim URL)</button> | ||
| <button id="get-tokens">Get Tokens (Local)</button> | ||
| <button id="get-tokens-background">Get Tokens (Background)</button> | ||
| <button id="renew-tokens">Renew Tokens</button> | ||
| <button id="logout">Logout</button> | ||
| <button id="user-info-btn">User Info</button> | ||
| <button id="revoke">Revoke Token</button> | ||
| <a href="/par/">Start Over</a> | ||
| </div> | ||
| <script type="module" src="./main.ts"></script> | ||
| </body> | ||
| </html> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| /* | ||
| * | ||
| * Copyright © 2025 Ping Identity Corporation. All right reserved. | ||
| * | ||
| * This software may be modified and distributed under the terms | ||
| * of the MIT license. See the LICENSE file for details. | ||
| * | ||
| */ | ||
| import { oidcApp } from '../utils/oidc-app.js'; | ||
|
|
||
| const AM_BASE = 'https://openam-sdks.forgeblocks.com/am'; | ||
| const REALM = 'alpha'; | ||
|
|
||
| const urlParams = new URLSearchParams(window.location.search); | ||
| const wellknown = urlParams.get('wellknown'); | ||
|
|
||
| const config = { | ||
| clientId: 'ParClient', | ||
| redirectUri: 'http://localhost:8443/par/', | ||
| scope: 'openid profile email', | ||
| par: true, | ||
| serverConfig: { | ||
| wellknown: | ||
| wellknown || | ||
| 'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration', | ||
| }, | ||
| }; | ||
|
|
||
| // Run journey Login to establish an AM session before background PAR auth | ||
| async function runLoginJourney(username: string, password: string): Promise<void> { | ||
| const authenticateUrl = `${AM_BASE}/json/realms/root/realms/${REALM}/authenticate?authIndexType=service&authIndexValue=Login`; | ||
|
|
||
| // Step 1: start the journey | ||
| const initRes = await fetch(authenticateUrl, { | ||
| method: 'POST', | ||
| credentials: 'include', | ||
| headers: { 'Content-Type': 'application/json', 'Accept-API-Version': 'resource=2.1' }, | ||
| body: '{}', | ||
| }); | ||
| const initJson = await initRes.json(); | ||
|
|
||
| if (initJson.successUrl) return; // already authenticated | ||
|
|
||
| // Fill NameCallback + PasswordCallback | ||
| for (const cb of initJson.callbacks ?? []) { | ||
| if (cb.type === 'NameCallback') cb.input[0].value = username; | ||
| if (cb.type === 'PasswordCallback') cb.input[0].value = password; | ||
| } | ||
|
|
||
| // Step 2: submit credentials | ||
| const submitRes = await fetch(authenticateUrl, { | ||
| method: 'POST', | ||
| credentials: 'include', | ||
| headers: { 'Content-Type': 'application/json', 'Accept-API-Version': 'resource=2.1' }, | ||
| body: JSON.stringify(initJson), | ||
| }); | ||
| const submitJson = await submitRes.json(); | ||
|
|
||
| if (!submitJson.tokenId && !submitJson.successUrl) { | ||
| throw new Error(submitJson.message || 'Login failed'); | ||
| } | ||
| } | ||
|
|
||
| const journeyForm = document.getElementById('journey-form') as HTMLFormElement; | ||
| const journeyStatus = document.getElementById('journey-status') as HTMLParagraphElement; | ||
| const backgroundBtn = document.getElementById('login-background') as HTMLButtonElement; | ||
|
|
||
| journeyForm.addEventListener('submit', async (e) => { | ||
| e.preventDefault(); | ||
| const username = (document.getElementById('username') as HTMLInputElement).value; | ||
| const password = (document.getElementById('password') as HTMLInputElement).value; | ||
| journeyStatus.textContent = 'Logging in…'; | ||
| try { | ||
| await runLoginJourney(username, password); | ||
| journeyStatus.textContent = '✓ Session established — background login now available.'; | ||
| backgroundBtn.disabled = false; | ||
| } catch (err) { | ||
| journeyStatus.textContent = `✗ ${err instanceof Error ? err.message : 'Login failed'}`; | ||
| } | ||
| }); | ||
|
|
||
| oidcApp({ config, urlParams }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| /* | ||
| * | ||
| * Copyright © 2025 Ping Identity Corporation. All right reserved. | ||
| * | ||
| * This software may be modified and distributed under the terms | ||
| * of the MIT license. See the LICENSE file for details. | ||
| * | ||
| */ | ||
| import { test, expect } from '@playwright/test'; | ||
| import { pingAmUsername, pingAmPassword } from './utils/demo-users.js'; | ||
| import { asyncEvents } from './utils/async-events.js'; | ||
|
|
||
| async function loginJourney(page, username: string, password: string) { | ||
| await page.getByLabel('User Name').fill(username); | ||
| await page.getByLabel('Password').fill(password); | ||
| await page.getByRole('button', { name: 'Login (Journey)' }).click(); | ||
| await expect(page.locator('#journey-status')).toContainText('Session established'); | ||
| } | ||
|
|
||
| // Synthetic PAR error endpoint — intercepted by Playwright before any real network call | ||
| const SYNTHETIC_PAR_ERROR_URL = 'http://localhost:8443/synthetic-par-error-endpoint'; | ||
| // The real wellknown used by the PAR app (intercepted to inject the synthetic PAR endpoint) | ||
| const DEFAULT_WELLKNOWN_PATTERN = | ||
| '**/openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration*'; | ||
|
|
||
| test.describe('PAR (Pushed Authorization Request) login tests', () => { | ||
| test('PAR authorize returns 400 error — SDK surfaces error to the UI without redirecting', async ({ | ||
| page, | ||
| }) => { | ||
| const { navigate } = asyncEvents(page); | ||
|
|
||
| // Intercept the wellknown to inject our synthetic PAR endpoint URL | ||
| await page.route(DEFAULT_WELLKNOWN_PATTERN, async (route) => { | ||
| const response = await route.fetch(); | ||
| const json = await response.json(); | ||
| await route.fulfill({ | ||
| status: 200, | ||
| contentType: 'application/json', | ||
| body: JSON.stringify({ | ||
| ...json, | ||
| pushed_authorization_request_endpoint: SYNTHETIC_PAR_ERROR_URL, | ||
| }), | ||
| }); | ||
| }); | ||
|
|
||
| // Intercept the synthetic PAR endpoint and return a 400 error | ||
| await page.route(SYNTHETIC_PAR_ERROR_URL, (route) => { | ||
| route.fulfill({ | ||
| status: 400, | ||
| contentType: 'application/json', | ||
| body: JSON.stringify({ | ||
| error: 'invalid_request', | ||
| error_description: 'Missing required PAR parameter', | ||
| }), | ||
| }); | ||
| }); | ||
|
|
||
| await navigate('/par/'); | ||
|
|
||
| // Clicking redirect login triggers PAR → receives 400 → SDK should surface an error | ||
| await page.getByRole('button', { name: /^Login \(Redirect\b/ }).click(); | ||
|
|
||
| // The SDK should surface an error in the UI instead of redirecting away | ||
| await expect(page.locator('.error')).toBeVisible({ timeout: 10000 }); | ||
| await expect(page.locator('.error')).toContainText('PAR_ERROR'); | ||
| }); | ||
|
|
||
| test('background login with PAR enabled (ParClient) obtains access token', async ({ page }) => { | ||
| const { navigate } = asyncEvents(page); | ||
|
|
||
| const parRequests: string[] = []; | ||
| page.on('request', (request) => { | ||
| if (request.method() === 'POST' && request.url().includes('/par')) { | ||
| parRequests.push(request.url()); | ||
| } | ||
| }); | ||
|
|
||
| await navigate('/par/'); | ||
|
|
||
| // Establish AM session via the Login journey before attempting background PAR auth | ||
| await loginJourney(page, pingAmUsername, pingAmPassword); | ||
|
|
||
| // Background button is now enabled — click and wait for the iframe to return a code | ||
| await page.getByRole('button', { name: /^Login \(Background\b/ }).click(); | ||
| await expect(page.locator('#accessToken-0')).not.toBeEmpty(); | ||
|
|
||
| // PAR POST was made for the background request | ||
| expect(parRequests.length).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| test('redirect login with PAR enabled (ParClient) obtains access token and uses slim authorize URL', async ({ | ||
| page, | ||
| }) => { | ||
| const { clickWithRedirect, navigate } = asyncEvents(page); | ||
|
|
||
| const parRequests: string[] = []; | ||
| const parAuthorizeUrls: string[] = []; | ||
|
|
||
| page.on('request', (request) => { | ||
| if (request.method() === 'POST' && request.url().includes('/par')) { | ||
| parRequests.push(request.url()); | ||
| } | ||
| // Capture the slim PAR authorize redirect — has request_uri, not scope | ||
| if (request.url().includes('/authorize') && request.url().includes('request_uri=')) { | ||
| parAuthorizeUrls.push(request.url()); | ||
| } | ||
| }); | ||
|
|
||
| await navigate('/par/'); | ||
|
|
||
| await clickWithRedirect(/^Login \(Redirect\b/, '**/am/XUI/**'); | ||
|
|
||
| await page.getByLabel('User Name').fill(pingAmUsername); | ||
| await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); | ||
| await clickWithRedirect('Next', 'http://localhost:8443/par/**'); | ||
|
|
||
| expect(page.url()).toContain('code'); | ||
| expect(page.url()).toContain('state'); | ||
|
|
||
| await expect(page.locator('#accessToken-0')).not.toBeEmpty(); | ||
|
|
||
| // PAR POST was made | ||
| expect(parRequests.length).toBeGreaterThan(0); | ||
|
|
||
| // Slim authorize URL contains only client_id + request_uri (not scope/code_challenge) | ||
| expect(parAuthorizeUrls.length).toBeGreaterThan(0); | ||
| const authorizeUrl = new URL(parAuthorizeUrls[0]); | ||
| expect(authorizeUrl.searchParams.has('client_id')).toBe(true); | ||
| expect(authorizeUrl.searchParams.has('request_uri')).toBe(true); | ||
| expect(authorizeUrl.searchParams.has('scope')).toBe(false); | ||
| expect(authorizeUrl.searchParams.has('code_challenge')).toBe(false); | ||
| expect(authorizeUrl.searchParams.has('redirect_uri')).toBe(false); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.