Skip to content
Draft
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
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
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
56 changes: 56 additions & 0 deletions e2e/oidc-suites/src/par.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
*
* 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('redirect login with PAR enabled obtains access token and uses slim authorize URL', async ({
page,
}) => {
const { clickWithRedirect, navigate } = asyncEvents(page);

const parRequests: string[] = [];
const authorizeRequests: string[] = [];

page.on('request', (request) => {
if (request.method() === 'POST' && request.url().includes('/par')) {
parRequests.push(request.url());
}
if (request.url().includes('/authorize')) {
authorizeRequests.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);

// Authorize URL only contains client_id + request_uri (not scope/code_challenge)
expect(authorizeRequests.length).toBeGreaterThan(0);
const authorizeUrl = new URL(authorizeRequests[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);
});
});
93 changes: 74 additions & 19 deletions packages/davinci-client/api-report/davinci-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export interface CollectorErrors {
}

// @public (undocumented)
export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector;
export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector;

// @public
export type CollectorValueType<T> = T extends {
Expand Down Expand Up @@ -212,7 +212,7 @@ export type CollectorValueType<T> = T extends {
} ? string[] : string | string[] | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue;

// @public (undocumented)
export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | FidoRegistrationField | FidoAuthenticationField | PollingField;
export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField;

// @public (undocumented)
export interface ContinueNode {
Expand Down Expand Up @@ -267,10 +267,10 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
resume: (input: {
continueToken: string;
}) => Promise<InternalErrorResponse | NodeStates>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode>;
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
poll: (collector: PollingCollector) => Poller;
pollStatus: (collector: PollingCollector) => Poller;
getClient: () => {
status: "start";
} | {
Expand All @@ -285,19 +285,19 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
description?: string;
name?: string;
status: "error";
} | {
status: "failure";
} | {
authorization?: {
code?: string;
state?: string;
};
status: "success";
} | {
status: "failure";
} | null;
getCollectors: () => Collectors[];
getError: () => DaVinciError | null;
getErrorCollectors: () => CollectorErrors[];
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode;
getServer: () => {
_links?: Links;
id?: string;
Expand All @@ -318,20 +318,20 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} | {
_links?: Links;
eventName?: string;
href?: string;
id?: string;
interactionId?: string;
interactionToken?: string;
status: "failure";
href?: string;
session?: string;
status: "success";
} | {
_links?: Links;
eventName?: string;
href?: string;
id?: string;
interactionId?: string;
interactionToken?: string;
href?: string;
session?: string;
status: "success";
status: "failure";
} | null;
cache: {
getLatestResponse: () => ((state: RootState< {
Expand Down Expand Up @@ -1035,7 +1035,7 @@ export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T exten
export type InferSingleValueCollectorType<T extends SingleValueCollectorTypes> = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>;

// @public (undocumented)
export type InferValueObjectCollectorType<T extends ObjectValueCollectorTypes> = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>;
export type InferValueObjectCollectorType<T extends ObjectValueCollectorTypes> = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : T extends 'PhoneNumberExtensionCollector' ? PhoneNumberExtensionCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>;

// @public (undocumented)
export type InitFlow = () => Promise<FlowNode | InternalErrorResponse>;
Expand Down Expand Up @@ -1170,8 +1170,8 @@ value: Record<string, unknown>;
}, string>;

// @public
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | PhoneNumberExtensionCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | PhoneNumberExtensionCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
};

// @public (undocumented)
Expand Down Expand Up @@ -1283,10 +1283,10 @@ export type ObjectValueAutoCollectorTypes = 'ObjectValueAutoCollector' | 'FidoRe
export type ObjectValueCollector<T extends ObjectValueCollectorTypes> = ObjectOptionsCollectorWithObjectValue<T> | ObjectOptionsCollectorWithStringValue<T> | ObjectValueCollectorWithObjectValue<T>;

// @public (undocumented)
export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>;
export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>;

// @public
export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector';
export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'PhoneNumberExtensionCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector';

// @public (undocumented)
export interface ObjectValueCollectorWithObjectValue<T extends ObjectValueCollectorTypes, IV = Record<string, string>, OV = Record<string, string>> {
Expand Down Expand Up @@ -1328,13 +1328,68 @@ export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'>
// @public (undocumented)
export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>;

// @public (undocumented)
export interface PhoneNumberExtensionCollector {
// (undocumented)
category: 'ObjectValueCollector';
// (undocumented)
error: string | null;
// (undocumented)
id: string;
// (undocumented)
input: {
key: string;
value: PhoneNumberExtensionInputValue;
type: string;
validation: (ValidationRequired | ValidationPhoneNumber)[] | null;
};
// (undocumented)
name: string;
// (undocumented)
output: {
key: string;
label: string;
type: string;
extensionLabel: string;
value: PhoneNumberExtensionOutputValue;
};
// (undocumented)
type: 'PhoneNumberExtensionCollector';
}

// @public (undocumented)
export type PhoneNumberExtensionField = PhoneNumberField & {
showExtension: boolean;
extensionLabel: string;
};

// @public (undocumented)
export interface PhoneNumberExtensionInputValue {
// (undocumented)
countryCode: string;
// (undocumented)
extension: string;
// (undocumented)
phoneNumber: string;
}

// @public (undocumented)
export interface PhoneNumberExtensionOutputValue {
// (undocumented)
countryCode?: string;
// (undocumented)
extension?: string;
// (undocumented)
phoneNumber?: string;
}

// @public (undocumented)
export type PhoneNumberField = {
type: 'PHONE_NUMBER';
key: string;
label: string;
defaultCountryCode: string | null;
required: boolean;
defaultCountryCode: string | null;
validatePhoneNumber: boolean;
};

Expand Down Expand Up @@ -1724,7 +1779,7 @@ export type UnknownField = Record<string, unknown>;
// @public (undocumented)
export const updateCollectorValues: ActionCreatorWithPayload< {
id: string;
value: string | string[] | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue;
value: string | string[] | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue;
index?: number;
}, string>;

Expand Down
Loading
Loading