Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
8d6743b
feat(oidc-client): add-par-support
ryanbas21 May 11, 2026
bdd1d93
fix(oidc-client): implicit PAR opt-in when server requires PAR and co…
ryanbas21 May 12, 2026
53ae2b6
fix: separate gh stderr from JSON, use process substitution for while…
ryanbas21 May 12, 2026
43deea9
fix(oidc-client): inject prompt=none into background PAR authorize
ryanbas21 May 12, 2026
0125b23
feat: add clean rebase and force-with-lease push for conflict-free PRs
ryanbas21 May 12, 2026
fafcc34
fix(oidc-client): replace unsafe AuthorizationError casts with toAuth…
ryanbas21 May 12, 2026
2ab1c45
fix: guard against empty CONFLICTED_FILES when rebase fails without m…
ryanbas21 May 12, 2026
fa8ea84
fix(oidc-client): map AuthorizationError to GenericError in authorize…
ryanbas21 May 12, 2026
7702294
fix: guard git checkout to prevent set -e from killing loop on dirty …
ryanbas21 May 12, 2026
5526f30
test(oidc-client): add test for PAR + non-pi.flow (iframe) server path
ryanbas21 May 12, 2026
36e316e
chore: remove personal rebase script from repo (moved to ~/.local/bin)
ryanbas21 May 12, 2026
2ea8bbc
feat(e2e): add PAR test app for oidc-client e2e coverage
ryanbas21 May 13, 2026
de05bca
fix(e2e): filter PAR authorize URL by request_uri to avoid SSO redire…
ryanbas21 May 13, 2026
a3f968a
fix(oidc-client): strip prompt from PAR POST body, append to slim aut…
ryanbas21 May 13, 2026
5758a12
chore(e2e): clarify PAR page heading/buttons and add to home page nav
ryanbas21 May 13, 2026
4137d47
fix(oidc-client): send prompt in both PAR body and slim authorize URL
ryanbas21 May 13, 2026
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
9 changes: 9 additions & 0 deletions .changeset/some-shirts-joke.md
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
1 change: 1 addition & 0 deletions e2e/am-mock-api/src/app/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

