Skip to content

Commit 297bf2a

Browse files
committed
feat(oidc-client): implement stronger patterns
1 parent dc4d4bd commit 297bf2a

23 files changed

Lines changed: 375 additions & 436 deletions

e2e/oidc-app/src/main.ts

Lines changed: 26 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,36 @@
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: 'WebOAuthClient',
7+
redirectUri: 'http://localhost:8443/',
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-
});
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);
16+
// create object from URL query parameters
17+
const urlParams = new URLSearchParams(window.location.search);
18+
const code = urlParams.get('code');
19+
// const state = urlParams.get('state');
20+
// get error and error_description if they exist
21+
const error = urlParams.get('error');
22+
// const errorDescription = urlParams.get('error_description');
23+
24+
if (!code && !error) {
25+
const response = await oidcClient.authorize.background();
26+
27+
if ('error' in response) {
28+
console.error('Authorization Error:', response);
29+
window.location.assign(response.redirectUrl);
30+
return;
31+
} else if ('code' in response) {
32+
console.log('Authorization Code:', response.code);
33+
}
6134
}
6235
}
6336

packages/oidc-client/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
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({
8+
/* config */
9+
});
10+
11+
// Authorize API
12+
const authResponse = oidcClient.authorize.background(); // Returns code and state if successful, error and Auth URL if not
13+
const authUrl = oidcClient.authorize.url(); // Returns Auth URL or error
14+
15+
// Tokens API
16+
const newTokens = oidcClient.tokens.exchange({
17+
/* code, state */
18+
}); // Returns new tokens or error
19+
const existingTokens = oidcClient.tokens.get(); // Returns existing tokens or error
20+
const revokeResponse = oidcClient.tokens.revoke(); // Returns null or error
21+
const endSessionResponse = oidcClient.tokens.endSession(); // Returns null or error
22+
23+
// User API
24+
const user = oidcClient.user.info(); // Returns user object or error
25+
const logoutResponse = oidcClient.user.logout(); // Returns null or error
26+
```

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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
18+
let response: Record<string, unknown>;
19+
20+
try {
21+
/**
22+
* If we support the pi.flow field, this means we are using a PingOne server.
23+
* PingOne servers do not support redirection through iframes because they
24+
* set iframe's to DENY.
25+
*/
26+
if (wellknown.response_modes_supported?.includes('pi.flow')) {
27+
/**
28+
* We need to make a post (or a get) request and both are supported by
29+
* PingOne.
30+
*/
31+
const authorizeUrl = await createAuthorizeUrl(authorizePath, {
32+
...optionsWithDefaults,
33+
prompt: 'none',
34+
responseMode: 'pi.flow',
35+
});
36+
const res = await fetch(authorizeUrl, {
37+
method: 'POST',
38+
credentials: 'include',
39+
});
40+
41+
response = await res.json();
42+
} else {
43+
const authorizeUrl = await createAuthorizeUrl(authorizePath, {
44+
...optionsWithDefaults,
45+
prompt: 'none',
46+
});
47+
response = await iFrameManager().getParamsByRedirect({
48+
url: authorizeUrl,
49+
/***
50+
* https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
51+
* The client MUST ignore unrecognized response parameters.
52+
*/
53+
successParams: ['code', 'state'],
54+
errorParams: ['error', 'error_description'],
55+
timeout: config.serverConfig.timeout || 3000,
56+
});
57+
}
58+
59+
// Normalize response, for both success and failure, to handle both
60+
// fetch and iframe
61+
return await handleResponse(response, authorizePath, optionsWithDefaults);
62+
} catch (error) {
63+
// If an error occurs, we return an error response with the authorize URL
64+
// so the application can handle the redirect.
65+
return handleError(error, authorizePath, optionsWithDefaults);
66+
}
67+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
type: 'auth_error';
12+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
type OptionalAuthorizeOptions = Partial<GetAuthorizationUrlOptions>;
7+
8+
export function createAuthorizeOptions(
9+
config: OidcConfig,
10+
options?: OptionalAuthorizeOptions,
11+
): GetAuthorizationUrlOptions {
12+
return {
13+
clientId: config.clientId,
14+
redirectUri: config.redirectUri,
15+
scope: config.scope || 'openid',
16+
responseType: config.responseType || 'code',
17+
...options,
18+
};
19+
}
20+
21+
export async function handleResponse(
22+
response: Record<string, unknown>,
23+
authorizePath: string,
24+
options: GetAuthorizationUrlOptions,
25+
): Promise<AuthorizeSuccessResponse | AuthorizeErrorResponse> {
26+
// Test if response is from a fetch to PingOne
27+
if ('authorizeResponse' in response) {
28+
const authorizeResponse = response.authorizeResponse as AuthorizeSuccessResponse;
29+
return authorizeResponse;
30+
}
31+
// Test if response is from an iframe
32+
if ('code' in response && 'state' in response) {
33+
const authorizeResponse = response as unknown as AuthorizeSuccessResponse;
34+
return authorizeResponse;
35+
}
36+
37+
/**
38+
* If we reach here, it means the response is missing code or state.
39+
* Let's create a new authorize URL with `prompt=login` to redirect the user to the
40+
* authorization endpoint provide it to the application so it can handle the redirect.
41+
*/
42+
const newAuthorizeUrl = await createAuthorizeUrl(authorizePath, options);
43+
44+
if ('error' in response && 'error_description' in response) {
45+
const errorResponse = {
46+
error: response.error as string,
47+
error_description: response.error_description as string,
48+
type: 'auth_error',
49+
redirectUrl: newAuthorizeUrl,
50+
} as AuthorizeErrorResponse;
51+
52+
return errorResponse;
53+
} else {
54+
return {
55+
error_description: 'Unknown authorization error',
56+
redirectUrl: newAuthorizeUrl,
57+
type: 'auth_error',
58+
} as AuthorizeErrorResponse;
59+
}
60+
}
61+
62+
export async function handleError(
63+
error: unknown,
64+
authorizePath: string,
65+
options: GetAuthorizationUrlOptions,
66+
): Promise<AuthorizeErrorResponse> {
67+
const message = error instanceof Error ? error.message : '';
68+
/**
69+
* Let's create a new authorize URL with `prompt=login` to redirect the user to the
70+
* authorization endpoint provide it to the application so it can handle the redirect.
71+
*/
72+
const newAuthorizeUrl = await createAuthorizeUrl(authorizePath, options);
73+
74+
return {
75+
error: 'network_error',
76+
error_description: message || 'An error occurred while fetching the authorization URL',
77+
redirectUrl: newAuthorizeUrl,
78+
type: 'auth_error',
79+
};
80+
}

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

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

0 commit comments

Comments
 (0)