Skip to content

Commit e192523

Browse files
authored
Merge pull request #348 from ForgeRock/token-exchange
feat(oidc-client): implement token exchange
2 parents 26c6983 + beb349a commit e192523

22 files changed

Lines changed: 503 additions & 105 deletions

.changeset/calm-waves-change.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@forgerock/iframe-manager': minor
3+
'@forgerock/storage': minor
4+
'@forgerock/sdk-oidc': minor
5+
'@forgerock/davinci-client': minor
6+
'@forgerock/oidc-client': minor
7+
---
8+
9+
Implemented token exchange within OIDC Client

e2e/oidc-app/src/main.ts

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,72 @@
11
import { oidc } from '@forgerock/oidc-client';
22

3-
async function app() {
4-
const oidcClient = await oidc({
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-
},
3+
// const pingAmConfig = {
4+
// config: {
5+
// clientId: 'WebOAuthClient',
6+
// redirectUri: 'http://localhost:8443/',
7+
// scope: 'openid',
8+
// serverConfig: {
9+
// wellknown:
10+
// 'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration',
11+
// },
12+
// },
13+
// };
14+
const pingOneConfig = {
15+
config: {
16+
clientId: '654b14e2-7cc5-4977-8104-c4113e43c537',
17+
redirectUri: 'http://localhost:8443/',
18+
scope: 'openid',
19+
serverConfig: {
20+
wellknown:
21+
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
1322
},
14-
});
23+
},
24+
};
25+
26+
async function app() {
27+
const oidcClient = await oidc(pingOneConfig);
1528

1629
// create object from URL query parameters
1730
const urlParams = new URLSearchParams(window.location.search);
1831
const code = urlParams.get('code');
19-
// const state = urlParams.get('state');
32+
const state = urlParams.get('state');
2033
// get error and error_description if they exist
2134
const error = urlParams.get('error');
2235
// const errorDescription = urlParams.get('error_description');
2336

37+
// Handle background authorization flow
2438
if (!code && !error) {
2539
const response = await oidcClient.authorize.background();
2640

2741
if ('error' in response) {
2842
console.error('Authorization Error:', response);
29-
// window.location.assign(response.redirectUrl);
43+
44+
if (response.redirectUrl) {
45+
window.location.assign(response.redirectUrl);
46+
} else {
47+
console.log('Authorization failed with no ability to redirect:', response);
48+
}
3049
return;
50+
51+
// Handle success response from background authorization
3152
} else if ('code' in response) {
3253
console.log('Authorization Code:', response.code);
54+
const tokenResponse = await oidcClient.token.exchange(response.code, response.state);
55+
if ('error' in response) {
56+
console.error('Token Exchange Error:', tokenResponse);
57+
} else {
58+
console.log('Token Exchange Response:', tokenResponse);
59+
}
60+
}
61+
62+
// Handle the user redirecting after authentication
63+
} else if (code && state) {
64+
const response = await oidcClient.token.exchange(code, state);
65+
66+
if ('error' in response) {
67+
console.error('Token Exchange Error:', response);
68+
} else {
69+
console.log('Token Exchange Response:', response);
3370
}
3471
}
3572
}

