diff --git a/README.md b/README.md index 1569293..208cde7 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,44 @@ Once the legacy tokens are sent to the consumer apps, the library assumes that t Note : The callback page does NOT handle authorize calls. Its sole purpose is to do the access token exchange and return back the legacy tokens to the consumer apps. +## State parameter + +You can pass in an additional state parameter for payloads to `requestOidcAuthentication` or `requestSilentOidcAuthentication`, which will be carried over to the `Callback` component. You can perform things like passing in an additional `redirect_to` metadata to inform `Callback` page where to redirect to next after authentication is completed: + +``` + requestOidcAuthentication({ + redirectCallbackUri: `${window.location.origin}/callback`, + state: { + redirect_to: '/tradershub/home' + } + }); +``` + +And within the `Callback` component, it will return the state from the `onSignInSuccess` callback function: + +``` +const CallbackPage = () => { + const { updateLoginAccounts } = useAuthContext(); + + return ( + { + const accounts = transformAccountsFromResponseBody(tokens); + + updateLoginAccounts(accounts); + + const redirectTo = (state as Record)?.redirect_to; + if (redirectTo) { + window.location.href = redirectTo; + } else { + window.location.href = '/'; + } + }} + /> + ); +}; +``` + ## Logout Flow This logout process combines two parts: clearing OAuth session cookies through the OAuth2Logout function and running custom cleanup logic specific to your app (like clearing user accounts or tokens). Let’s break it down step-by-step: diff --git a/src/components/Callback/Callback.tsx b/src/components/Callback/Callback.tsx index d3df5e9..3d6162a 100644 --- a/src/components/Callback/Callback.tsx +++ b/src/components/Callback/Callback.tsx @@ -8,9 +8,9 @@ import './Callback.scss'; type CallbackProps = { /** callback function triggerred when `requestOidcToken` is successful. Use this only when you want to request the legacy tokens yourself, otherwise pass your callback to `onSignInSuccess` prop instead */ - onRequestOidcTokenSuccess?: (accessToken: string) => void; + onRequestOidcTokenSuccess?: (accessToken: string, state: unknown) => void; /** callback function triggered when the OIDC authentication flow is successful */ - onSignInSuccess?: (tokens: LegacyTokens) => void; + onSignInSuccess?: (tokens: LegacyTokens, state: unknown) => void; /** callback function triggered when sign-in encounters an error */ onSignInError?: (error: Error) => void; /** URI to redirect to the callback page. This is where you should pass the callback page URL in your app .e.g. https://app.deriv.com/callback or https://smarttrader.deriv.com/en/callback */ @@ -62,18 +62,19 @@ export const Callback = ({ const fetchTokens = useCallback(async () => { try { - const { accessToken } = await requestOidcToken({ + const { accessToken, userManager } = await requestOidcToken({ redirectCallbackUri, postLogoutRedirectUri, }); + const user = await userManager.getUser(); if (accessToken) { - onRequestOidcTokenSuccess?.(accessToken); + onRequestOidcTokenSuccess?.(accessToken, user?.state); const legacyTokens = await requestLegacyToken(accessToken); - onSignInSuccess?.(legacyTokens); - const domains = ['deriv.com', 'binary.sx', 'pages.dev', 'localhost']; + onSignInSuccess?.(legacyTokens, user?.state); + const domains = ['deriv.com', 'deriv.dev', 'binary.sx', 'pages.dev', 'localhost']; const currentDomain = window.location.hostname.split('.').slice(-2).join('.'); if (domains.includes(currentDomain)) { Cookies.set('logged_state', 'true', { diff --git a/src/hooks/useOAuth2.ts b/src/hooks/useOAuth2.ts index eb63337..1d30d2d 100644 --- a/src/hooks/useOAuth2.ts +++ b/src/hooks/useOAuth2.ts @@ -45,7 +45,7 @@ export const useOAuth2 = (OAuth2GrowthBookConfig: OAuth2GBConfig, WSLogoutAndRed const onMessage = (event: MessageEvent) => { if (event.data === 'logout_complete') { - const domains = ['deriv.com', 'binary.sx', 'pages.dev', 'localhost']; + const domains = ['deriv.com', 'deriv.dev', 'binary.sx', 'pages.dev', 'localhost']; const currentDomain = window.location.hostname.split('.').slice(-2).join('.'); if (domains.includes(currentDomain)) { Cookies.set('logged_state', 'false', { diff --git a/src/oidc/oidc.ts b/src/oidc/oidc.ts index eb8e6a6..90ef3f8 100644 --- a/src/oidc/oidc.ts +++ b/src/oidc/oidc.ts @@ -29,11 +29,13 @@ type RequestOidcAuthenticationOptions = { redirectCallbackUri?: string; postLoginRedirectUri?: string; postLogoutRedirectUri?: string; + state?: Record; }; type requestOidcSilentAuthenticationOptions = { redirectCallbackUri?: string; redirectSilentCallbackUri: string; + state?: Record; }; type RequestOidcTokenOptions = { @@ -100,6 +102,7 @@ export const fetchOidcConfiguration = async (): Promise => { * @param options.redirectCallbackUri - The callback page URI to redirect back * @param options.postLoginRedirectUri - The URI to redirect after the callback page. This is where you usually pass the page URL where you initiated the login flow * @param options.postLogoutRedirectUri - The URI where the application should redirect after processing the logout + * * @param options.state - An optional payload you can pass to the authentication flow. This will allow OIDC to carry your state in the callback page, where you can perform additional redirection actions based on the state * * @returns Promise that resolves to an object containing the UserManager instance * @throws {OIDCError} With type AuthenticationRequestFailed if the authentication request fails @@ -110,7 +113,10 @@ export const fetchOidcConfiguration = async (): Promise => { * const { userManager } = await requestOidcAuthentication({ * redirectCallbackUri: 'https://smarttrader.deriv.com/en/callback', * postLoginRedirectUri: 'https://smarttrader.deriv.com/en/trading', - * postLogoutRedirectUri: https://smarttrader.deriv.com/en/trading'' + * postLogoutRedirectUri: https://smarttrader.deriv.com/en/trading', + * state: { + * redirect_to: '/tradershub/home' + * } * }); * } catch (error) { * // Handle authentication request error @@ -123,7 +129,7 @@ export const fetchOidcConfiguration = async (): Promise => { * - The post login/logout redirect URIs are stored in local storage as `config.post_login_redirect_uri` and `config.post_logout_redirect_uri` */ export const requestOidcAuthentication = async (options: RequestOidcAuthenticationOptions) => { - const { redirectCallbackUri, postLoginRedirectUri, postLogoutRedirectUri } = options; + const { redirectCallbackUri, postLoginRedirectUri, postLogoutRedirectUri, state } = options; // If the post login redirect URI is not specified, redirect the user back to where the OIDC authentication is initiated // This will be used later by the Callback component to redirect back to where the OIDC flow is initiated @@ -141,6 +147,7 @@ export const requestOidcAuthentication = async (options: RequestOidcAuthenticati extraQueryParams: { brand: 'deriv', }, + state, }); return { userManager }; } catch (error) { @@ -157,6 +164,7 @@ export const requestOidcAuthentication = async (options: RequestOidcAuthenticati * * @param options - Configuration options for the OIDC silent authentication request * @param options.redirectSilentCallbackUri - The silent callback page URI which will be rendered in an iframe to check login status + * @param options.state - An optional payload you can pass to the silent authentication flow. This will allow OIDC to carry your state in the callback page, where you can perform additional redirection actions based on the state * * @returns Promise that resolves to an object containing the UserManager instance * @throws {OIDCError} With type AuthenticationRequestFailed if the authentication request fails @@ -166,6 +174,9 @@ export const requestOidcAuthentication = async (options: RequestOidcAuthenticati * try { * const { userManager } = await requestOidcSilentAuthentication({ * redirectCallbackUri: 'https://smarttrader.deriv.com/en/silent-callback', + * * state: { + * redirect_to: '/tradershub/home' + * } * }); * } catch (error) { * // Handle authentication request error @@ -176,7 +187,7 @@ export const requestOidcAuthentication = async (options: RequestOidcAuthenticati * - An iframe will be generated and embedded in the page, which will send postMessage events to the parent window to indicate the login status */ export const requestOidcSilentAuthentication = async (options: requestOidcSilentAuthenticationOptions) => { - const { redirectCallbackUri, redirectSilentCallbackUri } = options; + const { redirectCallbackUri, redirectSilentCallbackUri, state } = options; try { const userManager = await createUserManager({ @@ -188,6 +199,7 @@ export const requestOidcSilentAuthentication = async (options: requestOidcSilent extraQueryParams: { brand: 'deriv', }, + state, silentRequestTimeoutInSeconds: 60000, }); return { userManager }; @@ -241,6 +253,7 @@ export const requestOidcToken = async (options: RequestOidcTokenOptions) => { return { accessToken: user?.access_token, + userManager, }; } catch (error) { console.error('unable to request access tokens: ', error); @@ -409,7 +422,7 @@ export const OAuth2Logout = async (options: OAuth2LogoutOptions) => { }; const onMessage = (event: MessageEvent) => { if (event.data === 'logout_complete') { - const domains = ['deriv.com', 'binary.sx', 'pages.dev', 'localhost']; + const domains = ['deriv.com', 'deriv.dev', 'binary.sx', 'pages.dev', 'localhost']; const currentDomain = window.location.hostname.split('.').slice(-2).join('.'); if (domains.includes(currentDomain)) { Cookies.set('logged_state', 'false', { @@ -525,7 +538,7 @@ export const handlePostLogout = (callbackFunction: () => void) => { const sessionStorageKey = `oidc.user:${serverUrl}:${appId}`; if (!window.sessionStorage.getItem(sessionStorageKey)) { - const domains = ['deriv.com', 'binary.sx', 'pages.dev', 'localhost']; + const domains = ['deriv.com', 'deriv.dev', 'binary.sx', 'pages.dev', 'localhost']; const currentDomain = window.location.hostname.split('.').slice(-2).join('.'); if (domains.includes(currentDomain)) { Cookies.set('logged_state', 'false', {