export const authPaths = {
par: ['/am/oauth2/realms/root/par'],
tokenExchange: [
'/am/auth/tokenExchange',
'/am/oauth2/realms/root/access_token',
Expand Down
5 changes: 5 additions & 0 deletions e2e/am-mock-api/src/app/responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,11 @@ export const recaptchaEnterpriseCallback = {
],
};

export const parResponse = {
request_uri: 'urn:ietf:params:oauth:request_uri:mock-par-request-uri',
expires_in: 60,
};

export const qrCodeCallbacksResponse = {
authId: 'qrcode-journey-confirmation',
callbacks: [
Expand Down
5 changes: 5 additions & 0 deletions e2e/am-mock-api/src/app/routes.auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
MetadataMarketPlacePingOneEvaluation,
newPiWellKnown,
qrCodeCallbacksResponse,
parResponse,
} from './responses.js';
import initialRegResponse from './response.registration.js';
import {
Expand Down Expand Up @@ -664,6 +665,10 @@ export default function (app) {

app.get('/callback', (req, res) => res.status(200).send('ok'));

app.post(authPaths.par, (req, res) => {
res.status(201).json(parResponse);
});

app.get('/am/.well-known/oidc-configuration', (req, res) => {
res.send(wellKnownForgeRock);
});
Expand Down
1 change: 1 addition & 0 deletions e2e/oidc-app/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ <h2>OIDC Client E2E Test Index | Ping Identity JavaScript SDK</h2>
<div id="nav">
<a href="/ping-am/">Ping AM</a>
<a href="/ping-one/">Ping One</a>
<a href="/par/">PAR (Pushed Authorization Request)</a>
</div>
</div>
<script type="module" src="index.ts"></script>
Expand Down
33 changes: 33 additions & 0 deletions e2e/oidc-app/src/par/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!doctype html>
<html>
<head>
<title>E2E Test | Ping Identity JavaScript SDK</title>

<style>
#logout {
display: none;
}
</style>
</head>
<body>
<div id="app">
<a href="/">Home</a>
<h1>OIDC App | PAR Login (Pushed Authorization Request)</h1>
<p>
Client: <code>ParClient</code> &mdash; PAR enabled. Authorize params POST back-channel to
<code>/par</code> first; the authorize redirect uses only
<code>client_id + request_uri</code>.
</p>
<button id="login-background">Login (Background &mdash; PAR + iframe)</button>
<button id="login-redirect">Login (Redirect &mdash; 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>
27 changes: 27 additions & 0 deletions e2e/oidc-app/src/par/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
*
* 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 urlParams = new URLSearchParams(window.location.search);
const clientId = urlParams.get('clientid');
const wellknown = urlParams.get('wellknown');

const config = {
clientId: 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',
},
};

oidcApp({ config, urlParams });
5 changes: 4 additions & 1 deletion e2e/oidc-app/src/utils/oidc-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ export async function oidcApp({ config, urlParams }) {
const code = urlParams.get('code');
const state = urlParams.get('state');
const piflow = urlParams.get('piflow');
const par = urlParams.get('par') === 'true';

const oidcClient: OidcClient = await oidc({ config });
const oidcClient: OidcClient = await oidc({
config: { ...config, ...(par && { par: true }) },
});
if ('error' in oidcClient) {
displayError(oidcClient);
}
Expand Down
2 changes: 1 addition & 1 deletion e2e/oidc-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const pages = ['ping-am', 'ping-one'];
const pages = ['ping-am', 'ping-one', 'par'];
export default defineConfig(() => ({
root: __dirname + '/src',
cacheDir: '../../node_modules/.vite/e2e/oidc-app',
Expand Down
85 changes: 85 additions & 0 deletions e2e/oidc-suites/src/par.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
*
* 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';

test.describe('PAR (Pushed Authorization Request) login tests', () => {
test('background login with PAR enabled (ParClient) obtains access token', async ({ page }) => {
const { clickWithRedirect, 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/');

await clickWithRedirect('Login (Background)', '**/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 for background request
expect(parRequests.length).toBeGreaterThan(0);
});

test('redirect login with PAR enabled 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 only 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('/ping-am/?par=true');

await clickWithRedirect('Login (Redirect)', '**/am/XUI/**');

await page.getByLabel('User Name').fill(pingAmUsername);
await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword);
await clickWithRedirect('Next', 'http://localhost:8443/ping-am/**');

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);
});
});
18 changes: 18 additions & 0 deletions packages/oidc-client/api-report/oidc-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ oidc: CombinedState< {
authorizeFetch: MutationDefinition< {
url: string;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizeSuccessResponse, "oidc", unknown>;
par: MutationDefinition< {
endpoint: string;
body: URLSearchParams;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, PushAuthorizationResponse, "oidc", unknown>;
authorizeIframe: MutationDefinition< {
url: string;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizationSuccess, "oidc", unknown>;
Expand Down Expand Up @@ -155,6 +159,10 @@ oidc: CombinedState< {
authorizeFetch: MutationDefinition< {
url: string;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizeSuccessResponse, "oidc", unknown>;
par: MutationDefinition< {
endpoint: string;
body: URLSearchParams;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, PushAuthorizationResponse, "oidc", unknown>;
authorizeIframe: MutationDefinition< {
url: string;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizationSuccess, "oidc", unknown>;
Expand Down Expand Up @@ -281,6 +289,8 @@ export interface OidcConfig extends AsyncLegacyConfigOptions {
// (undocumented)
clientId: string;
// (undocumented)
par?: boolean;
// (undocumented)
redirectUri: string;
// (undocumented)
responseType?: ResponseType_2;
Expand All @@ -296,6 +306,14 @@ export interface OidcConfig extends AsyncLegacyConfigOptions {
// @public (undocumented)
export type OptionalAuthorizeOptions = Partial<GetAuthorizationUrlOptions>;

// @public (undocumented)
export interface PushAuthorizationResponse {
// (undocumented)
expires_in: number;
// (undocumented)
request_uri: string;
}

export { RequestMiddleware }

export { ResponseType_2 as ResponseType }
Expand Down
18 changes: 18 additions & 0 deletions packages/oidc-client/api-report/oidc-client.types.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ oidc: CombinedState< {
authorizeFetch: MutationDefinition< {
url: string;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizeSuccessResponse, "oidc", unknown>;
par: MutationDefinition< {
endpoint: string;
body: URLSearchParams;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, PushAuthorizationResponse, "oidc", unknown>;
authorizeIframe: MutationDefinition< {
url: string;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizationSuccess, "oidc", unknown>;
Expand Down Expand Up @@ -155,6 +159,10 @@ oidc: CombinedState< {
authorizeFetch: MutationDefinition< {
url: string;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizeSuccessResponse, "oidc", unknown>;
par: MutationDefinition< {
endpoint: string;
body: URLSearchParams;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, PushAuthorizationResponse, "oidc", unknown>;
authorizeIframe: MutationDefinition< {
url: string;
}, BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError, {}, FetchBaseQueryMeta>, never, AuthorizationSuccess, "oidc", unknown>;
Expand Down Expand Up @@ -281,6 +289,8 @@ export interface OidcConfig extends AsyncLegacyConfigOptions {
// (undocumented)
clientId: string;
// (undocumented)
par?: boolean;
// (undocumented)
redirectUri: string;
// (undocumented)
responseType?: ResponseType_2;
Expand All @@ -296,6 +306,14 @@ export interface OidcConfig extends AsyncLegacyConfigOptions {
// @public (undocumented)
export type OptionalAuthorizeOptions = Partial<GetAuthorizationUrlOptions>;

// @public (undocumented)
export interface PushAuthorizationResponse {
// (undocumented)
expires_in: number;
// (undocumented)
request_uri: string;
}

export { RequestMiddleware }

export { ResponseType_2 as ResponseType }
Expand Down
1 change: 1 addition & 0 deletions packages/oidc-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@forgerock/sdk-oidc": "workspace:*",
"@forgerock/sdk-request-middleware": "workspace:*",
"@forgerock/sdk-types": "workspace:*",
"@forgerock/sdk-utilities": "workspace:*",
"@forgerock/storage": "workspace:*",
"@reduxjs/toolkit": "catalog:",
"effect": "catalog:effect"
Expand Down
Loading
Loading