diff --git a/.changeset/calm-waves-change.md b/.changeset/calm-waves-change.md new file mode 100644 index 000000000..9f7566fe8 --- /dev/null +++ b/.changeset/calm-waves-change.md @@ -0,0 +1,9 @@ +--- +'@forgerock/iframe-manager': minor +'@forgerock/storage': minor +'@forgerock/sdk-oidc': minor +'@forgerock/davinci-client': minor +'@forgerock/oidc-client': minor +--- + +Implemented token exchange within OIDC Client diff --git a/e2e/oidc-app/src/main.ts b/e2e/oidc-app/src/main.ts index 92c9748d0..0a1612c86 100644 --- a/e2e/oidc-app/src/main.ts +++ b/e2e/oidc-app/src/main.ts @@ -1,35 +1,72 @@ import { oidc } from '@forgerock/oidc-client'; -async function app() { - const oidcClient = await oidc({ - config: { - clientId: 'WebOAuthClient', - redirectUri: 'http://localhost:8443/', - scope: 'openid', - serverConfig: { - wellknown: - 'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration', - }, +// const pingAmConfig = { +// config: { +// clientId: 'WebOAuthClient', +// redirectUri: 'http://localhost:8443/', +// scope: 'openid', +// serverConfig: { +// wellknown: +// 'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration', +// }, +// }, +// }; +const pingOneConfig = { + config: { + clientId: '654b14e2-7cc5-4977-8104-c4113e43c537', + redirectUri: 'http://localhost:8443/', + scope: 'openid', + serverConfig: { + wellknown: + 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration', }, - }); + }, +}; + +async function app() { + const oidcClient = await oidc(pingOneConfig); // create object from URL query parameters const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); - // const state = urlParams.get('state'); + const state = urlParams.get('state'); // get error and error_description if they exist const error = urlParams.get('error'); // const errorDescription = urlParams.get('error_description'); + // Handle background authorization flow if (!code && !error) { const response = await oidcClient.authorize.background(); if ('error' in response) { console.error('Authorization Error:', response); - // window.location.assign(response.redirectUrl); + + if (response.redirectUrl) { + window.location.assign(response.redirectUrl); + } else { + console.log('Authorization failed with no ability to redirect:', response); + } return; + + // Handle success response from background authorization } else if ('code' in response) { console.log('Authorization Code:', response.code); + const tokenResponse = await oidcClient.token.exchange(response.code, response.state); + if ('error' in response) { + console.error('Token Exchange Error:', tokenResponse); + } else { + console.log('Token Exchange Response:', tokenResponse); + } + } + + // Handle the user redirecting after authentication + } else if (code && state) { + const response = await oidcClient.token.exchange(code, state); + + if ('error' in response) { + console.error('Token Exchange Error:', response); + } else { + console.log('Token Exchange Response:', response); } } } diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index 6277229ae..1fae89106 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -66,10 +66,10 @@ export async function davinci({ }) { const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom }); const store = createClientStore({ requestMiddleware, logger: log }); - const serverInfo = createStorage( - { storeType: 'localStorage' }, - 'serverInfo', - ); + const serverInfo = createStorage({ + type: 'localStorage', + name: 'serverInfo', + }); if (!config.serverConfig.wellknown) { const error = new Error( '`wellknown` property is a required as part of the `config.serverConfig`', diff --git a/packages/oidc-client/README.md b/packages/oidc-client/README.md index 23dccf0ba..63ae29fb7 100644 --- a/packages/oidc-client/README.md +++ b/packages/oidc-client/README.md @@ -4,7 +4,7 @@ A generic OpenID Connect (OIDC) client library for JavaScript and TypeScript, de ```js // Initialize OIDC Client -const oidcClient = oidc({ +const oidcClient1 = oidc({ /* config */ }); diff --git a/packages/oidc-client/package.json b/packages/oidc-client/package.json index e3a18ba5d..a87760bc2 100644 --- a/packages/oidc-client/package.json +++ b/packages/oidc-client/package.json @@ -31,6 +31,7 @@ "@forgerock/sdk-oidc": "workspace:*", "@forgerock/sdk-request-middleware": "workspace:*", "@forgerock/sdk-types": "workspace:*", + "@forgerock/storage": "workspace:*", "@reduxjs/toolkit": "catalog:", "effect": "^3.12.7" }, diff --git a/packages/oidc-client/src/lib/authorize.request.ts b/packages/oidc-client/src/lib/authorize.request.ts index eea5d11f8..ba2e1e813 100644 --- a/packages/oidc-client/src/lib/authorize.request.ts +++ b/packages/oidc-client/src/lib/authorize.request.ts @@ -39,14 +39,19 @@ export async function authorizeµ( * set iframe's to DENY. */ return authorizeFetchµ(url).pipe( - Micro.flatMap((response) => { - if ('authorizeResponse' in response) { - log.debug('Received authorize response', response.authorizeResponse); - return Micro.succeed(response.authorizeResponse); - } - log.error('Error in authorize response', response); - return Micro.fail(createAuthorizeErrorµ(response, wellknown, config, options)); - }), + Micro.flatMap( + (response): Micro.Micro => { + if ('authorizeResponse' in response) { + log.debug('Received authorize response', response.authorizeResponse); + return Micro.succeed(response.authorizeResponse); + } + log.error('Error in authorize response', response); + // For redirection, we need to remore `pi.flow` from the options + const redirectOptions = options; + delete redirectOptions.responseMode; + return createAuthorizeErrorµ(response, wellknown, config, options); + }, + ), ); } else { /** @@ -54,15 +59,17 @@ export async function authorizeµ( * redirect based server supporting iframes. An example would be PingAM. */ return authorizeIframeµ(url, config).pipe( - Micro.flatMap((response) => { - if ('code' in response && 'state' in response) { - log.debug('Received authorization code', response); - return Micro.succeed(response as unknown as AuthorizeSuccessResponse); - } - log.error('Error in authorize response', response); - const errorResponse = response as unknown as AuthorizeErrorResponse; - return Micro.fail(createAuthorizeErrorµ(errorResponse, wellknown, config, options)); - }), + Micro.flatMap( + (response): Micro.Micro => { + if ('code' in response && 'state' in response) { + log.debug('Received authorization code', response); + return Micro.succeed(response as unknown as AuthorizeSuccessResponse); + } + log.error('Error in authorize response', response); + const errorResponse = response as unknown as AuthorizeErrorResponse; + return createAuthorizeErrorµ(errorResponse, wellknown, config, options); + }, + ), ); } }), diff --git a/packages/oidc-client/src/lib/authorize.request.types.ts b/packages/oidc-client/src/lib/authorize.request.types.ts index 1618b27e9..9df7efe62 100644 --- a/packages/oidc-client/src/lib/authorize.request.types.ts +++ b/packages/oidc-client/src/lib/authorize.request.types.ts @@ -13,6 +13,6 @@ export interface AuthorizeSuccessResponse { export interface AuthorizeErrorResponse { error: string; error_description: string; - redirectUrl: string; // URL to redirect the user to for re-authorization - type: 'auth_error'; + redirectUrl?: string; // URL to redirect the user to for re-authorization + type: 'auth_error' | 'argument_error' | 'wellknown_error'; } diff --git a/packages/oidc-client/src/lib/authorize.request.utils.ts b/packages/oidc-client/src/lib/authorize.request.utils.ts index 7c260c2e3..34f804602 100644 --- a/packages/oidc-client/src/lib/authorize.request.utils.ts +++ b/packages/oidc-client/src/lib/authorize.request.utils.ts @@ -45,8 +45,8 @@ export function authorizeFetchµ(url: string) { export function authorizeIframeµ(url: string, config: OidcConfig) { return Micro.tryPromise({ - try: () => - iFrameManager().getParamsByRedirect({ + try: () => { + const params = iFrameManager().getParamsByRedirect({ url, /*** * https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2 @@ -55,15 +55,17 @@ export function authorizeIframeµ(url: string, config: OidcConfig) { successParams: ['code', 'state'], errorParams: ['error', 'error_description'], timeout: config.serverConfig.timeout || 3000, - }), + }); + return params; + }, catch: (err) => { - let message = 'Error fetching authorization URL'; + let message = 'Error calling authorization URL'; if (err instanceof Error) { message = err.message; } return { - error: 'Authorization Notwork Failure', + error: 'Authorization Network Failure', error_description: message, type: 'auth_error', } as AuthorizeErrorResponse; @@ -102,18 +104,10 @@ export function createAuthorizeErrorµ( options: GetAuthorizationUrlOptions, ) { return Micro.tryPromise({ - try: async () => { - const url = await createAuthorizeUrl(wellknown.authorization_endpoint, { + try: () => + createAuthorizeUrl(wellknown.authorization_endpoint, { ...options, - prompt: 'none', - }); - return { - error: 'AuthorizationUrlError', - error_description: `Error creating authorization URL for ${url}`, - type: 'auth_error', - redirectUrl: url, - } as AuthorizeErrorResponse; - }, + }), catch: (error) => { let message = 'Error creating authorization URL'; if (error instanceof Error) { @@ -125,7 +119,16 @@ export function createAuthorizeErrorµ( type: 'auth_error', } as AuthorizeErrorResponse; }, - }); + }).pipe( + Micro.flatMap((url) => { + return Micro.fail({ + error: res.error, + error_description: res.error_description, + type: 'auth_error', + redirectUrl: url, + } as AuthorizeErrorResponse); + }), + ); } export function createAuthorizeUrlµ( diff --git a/packages/oidc-client/src/lib/authorize.slice.ts b/packages/oidc-client/src/lib/authorize.slice.ts new file mode 100644 index 000000000..72838b0a0 --- /dev/null +++ b/packages/oidc-client/src/lib/authorize.slice.ts @@ -0,0 +1,23 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; + +const authorizeSlice = createApi({ + reducerPath: 'authorizeSlice', + baseQuery: fetchBaseQuery({ + credentials: 'include', + prepareHeaders: (headers) => { + headers.set('Content-Type', 'application/json'); + headers.set('Accept', 'application/json'); + headers.set('x-requested-with', 'ping-sdk'); + headers.set('x-requested-platform', 'javascript'); + + return headers; + }, + }), + endpoints: (builder) => ({ + handleAuthorize: builder.query({ + query: (authorizeUrl) => authorizeUrl, + }), + }), +}); + +export { authorizeSlice }; diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index c066f9a3d..072148c1c 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -6,23 +6,29 @@ */ import { CustomLogger, logger as loggerFn, LogLevel } from '@forgerock/sdk-logger'; import { createAuthorizeUrl } from '@forgerock/sdk-oidc'; -import { exitIsSuccess } from 'effect/Micro'; +import { createStorage, StorageConfig } from '@forgerock/storage'; +import { Micro } from 'effect'; +import { exitIsFail, exitIsSuccess } from 'effect/Micro'; import { authorizeµ } from './authorize.request.js'; import { createClientStore } from './client.store.utils.js'; +import { createValuesµ, handleTokenResponseµ, validateValuesµ } from './exchange.utils.js'; import { GenericError } from './error.types.js'; +import { oidcApi } from './oidc.api.js'; import { wellknownApi, wellknownSelector } from './wellknown.api.js'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; import type { OidcConfig } from './config.types.js'; -import { Micro } from 'effect'; +import type { AuthorizeErrorResponse } from './authorize.request.types.js'; +import type { TokenExchangeErrorResponse } from './exchange.types.js'; export async function oidc({ config, requestMiddleware, logger, + storage, }: { config: OidcConfig; requestMiddleware?: RequestMiddleware[]; @@ -30,19 +36,25 @@ export async function oidc({ level: LogLevel; custom?: CustomLogger; }; + storage?: Partial; }) { const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom }); + const storageClient = createStorage({ + type: 'localStorage', + name: 'oidcTokens', + ...storage, + } as StorageConfig); const store = createClientStore({ requestMiddleware, logger: log }); if (!config?.serverConfig?.wellknown) { return { - message: 'Requires a wellknown url initializing this factory.', + error: 'Requires a wellknown url initializing this factory.', type: 'argument_error', }; } if (!config?.clientId) { return { - message: 'Requires a clientId.', + error: 'Requires a clientId.', type: 'argument_error', }; } @@ -54,7 +66,7 @@ export async function oidc({ if (error || !data) { return { - message: `Error fetching wellknown config`, + error: `Error fetching wellknown config`, type: 'network_error', }; } @@ -75,11 +87,11 @@ export async function oidc({ if (!wellknown?.authorization_endpoint) { const err = { - message: 'Authorization endpoint not found in wellknown configuration', + error: 'Authorization endpoint not found in wellknown configuration', type: 'wellknown_error', } as const; - log.error(err.message); + log.error(err.error); return err; } @@ -92,11 +104,12 @@ export async function oidc({ if (!wellknown?.authorization_endpoint) { const err = { - message: 'Authorization endpoint not found in wellknown configuration', + error: 'Wellknown missing authorization endpoint', + error_description: 'Authorization endpoint not found in wellknown configuration', type: 'wellknown_error', - } as const; + } as AuthorizeErrorResponse; - log.error(err.message); + log.error(err.error); return err; } @@ -107,8 +120,63 @@ export async function oidc({ if (exitIsSuccess(result)) { return result.value; + } else if (exitIsFail(result)) { + return result.cause.error; + } else { + return { + error: 'Authorization failure', + error_description: result.cause.message, + type: 'auth_error', + } as AuthorizeErrorResponse; + } + }, + }, + token: { + exchange: async (code: string, state: string, options?: Partial) => { + const storeState = store.getState(); + const wellknown = wellknownSelector(wellknownUrl, storeState); + + if (!wellknown?.token_endpoint) { + const err = { + error: 'Wellknown missing token endpoint', + type: 'wellknown_error', + } as AuthorizeErrorResponse; + + log.error(err.error); + + return err; + } + + const buildTokenExchangeµ = Micro.sync(() => + createValuesµ(code, config, state, wellknown, options), + ).pipe( + Micro.flatMap((options) => validateValuesµ(options)), + Micro.flatMap((requestOptions) => + Micro.promise( + async () => await store.dispatch(oidcApi.endpoints.exchange.initiate(requestOptions)), + ), + ), + Micro.flatMap(({ data, error }) => handleTokenResponseµ(data, error)), + Micro.flatMap((data) => + Micro.promise(async () => { + await storageClient.set(data); + return data; + }), + ), + ); + + const result = await Micro.runPromiseExit(buildTokenExchangeµ); + + if (exitIsSuccess(result)) { + return result.value; + } else if (exitIsFail(result) && 'error' in result.cause) { + return result.cause.error; } else { - return result.cause; + return { + error: 'Token Exchange failure', + message: result.cause.message, + type: 'exchange_error', + } as TokenExchangeErrorResponse; } }, }, diff --git a/packages/oidc-client/src/lib/client.store.utils.ts b/packages/oidc-client/src/lib/client.store.utils.ts index 8667426cd..7f0faa7fe 100644 --- a/packages/oidc-client/src/lib/client.store.utils.ts +++ b/packages/oidc-client/src/lib/client.store.utils.ts @@ -8,6 +8,7 @@ import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-midd import type { logger as loggerFn } from '@forgerock/sdk-logger'; import { configureStore } from '@reduxjs/toolkit'; +import { oidcApi } from './oidc.api.js'; import { wellknownApi } from './wellknown.api.js'; export function createClientStore({ @@ -19,6 +20,7 @@ export function createClientStore({ }) { return configureStore({ reducer: { + [oidcApi.reducerPath]: oidcApi.reducer, [wellknownApi.reducerPath]: wellknownApi.reducer, }, middleware: (getDefaultMiddleware) => @@ -33,7 +35,9 @@ export function createClientStore({ logger, }, }, - }).concat(wellknownApi.middleware), + }) + .concat(wellknownApi.middleware) + .concat(oidcApi.middleware), }); } diff --git a/packages/oidc-client/src/lib/error.types.ts b/packages/oidc-client/src/lib/error.types.ts index a800c58a5..14b415fe1 100644 --- a/packages/oidc-client/src/lib/error.types.ts +++ b/packages/oidc-client/src/lib/error.types.ts @@ -6,7 +6,8 @@ */ export interface GenericError { code?: string | number; - message: string; + error: string; + message?: string; type: | 'argument_error' | 'auth_error' diff --git a/packages/oidc-client/src/lib/exchange.types.ts b/packages/oidc-client/src/lib/exchange.types.ts new file mode 100644 index 000000000..312eb74d0 --- /dev/null +++ b/packages/oidc-client/src/lib/exchange.types.ts @@ -0,0 +1,23 @@ +import { OidcConfig } from './config.types.js'; + +export interface TokenExchangeResponse { + access_token: string; + id_token?: string; + refresh_token?: string; + expires_in?: number; + scope?: string; + token_type?: string; +} + +export interface TokenExchangeErrorResponse { + error: string; + message: string; + type: 'exchange_error' | 'network_error' | 'unknown_error'; +} + +export interface TokenRequestOptions { + code: string; + config: OidcConfig; + endpoint: string; + verifier?: string; +} diff --git a/packages/oidc-client/src/lib/exchange.utils.ts b/packages/oidc-client/src/lib/exchange.utils.ts new file mode 100644 index 000000000..06825de9b --- /dev/null +++ b/packages/oidc-client/src/lib/exchange.utils.ts @@ -0,0 +1,89 @@ +import { SerializedError } from '@reduxjs/toolkit'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { Micro } from 'effect'; + +import type { TokenExchangeResponse, TokenRequestOptions } from './exchange.types.js'; +import type { TokenExchangeErrorResponse } from './exchange.types.js'; +import { GenericError, StorageConfig } from '@forgerock/storage'; +import { GetAuthorizationUrlOptions, getStoredAuthUrlValues } from '@forgerock/sdk-oidc'; +import { OidcConfig } from './config.types.js'; +import { WellKnownResponse } from '@forgerock/sdk-types'; + +export function createValuesµ( + code: string, + config: OidcConfig, + state: string, + wellknown: WellKnownResponse, + options?: Partial, +) { + const storedValues = getStoredAuthUrlValues(config.clientId, options?.prefix); + + return { + code, + config, + state, + storedValues, + wellknown, + }; +} + +export function handleTokenResponseµ( + data: TokenExchangeResponse | undefined, + error?: FetchBaseQueryError | SerializedError, +) { + if (error) { + let message; + if ('status' in error) { + message = 'error' in error ? error.error : JSON.stringify(error.data); + } else if ('message' in error) { + message = error.message; + } + + return Micro.fail({ + error: 'Token Exchange failure', + message: message || 'Unknown error during token exchange', + type: 'exchange_error', + } as TokenExchangeErrorResponse); + } + + if (!data) { + return Micro.fail({ + error: 'Token Exchange failure', + message: 'No data returned from token exchange', + type: 'exchange_error', + } as TokenExchangeErrorResponse); + } + + return Micro.succeed(data); +} + +export function validateValuesµ({ + code, + config, + state, + storedValues, + wellknown, +}: { + code: string; + config: OidcConfig; + state: string; + storedValues: GetAuthorizationUrlOptions; + wellknown: { token_endpoint: string }; +}) { + if (!storedValues || storedValues.state !== state) { + const err = { + error: 'State mismatch', + message: + 'The provided state does not match the stored state. This is likely due to passing in used, returned, authorize parameters.', + type: 'state_error', + } as GenericError; + + return Micro.fail(err as GenericError); + } + return Micro.succeed({ + code, + config, + endpoint: wellknown.token_endpoint, + ...(storedValues.verifier && { verifier: storedValues.verifier }), // Optional PKCE + } as TokenRequestOptions); +} diff --git a/packages/oidc-client/src/lib/oidc.api.ts b/packages/oidc-client/src/lib/oidc.api.ts new file mode 100644 index 000000000..20dab8e40 --- /dev/null +++ b/packages/oidc-client/src/lib/oidc.api.ts @@ -0,0 +1,52 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; +import { OidcConfig } from './config.types.js'; +import { TokenExchangeResponse } from './exchange.types.js'; + +export const oidcApi = createApi({ + reducerPath: 'oidc', + baseQuery: fetchBaseQuery(), + endpoints: (builder) => ({ + exchange: builder.mutation< + TokenExchangeResponse, + { + code: string; + config: OidcConfig; + endpoint: string; + verifier?: string; + } + >({ + query: ({ code, config, endpoint, verifier }) => { + const { clientId, redirectUri } = config; + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code, + client_id: clientId, + redirect_uri: redirectUri, + }); + + if (verifier) { + body.append('code_verifier', verifier); + } + + return { + url: endpoint, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body, + }; + }, + transformResponse: (res) => { + if (!res || typeof res !== 'object') { + throw new Error('Invalid response from token exchange'); + } + if ('access_token' in res) { + return res as TokenExchangeResponse; + } + throw new Error('Token exchange response does not contain access_token'); + }, + }), + }), +}); diff --git a/packages/oidc-client/src/lib/store.ts b/packages/oidc-client/src/lib/store.ts new file mode 100644 index 000000000..20f6f1139 --- /dev/null +++ b/packages/oidc-client/src/lib/store.ts @@ -0,0 +1,36 @@ +import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { logger as loggerFn } from '@forgerock/sdk-logger'; + +import { configureStore } from '@reduxjs/toolkit'; +import { wellknownApi } from './wellknown.api.js'; +import { authorizeSlice } from './authorize.slice.js'; + +export function createOidcStore({ + requestMiddleware, + logger, +}: { + requestMiddleware?: RequestMiddleware[]; + logger?: ReturnType; +}) { + return configureStore({ + reducer: { + [wellknownApi.reducerPath]: wellknownApi.reducer, + [authorizeSlice.reducerPath]: authorizeSlice.reducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + thunk: { + extraArgument: { + /** + * This becomes the `api.extra` argument, and will be passed into the + * customer query wrapper for `baseQuery` + */ + requestMiddleware, + logger, + }, + }, + }) + .concat(wellknownApi.middleware) + .concat(authorizeSlice.middleware), + }); +} diff --git a/packages/oidc-client/tsconfig.json b/packages/oidc-client/tsconfig.json index 6d83720aa..f9c5bdebb 100644 --- a/packages/oidc-client/tsconfig.json +++ b/packages/oidc-client/tsconfig.json @@ -3,6 +3,9 @@ "files": [], "include": [], "references": [ + { + "path": "../sdk-effects/storage" + }, { "path": "../sdk-types" }, @@ -18,6 +21,18 @@ { "path": "../sdk-effects/iframe-manager" }, + { + "path": "../sdk-effects/sdk-request-middleware" + }, + { + "path": "../sdk-effects/oidc" + }, + { + "path": "../sdk-effects/logger" + }, + { + "path": "../sdk-effects/iframe-manager" + }, { "path": "./tsconfig.lib.json" }, diff --git a/packages/oidc-client/tsconfig.lib.json b/packages/oidc-client/tsconfig.lib.json index c2112b390..6a283a7df 100644 --- a/packages/oidc-client/tsconfig.lib.json +++ b/packages/oidc-client/tsconfig.lib.json @@ -16,6 +16,9 @@ }, "include": ["src/**/*.ts"], "references": [ + { + "path": "../sdk-effects/storage/tsconfig.lib.json" + }, { "path": "../sdk-types/tsconfig.lib.json" }, diff --git a/packages/sdk-effects/iframe-manager/src/lib/iframe-manager.effects.ts b/packages/sdk-effects/iframe-manager/src/lib/iframe-manager.effects.ts index 057d98eaf..2fee278e3 100644 --- a/packages/sdk-effects/iframe-manager/src/lib/iframe-manager.effects.ts +++ b/packages/sdk-effects/iframe-manager/src/lib/iframe-manager.effects.ts @@ -116,7 +116,7 @@ export function iFrameManager() { // 1. Check for Error Parameters if (hasErrorParams(searchParams, errorParams)) { cleanup(); - reject(parsedParams); // Reject with all parsed params for context + resolve(parsedParams); // Reject with all parsed params for context return; } diff --git a/packages/sdk-effects/storage/src/lib/storage.effects.test.ts b/packages/sdk-effects/storage/src/lib/storage.effects.test.ts index ea732d462..b910495ab 100644 --- a/packages/sdk-effects/storage/src/lib/storage.effects.test.ts +++ b/packages/sdk-effects/storage/src/lib/storage.effects.test.ts @@ -85,7 +85,8 @@ const mockCustomStore: CustomStorageObject = { describe('storage Effect', () => { const storageName = 'MyStorage'; const baseConfig: Omit = { - storeType: 'localStorage', + type: 'localStorage', + name: storageName, prefix: 'testPrefix', }; const expectedKey = `${baseConfig.prefix}-${storageName}`; @@ -104,10 +105,11 @@ describe('storage Effect', () => { describe('with localStorage', () => { const config: StorageConfig = { ...baseConfig, - storeType: 'localStorage', + name: storageName, + type: 'localStorage', }; - const storageInstance = createStorage(config, storageName); + const storageInstance = createStorage(config); it('should call localStorage.getItem with the correct key and return value', async () => { localStorageMock.setItem(expectedKey, JSON.stringify(testValue)); @@ -169,12 +171,13 @@ describe('storage Effect', () => { }); describe('with sessionStorage', () => { + const storageName = 'MyStorage'; const config: StorageConfig = { ...baseConfig, - storeType: 'sessionStorage', + name: storageName, + type: 'sessionStorage', }; - const storageName = 'MyStorage'; - const storageInstance = createStorage(config, storageName); + const storageInstance = createStorage(config); it('should call sessionStorage.getItem with the correct key and return value', async () => { sessionStorageMock.setItem(expectedKey, JSON.stringify(testValue)); @@ -235,12 +238,13 @@ describe('storage Effect', () => { }); describe('with custom TokenStoreObject', () => { + const myStorage = 'MyStorage'; const config: StorageConfig = { ...baseConfig, - storeType: 'localStorage', + type: 'custom', + custom: mockCustomStore, }; - const myStorage = 'MyStorage'; - const storageInstance = createStorage(config, myStorage, mockCustomStore); + const storageInstance = createStorage(config); it('should call customStore.get with the correct key and return its value', async () => { (mockCustomStore.get as Mock).mockResolvedValueOnce(JSON.stringify(testValue)); @@ -298,9 +302,9 @@ describe('storage Effect', () => { }); it('should return a function that returns the storage interface', () => { - const config: StorageConfig = { ...baseConfig, storeType: 'localStorage' }; const myStorage = 'MyStorage'; - const storageInterface = createStorage(config, myStorage); + const config: StorageConfig = { ...baseConfig, type: 'localStorage' }; + const storageInterface = createStorage(config); expect(storageInterface).toHaveProperty('get'); expect(storageInterface).toHaveProperty('set'); expect(storageInterface).toHaveProperty('remove'); diff --git a/packages/sdk-effects/storage/src/lib/storage.effects.ts b/packages/sdk-effects/storage/src/lib/storage.effects.ts index 9b1999277..4f55235f6 100644 --- a/packages/sdk-effects/storage/src/lib/storage.effects.ts +++ b/packages/sdk-effects/storage/src/lib/storage.effects.ts @@ -6,9 +6,29 @@ */ import { CustomStorageObject } from '@forgerock/sdk-types'; -export interface StorageConfig { - storeType: CustomStorageObject | 'localStorage' | 'sessionStorage'; +export interface StorageClient { + get: () => Promise; + set: (value: Value) => Promise; + remove: () => Promise; +} + +export type StorageConfig = BrowserStorageConfig | CustomStorageConfig; + +export interface BrowserStorageConfig { + type: 'localStorage' | 'sessionStorage'; prefix?: string; + name: string; +} + +export interface CustomStorageConfig { + type: 'custom'; + prefix?: string; + name: string; + custom: CustomStorageObject; } export interface GenericError { @@ -23,18 +43,18 @@ export interface GenericError { | 'unknown_error'; } -export function createStorage( - config: StorageConfig, - storageName: string, - customStore?: CustomStorageObject, -) { - const { storeType, prefix = 'pic' } = config; +export function createStorage(config: StorageConfig) { + const { type: storeType, prefix = 'pic', name } = config; + + if (storeType === 'custom' && !('custom' in config)) { + throw new Error('Custom storage configuration must include a custom storage object'); + } - const key = `${prefix}-${storageName}`; + const key = `${prefix}-${name}`; return { get: async function storageGet(): Promise { - if (customStore) { - const value = await customStore.get(key); + if ('custom' in config) { + const value = await config.custom.get(key); if (value === null) { return value; } @@ -83,9 +103,9 @@ export function createStorage( }, set: async function storageSet(value: Value) { const valueToStore = JSON.stringify(value); - if (customStore) { + if ('custom' in config) { try { - await customStore.set(key, valueToStore); + await config.custom.set(key, valueToStore); return Promise.resolve(); } catch { return { @@ -119,8 +139,8 @@ export function createStorage( } }, remove: async function storageSet() { - if (customStore) { - return await customStore.remove(key); + if ('custom' in config) { + return await config.custom.remove(key); } if (storeType === 'sessionStorage') { return await sessionStorage.removeItem(key); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22ec94592..b9931b961 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -220,7 +220,7 @@ importers: version: 6.2.6(@types/node@22.14.1)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0) vitest: specifier: 3.0.5 - version: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) + version: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) vitest-canvas-mock: specifier: ^0.3.3 version: 0.3.3(vitest@3.0.5) @@ -284,7 +284,7 @@ importers: devDependencies: '@effect/vitest': specifier: ^0.19.0 - version: 0.19.10(effect@3.16.0)(vitest@3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0)) + version: 0.19.10(effect@3.16.0)(vitest@3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0)) e2e/oidc-app: dependencies: @@ -334,7 +334,7 @@ importers: devDependencies: vitest: specifier: ^3.0.4 - version: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) + version: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) packages/device-client: dependencies: @@ -366,6 +366,9 @@ importers: '@forgerock/sdk-types': specifier: workspace:* version: link:../sdk-types + '@forgerock/storage': + specifier: workspace:* + version: link:../sdk-effects/storage '@reduxjs/toolkit': specifier: 'catalog:' version: 2.8.2 @@ -8484,15 +8487,15 @@ snapshots: dependencies: effect: 3.16.0 - '@effect/vitest@0.19.10(effect@3.16.0)(vitest@3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0))': + '@effect/vitest@0.19.10(effect@3.16.0)(vitest@3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0))': dependencies: effect: 3.16.0 - vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) + vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) '@effect/vitest@0.6.12(effect@3.16.0)(vitest@3.0.5)': dependencies: effect: 3.16.0 - vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) + vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) '@emnapi/core@1.4.3': dependencies: @@ -9198,7 +9201,7 @@ snapshots: semver: 7.7.2 tsconfig-paths: 4.2.0 vite: 6.2.6(@types/node@22.14.1)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0) - vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) + vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10058,7 +10061,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) + vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -10114,7 +10117,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) + vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) '@vitest/utils@3.0.4': dependencies: @@ -14974,9 +14977,9 @@ snapshots: vitest-canvas-mock@0.3.3(vitest@3.0.5): dependencies: jest-canvas-mock: 2.5.2 - vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) + vitest: 3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0) - vitest@3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4(vitest@3.0.5))(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0): + vitest@3.0.5(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(terser@5.42.0)(yaml@2.8.0): dependencies: '@vitest/expect': 3.0.5 '@vitest/mocker': 3.0.5(msw@2.8.5(@types/node@22.14.1)(typescript@5.8.3))(vite@6.2.6(@types/node@22.14.1)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))