From 9a03632e044a325da9bdf4073446c465bb34b2fd Mon Sep 17 00:00:00 2001 From: ryanbas21 Date: Mon, 9 Jun 2025 11:37:58 -0600 Subject: [PATCH 1/3] fix(davinci-client): return-url-from-external-idp updating the api for the social login external idp method. Instead of redirecting for the application, we will return a url for the application to redirect to themselves. --- .changeset/clear-cars-tan.md | 5 ++ .../components/social-login-button.ts | 15 +++++- e2e/davinci-app/main.ts | 1 - .../davinci-client/src/lib/client.store.ts | 20 +++++-- .../davinci-client/src/lib/davinci.utils.ts | 52 ++++++++++--------- 5 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 .changeset/clear-cars-tan.md diff --git a/.changeset/clear-cars-tan.md b/.changeset/clear-cars-tan.md new file mode 100644 index 0000000000..1051ef7f2f --- /dev/null +++ b/.changeset/clear-cars-tan.md @@ -0,0 +1,5 @@ +--- +'@forgerock/davinci-client': patch +--- + +Return a url from the externalIdp function for app developers to use to redirect their url diff --git a/e2e/davinci-app/components/social-login-button.ts b/e2e/davinci-app/components/social-login-button.ts index 64a2416970..298f73b1a0 100644 --- a/e2e/davinci-app/components/social-login-button.ts +++ b/e2e/davinci-app/components/social-login-button.ts @@ -5,17 +5,28 @@ * of the MIT license. See the LICENSE file for details. */ import type { IdpCollector } from '@forgerock/davinci-client/types'; +import { InternalErrorResponse } from 'packages/davinci-client/src/lib/client.types.js'; export default function submitButtonComponent( formEl: HTMLFormElement, collector: IdpCollector, - updater: () => void, + updater: () => string | InternalErrorResponse, ) { const button = document.createElement('button'); console.log('collector', collector); button.value = collector.output.label; button.innerHTML = collector.output.label; - button.onclick = () => updater(); + button.onclick = () => { + const url = updater(); + if (typeof url === 'string') { + window.location.assign(url); + } else { + /** + * this is an error now + **/ + console.error(url); + } + }; formEl?.appendChild(button); } diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts index 7f3a57d5ec..580cb8d410 100644 --- a/e2e/davinci-app/main.ts +++ b/e2e/davinci-app/main.ts @@ -239,7 +239,6 @@ const urlParams = new URLSearchParams(window.location.search); ); } else if (collector.type === 'IdpCollector') { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - collector; socialLoginButtonComponent(formEl, collector, davinciClient.externalIdp(collector)); } else if (collector.type === 'FlowCollector') { flowLinkComponent( diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index a38e320aa7..0c6c9ae058 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -33,10 +33,16 @@ import type { ObjectValueCollectors, PhoneNumberInputValue, } from './collector.types.js'; -import type { InitFlow, NodeStates, Updater, Validator } from './client.types.js'; +import type { + InitFlow, + InternalErrorResponse, + NodeStates, + Updater, + Validator, +} from './client.types.js'; import { returnValidator } from './collector.utils.js'; -import { authorize } from './davinci.utils.js'; import { StartNode } from './node.types.js'; +import { returnRedirectUrlForSocialLogin } from './davinci.utils.js'; /** * Create a client function that returns a set of methods @@ -108,12 +114,18 @@ export async function davinci({ * @param collector IdpCollector * @returns {function} */ - externalIdp: (collector: IdpCollector) => { + externalIdp: (collector: IdpCollector): (() => string | InternalErrorResponse) => { const rootState: RootState = store.getState(); const serverSlice = nodeSlice.selectors.selectServer(rootState); - return () => authorize(serverSlice, collector, log); + return () => { + const result = returnRedirectUrlForSocialLogin(serverSlice, collector, log); + if (typeof result == 'string') { + return result; + } + return result; + }; }, /** diff --git a/packages/davinci-client/src/lib/davinci.utils.ts b/packages/davinci-client/src/lib/davinci.utils.ts index 9695e8e624..ec895a78e3 100644 --- a/packages/davinci-client/src/lib/davinci.utils.ts +++ b/packages/davinci-client/src/lib/davinci.utils.ts @@ -246,39 +246,43 @@ export function handleResponse( } } -export function authorize( +export function returnRedirectUrlForSocialLogin( serverSlice: RootState['node']['server'], collector: IdpCollector, logger: ReturnType, -): InternalErrorResponse | void { +): InternalErrorResponse | string { if (serverSlice && '_links' in serverSlice) { const continueUrl = serverSlice._links?.['continue']?.href ?? null; if (continueUrl) { window.localStorage.setItem('continueUrl', continueUrl); if (collector.output.url) { - window.location.assign(collector.output.url); + return collector.output.url; + } else { + logger.error('No url found in collector, social login needs a url in the collector'); + return { + error: { + message: + 'No url found in collector, social login needs a url in the collector to navigate to', + type: 'network_error', + }, + type: 'internal_error', + }; } - } else { - logger.error('No url found in collector, social login needs a url in the collector'); - return { - error: { - message: - 'No url found in collector, social login needs a url in the collector to navigate to', - type: 'network_error', - }, - type: 'internal_error', - }; } - logger.error( - 'No Continue Url found, social login needs a continue url to be saved in localStorage', - ); - return { - error: { - message: - 'No Continue Url found, social login needs a continue url to be saved in localStorage', - type: 'network_error', - }, - type: 'internal_error', - }; } + /** + * If we have no continue url + * we have to return an error + **/ + logger.error( + 'No Continue Url found, social login needs a continue url to be saved in localStorage', + ); + return { + error: { + message: + 'No Continue Url found, social login needs a continue url to be saved in localStorage', + type: 'network_error', + }, + type: 'internal_error', + }; } From 8f642a8e4832f002804eca9eaf1dcd0ce407d9cd Mon Sep 17 00:00:00 2001 From: ryanbas21 Date: Tue, 10 Jun 2025 14:16:25 -0600 Subject: [PATCH 2/3] chore: refactor --- .../components/social-login-button.ts | 15 ++--- e2e/davinci-app/main.ts | 2 +- packages/davinci-client/package.json | 1 + .../davinci-client/src/lib/client.store.ts | 60 ++++++++++++++----- .../davinci-client/src/lib/davinci.api.ts | 18 +++--- .../davinci-client/src/lib/davinci.utils.ts | 47 +-------------- packages/davinci-client/tsconfig.json | 5 +- packages/davinci-client/tsconfig.lib.json | 5 +- pnpm-lock.yaml | 3 + 9 files changed, 74 insertions(+), 82 deletions(-) diff --git a/e2e/davinci-app/components/social-login-button.ts b/e2e/davinci-app/components/social-login-button.ts index 298f73b1a0..a7a461efc5 100644 --- a/e2e/davinci-app/components/social-login-button.ts +++ b/e2e/davinci-app/components/social-login-button.ts @@ -10,21 +10,18 @@ import { InternalErrorResponse } from 'packages/davinci-client/src/lib/client.ty export default function submitButtonComponent( formEl: HTMLFormElement, collector: IdpCollector, - updater: () => string | InternalErrorResponse, + updater: () => Promise, ) { const button = document.createElement('button'); console.log('collector', collector); button.value = collector.output.label; button.innerHTML = collector.output.label; - button.onclick = () => { - const url = updater(); - if (typeof url === 'string') { - window.location.assign(url); + button.onclick = async () => { + await updater(); + if ('url' in collector.output && typeof collector.output.url === 'string') { + window.location.assign(collector.output.url); } else { - /** - * this is an error now - **/ - console.error(url); + console.error('no url to continue from'); } }; diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts index 580cb8d410..4c05d65233 100644 --- a/e2e/davinci-app/main.ts +++ b/e2e/davinci-app/main.ts @@ -239,7 +239,7 @@ const urlParams = new URLSearchParams(window.location.search); ); } else if (collector.type === 'IdpCollector') { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - socialLoginButtonComponent(formEl, collector, davinciClient.externalIdp(collector)); + socialLoginButtonComponent(formEl, collector, davinciClient.externalIdp()); } else if (collector.type === 'FlowCollector') { flowLinkComponent( formEl, // You can ignore this; it's just for rendering diff --git a/packages/davinci-client/package.json b/packages/davinci-client/package.json index 3ae592c595..fc1ef65d6d 100644 --- a/packages/davinci-client/package.json +++ b/packages/davinci-client/package.json @@ -27,6 +27,7 @@ "@forgerock/sdk-oidc": "workspace:*", "@forgerock/sdk-request-middleware": "workspace:*", "@forgerock/sdk-types": "workspace:*", + "@forgerock/storage": "workspace:*", "@reduxjs/toolkit": "catalog:", "immer": "catalog:" }, diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index 0c6c9ae058..dbec879f68 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -8,6 +8,7 @@ * Import RTK slices and api */ import { CustomLogger, logger as loggerFn, LogLevel } from '@forgerock/sdk-logger'; +import { createStorage } from '@forgerock/storage'; import { createClientStore, handleUpdateValidateError, RootState } from './client.store.utils.js'; import { nodeSlice } from './node.slice.js'; @@ -28,7 +29,6 @@ import type { } from './davinci.types.js'; import type { SingleValueCollectors, - IdpCollector, MultiSelectCollector, ObjectValueCollectors, PhoneNumberInputValue, @@ -41,8 +41,7 @@ import type { Validator, } from './client.types.js'; import { returnValidator } from './collector.utils.js'; -import { StartNode } from './node.types.js'; -import { returnRedirectUrlForSocialLogin } from './davinci.utils.js'; +import { ContinueNode, StartNode } from './node.types.js'; /** * Create a client function that returns a set of methods @@ -66,7 +65,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' }, + 'socialLoginUrl', + ); if (!config.serverConfig.wellknown) { const error = new Error( '`wellknown` property is a required as part of the `config.serverConfig`', @@ -114,17 +116,23 @@ export async function davinci({ * @param collector IdpCollector * @returns {function} */ - externalIdp: (collector: IdpCollector): (() => string | InternalErrorResponse) => { + externalIdp: (): (() => Promise) => { const rootState: RootState = store.getState(); - const serverSlice = nodeSlice.selectors.selectServer(rootState); - return () => { - const result = returnRedirectUrlForSocialLogin(serverSlice, collector, log); - if (typeof result == 'string') { - return result; - } - return result; + if (serverSlice && serverSlice.status === 'continue') { + return async () => { + await serverInfo.set(serverSlice); + }; + } + return async () => { + return { + error: { + message: + 'Not in a continue node state, must be in a continue node to use external idp method', + type: 'state_error', + }, + } as InternalErrorResponse; }; }, @@ -178,12 +186,32 @@ export async function davinci({ * @method: resume - Resume a social login flow when returned to application * @returns unknown */ - resume: async ({ continueToken }: { continueToken: string }) => { - await store.dispatch(davinciApi.endpoints.resume.initiate({ continueToken })); + resume: async ({ + continueToken, + }: { + continueToken: string; + }): Promise => { + try { + const storedServerInfo = (await serverInfo.get()) as ContinueNode['server']; + await store.dispatch( + davinciApi.endpoints.resume.initiate({ continueToken, serverInfo: storedServerInfo }), + ); + await serverInfo.remove(); - const node = nodeSlice.selectSlice(store.getState()); + const node = nodeSlice.selectSlice(store.getState()); - return node; + return node; + } catch { + // logger.error('No url found in collector, social login needs a url in the collector'); + return { + error: { + message: + 'No url found in storage, social login needs a continue url which is saved in local storage. You may have cleared your browser data', + type: 'internal_error', + }, + type: 'internal_error', + }; + } }, /** diff --git a/packages/davinci-client/src/lib/davinci.api.ts b/packages/davinci-client/src/lib/davinci.api.ts index bcce8dc2ec..f259a6bfed 100644 --- a/packages/davinci-client/src/lib/davinci.api.ts +++ b/packages/davinci-client/src/lib/davinci.api.ts @@ -347,10 +347,10 @@ export const davinciApi = createApi({ handleResponse(cacheEntry, api.dispatch, response?.status || 0, logger); }, }), - resume: builder.query({ - async queryFn({ continueToken }, api, _c, baseQuery) { - const continueUrl = window.localStorage.getItem('continueUrl') || null; + resume: builder.query({ + async queryFn({ serverInfo, continueToken }, api, _c, baseQuery) { const { requestMiddleware, logger } = api.extra as Extras; + const links = serverInfo._links; if (!continueToken) { return { @@ -362,7 +362,12 @@ export const davinciApi = createApi({ }, }; } - if (!continueUrl) { + if ( + !links || + !('continue' in links) || + !('href' in links['continue']) || + !links['continue'].href + ) { return { error: { data: 'No continue url', @@ -373,10 +378,7 @@ export const davinciApi = createApi({ }; } - if (continueUrl) { - window.localStorage.removeItem('continueUrl'); - } - + const continueUrl = links['continue'].href; const request: FetchArgs = { url: continueUrl, credentials: 'include', diff --git a/packages/davinci-client/src/lib/davinci.utils.ts b/packages/davinci-client/src/lib/davinci.utils.ts index ec895a78e3..27f4891578 100644 --- a/packages/davinci-client/src/lib/davinci.utils.ts +++ b/packages/davinci-client/src/lib/davinci.utils.ts @@ -11,8 +11,6 @@ import type { Dispatch } from '@reduxjs/toolkit'; import { logger as loggerFn } from '@forgerock/sdk-logger'; -import type { RootState } from './client.store.utils.js'; - import { nodeSlice } from './node.slice.js'; import type { @@ -24,9 +22,7 @@ import type { DaVinciSuccessResponse, } from './davinci.types.js'; import type { ContinueNode } from './node.types.js'; -import { DeviceValue, IdpCollector, PhoneNumberInputValue } from './collector.types.js'; -import { InternalErrorResponse } from './client.types.js'; - +import { DeviceValue, PhoneNumberInputValue } from './collector.types.js'; /** * @function transformSubmitRequest - Transforms a NextNode into a DaVinciRequest for form submissions * @param {ContinueNode} node - The node to transform into a DaVinciRequest @@ -245,44 +241,3 @@ export function handleResponse( } } } - -export function returnRedirectUrlForSocialLogin( - serverSlice: RootState['node']['server'], - collector: IdpCollector, - logger: ReturnType, -): InternalErrorResponse | string { - if (serverSlice && '_links' in serverSlice) { - const continueUrl = serverSlice._links?.['continue']?.href ?? null; - if (continueUrl) { - window.localStorage.setItem('continueUrl', continueUrl); - if (collector.output.url) { - return collector.output.url; - } else { - logger.error('No url found in collector, social login needs a url in the collector'); - return { - error: { - message: - 'No url found in collector, social login needs a url in the collector to navigate to', - type: 'network_error', - }, - type: 'internal_error', - }; - } - } - } - /** - * If we have no continue url - * we have to return an error - **/ - logger.error( - 'No Continue Url found, social login needs a continue url to be saved in localStorage', - ); - return { - error: { - message: - 'No Continue Url found, social login needs a continue url to be saved in localStorage', - type: 'network_error', - }, - type: 'internal_error', - }; -} diff --git a/packages/davinci-client/tsconfig.json b/packages/davinci-client/tsconfig.json index 8bf1ebed66..906cfbf878 100644 --- a/packages/davinci-client/tsconfig.json +++ b/packages/davinci-client/tsconfig.json @@ -11,7 +11,7 @@ }, "references": [ { - "path": "../sdk-effects/logger" + "path": "../sdk-effects/storage" }, { "path": "../sdk-types" @@ -22,6 +22,9 @@ { "path": "../sdk-effects/oidc" }, + { + "path": "../sdk-effects/logger" + }, { "path": "./tsconfig.lib.json" }, diff --git a/packages/davinci-client/tsconfig.lib.json b/packages/davinci-client/tsconfig.lib.json index 38959c91ee..ba359f9877 100644 --- a/packages/davinci-client/tsconfig.lib.json +++ b/packages/davinci-client/tsconfig.lib.json @@ -32,7 +32,7 @@ ], "references": [ { - "path": "../sdk-effects/logger/tsconfig.lib.json" + "path": "../sdk-effects/storage/tsconfig.lib.json" }, { "path": "../sdk-types/tsconfig.lib.json" @@ -42,6 +42,9 @@ }, { "path": "../sdk-effects/oidc/tsconfig.lib.json" + }, + { + "path": "../sdk-effects/logger/tsconfig.lib.json" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c2226da88..08ac9357a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,6 +303,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 From 609d381e0eeb5ece7742f22ac60ea6ad187211e3 Mon Sep 17 00:00:00 2001 From: Justin Lowery Date: Tue, 10 Jun 2025 17:49:42 -0500 Subject: [PATCH 3/3] update storage name to serverInfo --- packages/davinci-client/src/lib/client.store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index dbec879f68..39b3f03192 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -67,7 +67,7 @@ export async function davinci({ const store = createClientStore({ requestMiddleware, logger: log }); const serverInfo = createStorage( { storeType: 'localStorage' }, - 'socialLoginUrl', + 'serverInfo', ); if (!config.serverConfig.wellknown) { const error = new Error(