packages/davinci-client/src/lib/client.store.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
6666
}) {
6767
const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom });
6868
const store = createClientStore({ requestMiddleware, logger: log });
69-
const serverInfo = createStorage<ContinueNode['server']>(
70-
{ storeType: 'localStorage' },
71-
'serverInfo',
72-
);
69+
const serverInfo = createStorage<ContinueNode['server']>({
70+
type: 'localStorage',
71+
name: 'serverInfo',
72+
});
7373
if (!config.serverConfig.wellknown) {
7474
const error = new Error(
7575
'`wellknown` property is a required as part of the `config.serverConfig`',

packages/oidc-client/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A generic OpenID Connect (OIDC) client library for JavaScript and TypeScript, de
44

55
```js
66
// Initialize OIDC Client
7-
const oidcClient = oidc({
7+
const oidcClient1 = oidc({
88
/* config */
99
});
1010

packages/oidc-client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@forgerock/sdk-oidc": "workspace:*",
3232
"@forgerock/sdk-request-middleware": "workspace:*",
3333
"@forgerock/sdk-types": "workspace:*",
34+
"@forgerock/storage": "workspace:*",
3435
"@reduxjs/toolkit": "catalog:",
3536
"effect": "^3.12.7"
3637
},

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

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,37 @@ export async function authorizeµ(
3939
* set iframe's to DENY.
4040
*/
4141
return authorizeFetchµ(url).pipe(
42-
Micro.flatMap((response) => {
43-
if ('authorizeResponse' in response) {
44-
log.debug('Received authorize response', response.authorizeResponse);
45-
return Micro.succeed(response.authorizeResponse);
46-
}
47-
log.error('Error in authorize response', response);
48-
return Micro.fail(createAuthorizeErrorµ(response, wellknown, config, options));
49-
}),
42+
Micro.flatMap(
43+
(response): Micro.Micro<AuthorizeSuccessResponse, AuthorizeErrorResponse, never> => {
44+
if ('authorizeResponse' in response) {
45+
log.debug('Received authorize response', response.authorizeResponse);
46+
return Micro.succeed(response.authorizeResponse);
47+
}
48+
log.error('Error in authorize response', response);
49+
// For redirection, we need to remore `pi.flow` from the options
50+
const redirectOptions = options;
51+
delete redirectOptions.responseMode;
52+
return createAuthorizeErrorµ(response, wellknown, config, options);
53+
},
54+
),
5055
);
5156
} else {
5257
/**
5358
* If the response mode is not pi.flow, then we are likely using a traditional
5459
* redirect based server supporting iframes. An example would be PingAM.
5560
*/
5661
return authorizeIframeµ(url, config).pipe(
57-
Micro.flatMap((response) => {
58-
if ('code' in response && 'state' in response) {
59-
log.debug('Received authorization code', response);
60-
return Micro.succeed(response as unknown as AuthorizeSuccessResponse);
61-
}
62-
log.error('Error in authorize response', response);
63-
const errorResponse = response as unknown as AuthorizeErrorResponse;
64-
return Micro.fail(createAuthorizeErrorµ(errorResponse, wellknown, config, options));
65-
}),
62+
Micro.flatMap(
63+
(response): Micro.Micro<AuthorizeSuccessResponse, AuthorizeErrorResponse, never> => {
64+
if ('code' in response && 'state' in response) {
65+
log.debug('Received authorization code', response);
66+
return Micro.succeed(response as unknown as AuthorizeSuccessResponse);
67+
}
68+
log.error('Error in authorize response', response);
69+
const errorResponse = response as unknown as AuthorizeErrorResponse;
70+
return createAuthorizeErrorµ(errorResponse, wellknown, config, options);
71+
},
72+
),
6673
);
6774
}
6875
}),

packages/oidc-client/src/lib/authorize.request.types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ export interface AuthorizeSuccessResponse {
1313
export interface AuthorizeErrorResponse {
1414
error: string;
1515
error_description: string;
16-
redirectUrl: string; // URL to redirect the user to for re-authorization
17-
type: 'auth_error';
16+
redirectUrl?: string; // URL to redirect the user to for re-authorization
17+
type: 'auth_error' | 'argument_error' | 'wellknown_error';
1818
}

packages/oidc-client/src/lib/authorize.request.utils.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ export function authorizeFetchµ(url: string) {
4545

4646
export function authorizeIframeµ(url: string, config: OidcConfig) {
4747
return Micro.tryPromise({
48-
try: () =>
49-
iFrameManager().getParamsByRedirect({
48+
try: () => {
49+
const params = iFrameManager().getParamsByRedirect({
5050
url,
5151
/***
5252
* https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
@@ -55,15 +55,17 @@ export function authorizeIframeµ(url: string, config: OidcConfig) {
5555
successParams: ['code', 'state'],
5656
errorParams: ['error', 'error_description'],
5757
timeout: config.serverConfig.timeout || 3000,
58-
}),
58+
});
59+
return params;
60+
},
5961
catch: (err) => {
60-
let message = 'Error fetching authorization URL';
62+
let message = 'Error calling authorization URL';
6163
if (err instanceof Error) {
6264
message = err.message;
6365
}
6466

6567
return {
66-
error: 'Authorization Notwork Failure',
68+
error: 'Authorization Network Failure',
6769
error_description: message,
6870
type: 'auth_error',
6971
} as AuthorizeErrorResponse;
@@ -102,18 +104,10 @@ export function createAuthorizeErrorµ(
102104
options: GetAuthorizationUrlOptions,
103105
) {
104106
return Micro.tryPromise({
105-
try: async () => {
106-
const url = await createAuthorizeUrl(wellknown.authorization_endpoint, {
107+
try: () =>
108+
createAuthorizeUrl(wellknown.authorization_endpoint, {
107109
...options,
108-
prompt: 'none',
109-
});
110-
return {
111-
error: 'AuthorizationUrlError',
112-
error_description: `Error creating authorization URL for ${url}`,
113-
type: 'auth_error',
114-
redirectUrl: url,
115-
} as AuthorizeErrorResponse;
116-
},
110+
}),
117111
catch: (error) => {
118112
let message = 'Error creating authorization URL';
119113
if (error instanceof Error) {
@@ -125,7 +119,16 @@ export function createAuthorizeErrorµ(
125119
type: 'auth_error',
126120
} as AuthorizeErrorResponse;
127121
},
128-
});
122+
}).pipe(
123+
Micro.flatMap((url) => {
124+
return Micro.fail({
125+
error: res.error,
126+
error_description: res.error_description,
127+
type: 'auth_error',
128+
redirectUrl: url,
129+
} as AuthorizeErrorResponse);
130+
}),
131+
);
129132
}
130133

131134
export function createAuthorizeUrlµ(
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 };

0 commit comments

Comments
 (0)