Skip to content

Commit 3129ce2

Browse files
committed
feat(oidc-client): implement token exchange
1 parent 41a0e28 commit 3129ce2

12 files changed

Lines changed: 177 additions & 20 deletions

e2e/oidc-app/src/main.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async function app() {
1616
// create object from URL query parameters
1717
const urlParams = new URLSearchParams(window.location.search);
1818
const code = urlParams.get('code');
19-
// const state = urlParams.get('state');
19+
const state = urlParams.get('state');
2020
// get error and error_description if they exist
2121
const error = urlParams.get('error');
2222
// const errorDescription = urlParams.get('error_description');
@@ -26,11 +26,19 @@ async function app() {
2626

2727
if ('error' in response) {
2828
console.error('Authorization Error:', response);
29-
// window.location.assign(response.redirectUrl);
29+
window.location.assign(response.redirectUrl);
3030
return;
3131
} else if ('code' in response) {
3232
console.log('Authorization Code:', response.code);
3333
}
34+
} else if (code && state) {
35+
const response = await oidcClient.token.exchange(code, state);
36+
37+
if ('error' in response) {
38+
console.error('Token Exchange Error:', response);
39+
} else {
40+
console.log('Token Exchange Response:', response);
41+
}
3442
}
3543
}
3644

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.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' | 'wellknown_error' | 'network_error' | 'unknown_error';
1818
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,10 @@ export function createAuthorizeErrorµ(
105105
try: async () => {
106106
const url = await createAuthorizeUrl(wellknown.authorization_endpoint, {
107107
...options,
108-
prompt: 'none',
109108
});
110109
return {
111-
error: 'AuthorizationUrlError',
112-
error_description: `Error creating authorization URL for ${url}`,
110+
error: res.error,
111+
error_description: res.error_description,
113112
type: 'auth_error',
114113
redirectUrl: url,
115114
} as AuthorizeErrorResponse;

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

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,24 @@
55
* of the MIT license. See the LICENSE file for details.
66
*/
77
import { CustomLogger, logger as loggerFn, LogLevel } from '@forgerock/sdk-logger';
8-
import { createAuthorizeUrl } from '@forgerock/sdk-oidc';
8+
import { createAuthorizeUrl, getStoredAuthUrlValues } from '@forgerock/sdk-oidc';
9+
import { createStorage } from '@forgerock/storage';
10+
import { Micro } from 'effect';
911
import { exitIsSuccess } from 'effect/Micro';
1012

1113
import { authorizeµ } from './authorize.request.js';
1214
import { createClientStore } from './client.store.utils.js';
1315
import { GenericError } from './error.types.js';
16+
import { oidcApi } from './oidc.api.js';
1417
import { wellknownApi, wellknownSelector } from './wellknown.api.js';
1518

1619
import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware';
1720
import type { GetAuthorizationUrlOptions } from '@forgerock/sdk-types';
1821

1922
import type { OidcConfig } from './config.types.js';
20-
import { Micro } from 'effect';
23+
import { AuthorizeErrorResponse } from './authorize.request.types.js';
24+
import { TokenExchangeOptions } from './client.store.types.js';
25+
import { TokenRequestOptions } from './token.types.js';
2126

2227
export async function oidc<ActionType extends ActionTypes = ActionTypes>({
2328
config,
@@ -36,13 +41,13 @@ export async function oidc<ActionType extends ActionTypes = ActionTypes>({
3641

3742
if (!config?.serverConfig?.wellknown) {
3843
return {
39-
message: 'Requires a wellknown url initializing this factory.',
44+
error: 'Requires a wellknown url initializing this factory.',
4045
type: 'argument_error',
4146
};
4247
}
4348
if (!config?.clientId) {
4449
return {
45-
message: 'Requires a clientId.',
50+
error: 'Requires a clientId.',
4651
type: 'argument_error',
4752
};
4853
}
@@ -54,7 +59,7 @@ export async function oidc<ActionType extends ActionTypes = ActionTypes>({
5459

5560
if (error || !data) {
5661
return {
57-
message: `Error fetching wellknown config`,
62+
error: `Error fetching wellknown config`,
5863
type: 'network_error',
5964
};
6065
}
@@ -75,11 +80,11 @@ export async function oidc<ActionType extends ActionTypes = ActionTypes>({
7580

7681
if (!wellknown?.authorization_endpoint) {
7782
const err = {
78-
message: 'Authorization endpoint not found in wellknown configuration',
83+
error: 'Authorization endpoint not found in wellknown configuration',
7984
type: 'wellknown_error',
8085
} as const;
8186

82-
log.error(err.message);
87+
log.error(err.error);
8388

8489
return err;
8590
}
@@ -92,11 +97,12 @@ export async function oidc<ActionType extends ActionTypes = ActionTypes>({
9297

9398
if (!wellknown?.authorization_endpoint) {
9499
const err = {
95-
message: 'Authorization endpoint not found in wellknown configuration',
100+
error: 'Wellknown missing authorization endpoint',
101+
error_description: 'Authorization endpoint not found in wellknown configuration',
96102
type: 'wellknown_error',
97-
} as const;
103+
} as AuthorizeErrorResponse;
98104

99-
log.error(err.message);
105+
log.error(err.error);
100106

101107
return err;
102108
}
@@ -108,9 +114,75 @@ export async function oidc<ActionType extends ActionTypes = ActionTypes>({
108114
if (exitIsSuccess(result)) {
109115
return result.value;
110116
} else {
111-
return result.cause;
117+
return {
118+
error: 'Authorization failure',
119+
error_description: result.cause.message,
120+
type: 'auth_error',
121+
} as AuthorizeErrorResponse;
112122
}
113123
},
114124
},
125+
token: {
126+
exchange: async (code: string, state: string, options?: TokenExchangeOptions) => {
127+
const storeState = store.getState();
128+
const wellknown = wellknownSelector(wellknownUrl, storeState);
129+
130+
if (!wellknown?.token_endpoint) {
131+
const err = {
132+
error: 'Wellknown missing token endpoint',
133+
type: 'wellknown_error',
134+
} as AuthorizeErrorResponse;
135+
136+
log.error(err.error);
137+
138+
return err;
139+
}
140+
141+
// TODO: Validate state
142+
const values = getStoredAuthUrlValues(config.clientId, options?.prefix);
143+
144+
if (values.state !== state) {
145+
const err = {
146+
error: 'State mismatch',
147+
type: 'auth_error',
148+
} as GenericError;
149+
150+
log.error(err.error);
151+
152+
return err;
153+
}
154+
155+
const requestOptions: TokenRequestOptions = {
156+
code,
157+
config,
158+
endpoint: wellknown.token_endpoint,
159+
};
160+
if (values.verifier) {
161+
requestOptions.verifier = values.verifier;
162+
}
163+
164+
const { data, error } = await store.dispatch(
165+
oidcApi.endpoints.exchange.initiate(requestOptions),
166+
);
167+
168+
if (error || !data) {
169+
const err = {
170+
error: 'Error exchanging token',
171+
type: 'network_error',
172+
} as GenericError;
173+
174+
log.error(err.error);
175+
176+
return err;
177+
}
178+
179+
// TODO: handle response and errors; if success, store tokens and return them
180+
createStorage({ storeType: 'localStorage' }, 'oidcTokens', options?.customStorage).set(
181+
data,
182+
);
183+
184+
return data;
185+
},
186+
},
115187
};
116188
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { CustomStorageObject } from '@forgerock/sdk-types';
2+
3+
export interface TokenExchangeOptions {
4+
prefix?: string;
5+
customStorage: CustomStorageObject;
6+
}

packages/oidc-client/src/lib/client.store.utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-midd
88
import type { logger as loggerFn } from '@forgerock/sdk-logger';
99

1010
import { configureStore } from '@reduxjs/toolkit';
11+
import { oidcApi } from './oidc.api.js';
1112
import { wellknownApi } from './wellknown.api.js';
1213

1314
export function createClientStore<ActionType extends ActionTypes>({
@@ -19,6 +20,7 @@ export function createClientStore<ActionType extends ActionTypes>({
1920
}) {
2021
return configureStore({
2122
reducer: {
23+
[oidcApi.reducerPath]: oidcApi.reducer,
2224
[wellknownApi.reducerPath]: wellknownApi.reducer,
2325
},
2426
middleware: (getDefaultMiddleware) =>
@@ -33,7 +35,9 @@ export function createClientStore<ActionType extends ActionTypes>({
3335
logger,
3436
},
3537
},
36-
}).concat(wellknownApi.middleware),
38+
})
39+
.concat(wellknownApi.middleware)
40+
.concat(oidcApi.middleware),
3741
});
3842
}
3943

packages/oidc-client/src/lib/error.types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
*/
77
export interface GenericError {
88
code?: string | number;
9-
message: string;
9+
error: string;
10+
message?: string;
1011
type:
1112
| 'argument_error'
1213
| 'auth_error'
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
2+
import { OidcConfig } from './config.types.js';
3+
import { TokenExchangeResponse } from './token.types.js';
4+
5+
export const oidcApi = createApi({
6+
reducerPath: 'oidc',
7+
baseQuery: fetchBaseQuery(),
8+
endpoints: (builder) => ({
9+
exchange: builder.mutation<
10+
TokenExchangeResponse,
11+
{
12+
code: string;
13+
config: OidcConfig;
14+
endpoint: string;
15+
verifier?: string;
16+
}
17+
>({
18+
query: ({ code, config, endpoint, verifier }) => {
19+
const { clientId, redirectUri } = config;
20+
const body = new URLSearchParams({
21+
grant_type: 'authorization_code',
22+
code,
23+
client_id: clientId,
24+
redirect_uri: redirectUri,
25+
});
26+
27+
if (verifier) {
28+
body.append('code_verifier', verifier);
29+
}
30+
31+
return {
32+
url: endpoint,
33+
method: 'POST',
34+
headers: {
35+
Accept: 'application/json',
36+
'Content-Type': 'application/x-www-form-urlencoded',
37+
},
38+
body,
39+
};
40+
},
41+
}),
42+
}),
43+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { OidcConfig } from './config.types.js';
2+
3+
export interface TokenExchangeResponse {
4+
token: string;
5+
idToken?: string;
6+
refreshToken?: string;
7+
expiresIn?: number;
8+
scope?: string;
9+
tokenType?: string;
10+
}
11+
12+
export interface TokenRequestOptions {
13+
code: string;
14+
config: OidcConfig;
15+
endpoint: string;
16+
verifier?: string;
17+
}

0 commit comments

Comments
 (0)