Skip to content

Commit add5a17

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

25 files changed

Lines changed: 778 additions & 111 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/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@
88
<meta name="viewport" content="width=device-width, initial-scale=1" />
99
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
1010
<link rel="stylesheet" href="/src/styles.css" />
11+
12+
<style>
13+
#logout {
14+
display: none;
15+
}
16+
</style>
1117
</head>
1218
<body>
1319
<h1>Welcome</h1>
20+
<button id="login">Login</button>
21+
<button id="logout">Logout</button>
1422
<script type="module" src="/src/main.ts"></script>
1523
</body>
1624
</html>

e2e/oidc-app/src/main.ts

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,83 @@
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 revoke',
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');
20-
// get error and error_description if they exist
21-
const error = urlParams.get('error');
22-
// const errorDescription = urlParams.get('error_description');
32+
const state = urlParams.get('state');
2333

24-
if (!code && !error) {
34+
document.getElementById('login')?.addEventListener('click', async () => {
2535
const response = await oidcClient.authorize.background();
2636

2737
if ('error' in response) {
2838
console.error('Authorization Error:', response);
29-
// window.location.assign(response.redirectUrl);
39+
40+
if (response.redirectUrl) {
41+
window.location.assign(response.redirectUrl);
42+
} else {
43+
console.log('Authorization failed with no ability to redirect:', response);
44+
}
3045
return;
46+
47+
// Handle success response from background authorization
3148
} else if ('code' in response) {
3249
console.log('Authorization Code:', response.code);
50+
const tokenResponse = await oidcClient.token.exchange(response.code, response.state);
51+
52+
if ('error' in response) {
53+
console.error('Token Exchange Error:', tokenResponse);
54+
} else {
55+
console.log('Token Exchange Response:', tokenResponse);
56+
document.getElementById('logout')!.style.display = 'block';
57+
document.getElementById('login')!.style.display = 'none';
58+
}
59+
}
60+
});
61+
62+
document.getElementById('logout')?.addEventListener('click', async () => {
63+
const response = await oidcClient.user.logout();
64+
65+
if (response && 'error' in response) {
66+
console.error('Logout Error:', response);
67+
} else {
68+
console.log('Logout successful');
69+
document.getElementById('logout')!.style.display = 'none';
70+
document.getElementById('login')!.style.display = 'block';
71+
}
72+
});
73+
74+
if (code && state) {
75+
const response = await oidcClient.token.exchange(code, state);
76+
77+
if ('error' in response) {
78+
console.error('Token Exchange Error:', response);
79+
} else {
80+
console.log('Token Exchange Response:', response);
3381
}
3482
}
3583
}

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)