Skip to content

Commit abc572c

Browse files
committed
feat(oidc-client): implement stronger patterns
1 parent 0d07220 commit abc572c

20 files changed

+323
-434
lines changed

e2e/oidc-app/src/main.ts

Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,21 @@
11
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';
102

113
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-
374
const oidcClient = await oidc({
38-
serverConfig: {
39-
wellknown:
40-
'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration',
5+
config: {
6+
clientId: 'client_id',
7+
redirectUri: 'https://example.com/redirect',
8+
scope: 'openid',
9+
serverConfig: {
10+
wellknown:
11+
'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration',
12+
},
4113
},
4214
});
4315

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-
});
16+
const result = await oidcClient.authorize.url();
5517

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-
}
18+
console.log('Authorize URL:', result);
6219
}
6320

6421
app();

packages/oidc-client/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
11
# oidc-client
22

33
A generic OpenID Connect (OIDC) client library for JavaScript and TypeScript, designed to work with any OIDC-compliant identity provider.
4+
5+
```js
6+
// Initialize OIDC Client
7+
const oidcClient = oidc({ /* config */ });
8+
9+
// Authorize API
10+
const authResponse = oidcClient.authorize.background(); // Returns code and state if successful, error and Auth URL if not
11+
const authUrl = oidcClient.authorize.url(); // Returns Auth URL or error
12+
13+
// Tokens API
14+
const newTokens = oidcClient.tokens.exchange({ /* code, state */ }); // Returns new tokens or error
15+
const existingTokens = oidcClient.tokens.get(); // Returns existing tokens or error
16+
const revokeResponse = oidcClient.tokens.revoke(); // Returns null or error
17+
const endSessionResponse = oidcClient.tokens.endSession(); // Returns null or error
18+
19+
// User API
20+
const user = oidcClient.user.info(); // Returns user object or error
21+
const logoutResponse = oidcClient.user.logout(); // Returns null or error
22+
```

packages/oidc-client/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"@forgerock/sdk-oidc": "workspace:*",
3232
"@forgerock/sdk-request-middleware": "workspace:*",
3333
"@forgerock/sdk-types": "workspace:*",
34-
"@forgerock/storage": "workspace:*",
3534
"@reduxjs/toolkit": "catalog:"
3635
},
3736
"nx": {

packages/oidc-client/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export * from './lib/token-store.js';
21
export * from './lib/client.store.js';
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { iFrameManager } from '@forgerock/iframe-manager';
2+
import { createAuthorizeUrl, GetAuthorizationUrlOptions } from '@forgerock/sdk-oidc';
3+
4+
import { createAuthorizeOptions, handleError, handleResponse } from './authorize.request.utils.js';
5+
6+
import type { WellKnownResponse } from '@forgerock/sdk-types';
7+
8+
import type { OidcConfig } from './config.types.js';
9+
10+
export async function authorize(
11+
wellknown: WellKnownResponse,
12+
config: OidcConfig,
13+
options?: GetAuthorizationUrlOptions,
14+
) {
15+
const authorizePath = wellknown.authorization_endpoint;
16+
const optionsWithDefaults = createAuthorizeOptions(config, options);
17+
const authorizeUrl = await createAuthorizeUrl(authorizePath, optionsWithDefaults);
18+
19+
let response: Record<string, unknown>;
20+
21+
try {
22+
/**
23+
* If we support the pi.flow field, this means we are using a PingOne server.
24+
* PingOne servers do not support redirection through iframes because they
25+
* set iframe's to DENY.
26+
*/
27+
if (wellknown.response_modes_supported?.includes('pi.flow')) {
28+
/**
29+
* We need to make a post (or a get) request and both are supported by
30+
* PingOne.
31+
*/
32+
const res = await fetch(authorizeUrl, {
33+
method: 'POST',
34+
credentials: 'include',
35+
});
36+
37+
response = await res.json();
38+
} else {
39+
response = await iFrameManager().getParamsByRedirect({
40+
url: authorizeUrl,
41+
/***
42+
* https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
43+
* The client MUST ignore unrecognized response parameters.
44+
*/
45+
successParams: ['code', 'state'],
46+
errorParams: ['error', 'error_description'],
47+
timeout: config.serverConfig.timeout || 3000,
48+
});
49+
}
50+
51+
// Normalize response, for both success and failure, to handle both
52+
// fetch and iframe
53+
return await handleResponse(response, authorizePath, optionsWithDefaults);
54+
} catch (error) {
55+
// If an error occurs, we return an error response with the authorize URL
56+
// so the application can handle the redirect.
57+
return handleError(error, authorizePath, optionsWithDefaults);
58+
}
59+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface AuthorizeSuccessResponse {
2+
code: string;
3+
state: string;
4+
redirectUrl?: string; // Optional, used when the response is from a P1 server
5+
}
6+
7+
export interface AuthorizeErrorResponse {
8+
error: string;
9+
error_description: string;
10+
redirectUrl: string; // URL to redirect the user to for re-authorization
11+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { createAuthorizeUrl, GetAuthorizationUrlOptions } from '@forgerock/sdk-oidc';
2+
3+
import { AuthorizeErrorResponse, AuthorizeSuccessResponse } from './authorize.request.types.js';
4+
import { OidcConfig } from './config.types.js';
5+
6+
export function createAuthorizeOptions(
7+
config: OidcConfig,
8+
options?: GetAuthorizationUrlOptions,
9+
): GetAuthorizationUrlOptions {
10+
return {
11+
clientId: config.clientId,
12+
redirectUri: config.redirectUri,
13+
scope: config.scope || 'openid',
14+
responseType: config.responseType || 'code',
15+
...options,
16+
};
17+
}
18+
19+
export async function handleResponse(
20+
response: Record<string, unknown>,
21+
authorizePath: string,
22+
options: GetAuthorizationUrlOptions,
23+
) {
24+
// Test if response is from a fetch to PingOne
25+
if ('authorizeResponse' in response) {
26+
const authorizeResponse = response.authorizeResponse as AuthorizeSuccessResponse;
27+
return authorizeResponse;
28+
}
29+
// Test if response is from an iframe
30+
if ('code' in response && 'state' in response) {
31+
const authorizeResponse = response as unknown as AuthorizeSuccessResponse;
32+
return authorizeResponse;
33+
}
34+
35+
/**
36+
* If we reach here, it means the response is missing code or state.
37+
* Let's create a new authorize URL to redirect the user to the authorization endpoint
38+
* provide it to the application so it can handle the redirect.
39+
*/
40+
const newAuthorizeUrl = await createAuthorizeUrl(authorizePath, options);
41+
return {
42+
error: 'invalid_response',
43+
error_description: 'Missing code or state in authorization response after redirect',
44+
redirectUrl: newAuthorizeUrl,
45+
} as AuthorizeErrorResponse;
46+
}
47+
48+
export async function handleError(
49+
error: unknown,
50+
authorizePath: string,
51+
options: GetAuthorizationUrlOptions,
52+
): Promise<AuthorizeErrorResponse> {
53+
const message = error instanceof Error ? error.message : String(error);
54+
const newAuthorizeUrl = await createAuthorizeUrl(authorizePath, options);
55+
56+
return {
57+
error: 'network_error',
58+
error_description: message || 'An error occurred while fetching the authorization URL',
59+
redirectUrl: newAuthorizeUrl,
60+
};
61+
}

packages/oidc-client/src/lib/authorize.slice.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)