Skip to content

Commit 0d07220

Browse files
committed
feat: implement-authorize-oidc-client
introduce the oidc-client with the authorize api. authoirize handles the calling of authorize routes based on the well-known config. when a p1 env is detected, it will use a post request otherwise, a background request is done via a hidden iframe
1 parent 5e51d67 commit 0d07220

21 files changed

+556
-17
lines changed

.changeset/dirty-queens-design.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@forgerock/iframe-manager': minor
3+
'@forgerock/sdk-oidc': minor
4+
'@forgerock/oidc-client': minor
5+
---
6+
7+
authorize functionality in oidc-client

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
.npm/_logs
77

88
# Generated code
9+
logs/*
910
tmp/
1011
e2e/**/dist
1112
*/dist/*

CLAUDE.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is the Ping JavaScript SDK - a monorepo containing multiple packages for web applications integrating with the Ping platform. The SDK provides APIs for user authentication, device management, and accessing Ping-secured resources.
8+
9+
## Development Commands
10+
11+
### Core Commands
12+
13+
- `pnpm build` - Build all affected packages
14+
- `pnpm test` - Run tests for all affected packages
15+
- `pnpm lint` - Lint all affected packages
16+
- `pnpm format` - Format code using Prettier
17+
- `pnpm nx typecheck` - Run TypeScript type checking
18+
19+
### Package Management
20+
21+
- `pnpm create-package` - Generate a new library package using Nx
22+
- `pnpm nx serve <package-name>` - Serve a specific package in development
23+
- `pnpm nx test <package-name> --watch` - Run tests for a specific package in watch mode
24+
25+
### E2E Testing
26+
27+
- `pnpm test:e2e` - Run end-to-end tests for affected packages
28+
- Individual e2e apps are in `e2e/` directory with their own test suites
29+
30+
## Architecture
31+
32+
### Monorepo Structure
33+
34+
The repository uses **Nx** as the monorepo tool with the following structure:
35+
36+
```
37+
packages/
38+
├── davinci-client/ # DaVinci flow orchestration client
39+
├── device-client/ # Device management (binding, profiles, WebAuthn)
40+
├── oidc-client/ # OpenID Connect authentication client
41+
├── protect/ # Ping Protect fraud detection
42+
├── sdk-effects/ # Effect-based utilities (logger, storage, etc.)
43+
│ ├── iframe-manager/
44+
│ ├── logger/
45+
│ ├── oidc/
46+
│ ├── sdk-request-middleware/
47+
│ └── storage/
48+
├── sdk-types/ # Shared TypeScript types
49+
└── sdk-utilities/ # Common utilities (PKCE, URL handling)
50+
```
51+
52+
### Key Packages
53+
54+
- **davinci-client**: State management for DaVinci authentication flows using Redux Toolkit
55+
- **oidc-client**: OIDC authentication with token management and storage
56+
- **device-client**: Device binding, profiles, OATH, Push, and WebAuthn capabilities
57+
- **protect**: Fraud detection and risk assessment integration
58+
- **sdk-effects**: Effect-based architecture packages for common functionalities
59+
60+
### Technology Stack
61+
62+
- **Package Manager**: pnpm with workspace configuration
63+
- **Build Tool**: Vite for building and bundling
64+
- **Testing**: Vitest for unit tests, Playwright for e2e tests
65+
- **State Management**: Redux Toolkit (in davinci-client)
66+
- **TypeScript**: Strict typing with composite project configuration
67+
- **Linting**: ESLint with Prettier integration
68+
69+
### Nx Configuration
70+
71+
- Uses target defaults for build, test, lint, and typecheck
72+
- Caching enabled for build and test targets
73+
- Workspace layout configured for packages and e2e apps
74+
- Parallel execution limited to 1 for consistency
75+
76+
### Package Dependencies
77+
78+
Packages use workspace references (`workspace:*`) for internal dependencies and catalog references (`catalog:`) for shared external dependencies like `@reduxjs/toolkit`.
79+
80+
### Testing Strategy
81+
82+
- Unit tests: Vitest with coverage reporting
83+
- E2E tests: Playwright with dedicated test apps in `e2e/` directory
84+
- Mock API server available in `e2e/mock-api-v2/` for testing
85+
86+
## Development Notes
87+
88+
- All packages are built as ES modules with TypeScript declarations
89+
- Use `pnpm nx affected` commands to run tasks only on changed packages
90+
- The repository uses conventional commits and automated releases via changesets
91+
- Individual packages can be tested independently using their specific build/test scripts

e2e/oidc-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"serve": "pnpm nx nxServe"
1010
},
1111
"dependencies": {
12+
"@forgerock/javascript-sdk": "^4.8.2",
1213
"@forgerock/oidc-client": "workspace:*"
1314
},
1415
"nx": {

e2e/oidc-app/src/main.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,64 @@
1-
console.log('Starting OIDC App...');
1+
import { oidc } from '@forgerock/oidc-client';
2+
import {
3+
CallbackType,
4+
Config,
5+
FRAuth,
6+
FRStep,
7+
NameCallback,
8+
PasswordCallback,
9+
} from '@forgerock/javascript-sdk';
10+
11+
async function app() {
12+
await Config.setAsync({
13+
clientId: 'WebOAuthClient',
14+
redirectUri: window.location.origin + '/',
15+
scope: 'openid profile email me.read',
16+
serverConfig: {
17+
wellknown:
18+
'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration',
19+
},
20+
});
21+
22+
const step = await FRAuth.start();
23+
console.log('Step:', step);
24+
25+
if ('callbacks' in step) {
26+
const name = step.getCallbackOfType<NameCallback>(CallbackType.NameCallback);
27+
28+
const password = step.getCallbackOfType<PasswordCallback>(CallbackType.PasswordCallback);
29+
30+
name.setName('devicetestuser');
31+
password.setPassword('password');
32+
}
33+
34+
const success = await FRAuth.next(step as FRStep);
35+
console.log('success:', success);
36+
37+
const oidcClient = await oidc({
38+
serverConfig: {
39+
wellknown:
40+
'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration',
41+
},
42+
});
43+
44+
if (oidcClient.error || !oidcClient.authorizeSilently || !oidcClient.createAuthorizeUrl) {
45+
console.error('Error initializing oidc client:', oidcClient.error);
46+
return;
47+
}
48+
49+
const result = await oidcClient.authorizeSilently({
50+
clientId: 'WebOAuthClient',
51+
redirectUri: window.location.origin + '/',
52+
responseType: 'code',
53+
scope: 'openid',
54+
});
55+
56+
if ('error' in result) {
57+
console.error('Error during authorization:', result.error);
58+
return;
59+
} else {
60+
console.log('returning resolved params,', result);
61+
}
62+
}
63+
64+
app();

packages/oidc-client/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@
2626
"test:watch": "pnpm nx nxTest --watch"
2727
},
2828
"dependencies": {
29+
"@forgerock/iframe-manager": "workspace:*",
30+
"@forgerock/sdk-logger": "workspace:*",
31+
"@forgerock/sdk-oidc": "workspace:*",
32+
"@forgerock/sdk-request-middleware": "workspace:*",
2933
"@forgerock/sdk-types": "workspace:*",
30-
"@forgerock/storage": "workspace:*"
34+
"@forgerock/storage": "workspace:*",
35+
"@reduxjs/toolkit": "catalog:"
3136
},
3237
"nx": {
3338
"tags": ["scope:package"]

packages/oidc-client/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './lib/token-store.js';
2+
export * from './lib/client.store.js';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
2+
3+
const authorizeSlice = createApi({
4+
reducerPath: 'authorizeSlice',
5+
baseQuery: fetchBaseQuery({
6+
credentials: 'include',
7+
prepareHeaders: (headers) => {
8+
headers.set('Content-Type', 'application/json');
9+
headers.set('Accept', 'application/json');
10+
headers.set('x-requested-with', 'ping-sdk');
11+
headers.set('x-requested-platform', 'javascript');
12+
13+
return headers;
14+
},
15+
}),
16+
endpoints: (builder) => ({
17+
handleAuthorize: builder.query<string, string>({
18+
query: (authorizeUrl) => authorizeUrl,
19+
}),
20+
}),
21+
});
22+
23+
export { authorizeSlice };
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { iFrameManager } from '@forgerock/iframe-manager';
2+
import { createAuthorizeUrl, GetAuthorizationUrlOptions } from '@forgerock/sdk-oidc';
3+
import { createOidcStore } from './store.js';
4+
import { fetchWellKnownConfig } from './wellknown.api.js';
5+
import { RequestMiddleware } from '@forgerock/sdk-request-middleware';
6+
import { handleAuthorize, recreateAuthorizeUrl } from './client.store.utils.js';
7+
8+
interface OIDCConfig {
9+
prefix?: string;
10+
serverConfig: {
11+
wellknown: string;
12+
};
13+
middleware?: RequestMiddleware[];
14+
logger?: ReturnType<typeof import('@forgerock/sdk-logger').logger>;
15+
}
16+
17+
interface AuthorizeSuccessResponse {
18+
code: string;
19+
state: string;
20+
redirectUrl?: string; // Optional, used when the response is from a P1 server
21+
}
22+
23+
interface AuthorizeUrlResponse {
24+
authorizeUrl: string;
25+
}
26+
27+
interface AuthorizeErrorResponse {
28+
error: string;
29+
error_description?: string;
30+
state?: string;
31+
}
32+
33+
type AuthorizeResponse = AuthorizeSuccessResponse | AuthorizeUrlResponse | AuthorizeErrorResponse;
34+
35+
export type { AuthorizeSuccessResponse, AuthorizeErrorResponse, AuthorizeResponse, OIDCConfig };
36+
37+
export async function oidc(config: OIDCConfig) {
38+
const store = createOidcStore({ requestMiddleware: config.middleware, logger: config.logger });
39+
const iframeMgr = iFrameManager();
40+
41+
if (!config?.serverConfig?.wellknown) {
42+
return {
43+
error: 'requires a wellknown url initializing this factory.',
44+
};
45+
}
46+
47+
const wellKnownUrl = config.serverConfig.wellknown;
48+
const { data, error } = await store.dispatch(
49+
fetchWellKnownConfig.endpoints.fetchWellKnownConfig.initiate(wellKnownUrl),
50+
);
51+
if (error || !data) {
52+
return {
53+
error: `Error fetching wellknown config`,
54+
};
55+
}
56+
57+
/**
58+
* if true, then we need to use GET or POST (p1?)
59+
* if false, we use iframe.
60+
*/
61+
const supportsPiFlow = data.response_modes_supported?.includes('pi.flow');
62+
63+
return {
64+
createAuthorizeUrl: createAuthorizeUrl,
65+
authorizeSilently: async (
66+
options: GetAuthorizationUrlOptions,
67+
timeout = 3000,
68+
): Promise<AuthorizeResponse | { error: string }> => {
69+
const authorizePath = data.authorization_endpoint;
70+
71+
const authorizeUrl = await createAuthorizeUrl(authorizePath, options);
72+
73+
try {
74+
/**
75+
* If we support the pi flow field,
76+
* this means we are using a p1 server. p1 servers
77+
* do not support redirection through iframes because they
78+
* set iframe's to DENY.
79+
*/
80+
if (supportsPiFlow) {
81+
/**
82+
* We need to make a post (or a get) request
83+
* and both are supported by p1.
84+
*/
85+
return handleAuthorize(authorizeUrl);
86+
}
87+
88+
const resolvedParams = await iframeMgr.getParamsByRedirect({
89+
url: authorizeUrl,
90+
/***
91+
* https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
92+
* The client MUST ignore unrecognized response parameters.
93+
* The authorization code string size is left undefined by this specification.
94+
* The client should avoid making assumptions about code value sizes.
95+
* The authorization server SHOULD document the size of any value it issues.
96+
*/
97+
successParams: ['code', 'state'],
98+
errorParams: ['error', 'error_description'],
99+
timeout,
100+
});
101+
102+
if ('error' in resolvedParams) {
103+
return recreateAuthorizeUrl(resolvedParams, authorizePath, options);
104+
}
105+
106+
const { code, state } = resolvedParams;
107+
108+
return {
109+
code,
110+
state,
111+
};
112+
} catch (iframeError: unknown) {
113+
return {
114+
error: 'iframe_error',
115+
error_description: (iframeError as Error)?.message || 'Authorization iframe failed',
116+
};
117+
}
118+
},
119+
};
120+
}

0 commit comments

Comments
 (0)