From 9962e1975a3cd9f8bafaa3308644133026b9b2e2 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Fri, 21 Nov 2025 11:50:27 +0530 Subject: [PATCH 1/2] feat: add getSSOCredentials method for Native to Web SSO support --- .../java/com/auth0/react/A0Auth0Module.kt | 27 ++ .../oldarch/com/auth0/react/A0Auth0Spec.kt | 4 + ios/A0Auth0.mm | 7 + ios/NativeBridge.swift | 29 ++ src/core/interfaces/ICredentialsManager.ts | 43 ++- src/hooks/Auth0Context.ts | 47 ++++ src/hooks/Auth0Provider.tsx | 21 ++ src/hooks/__tests__/Auth0Provider.spec.tsx | 251 ++++++++++++++++++ .../adapters/NativeCredentialsManager.ts | 9 +- .../NativeCredentialsManager.spec.ts | 110 ++++++++ src/platforms/native/bridge/INativeBridge.ts | 25 ++ .../native/bridge/NativeBridgeManager.ts | 14 + .../web/adapters/WebCredentialsManager.ts | 14 +- src/specs/NativeA0Auth0.ts | 14 + src/types/common.ts | 30 +++ 15 files changed, 642 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index 79f2dde7..c8c8da2f 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -391,6 +391,33 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } } + @ReactMethod + override fun getSSOCredentials(parameters: ReadableMap?, headers: ReadableMap?, promise: Promise) { + val params = parameters?.toHashMap() ?: emptyMap() + val headerMap = headers?.toHashMap() ?: emptyMap() + + secureCredentialsManager.getSSOCredentials( + params as Map, + headerMap as Map, + object : com.auth0.android.callback.Callback { + override fun onSuccess(result: com.auth0.android.result.SessionTransferCredentials) { + val map = WritableNativeMap().apply { + putString("sessionTransferToken", result.sessionTransferToken) + putString("tokenType", result.tokenType) + putInt("expiresIn", result.expiresIn) + result.idToken?.let { putString("idToken", it) } + result.refreshToken?.let { putString("refreshToken", it) } + } + promise.resolve(map) + } + + override fun onFailure(error: CredentialsManagerException) { + handleCredentialsManagerError(error, promise) + } + } + ) + } + override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) { // No-op } diff --git a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt index 58c764ae..8df6d0a6 100644 --- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt +++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt @@ -90,4 +90,8 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ @ReactMethod @DoNotStrip abstract fun clearDPoPKey(promise: Promise) + + @ReactMethod + @DoNotStrip + abstract fun getSSOCredentials(parameters: ReadableMap?, headers: ReadableMap?, promise: Promise) } \ No newline at end of file diff --git a/ios/A0Auth0.mm b/ios/A0Auth0.mm index 7276eb58..35d7de6a 100644 --- a/ios/A0Auth0.mm +++ b/ios/A0Auth0.mm @@ -144,6 +144,13 @@ - (dispatch_queue_t)methodQueue [self.nativeBridge getDPoPHeadersWithUrl:url method:method accessToken:accessToken tokenType:tokenType nonce:nonce resolve:resolve reject:reject]; } +RCT_EXPORT_METHOD(getSSOCredentials:(NSDictionary *)parameters + headers:(NSDictionary *)headers + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self.nativeBridge getSSOCredentialsWithParameters:parameters headers:headers resolve:resolve reject:reject]; +} + diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift index 8b142b2e..9c640bdf 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -244,6 +244,35 @@ public class NativeBridge: NSObject { resolve(removed) } + @objc public func getSSOCredentials(parameters: [String: Any], headers: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + credentialsManager.ssoCredentials(parameters: parameters, headers: headers) { result in + switch result { + case .success(let ssoCredentials): + var response: [String: Any] = [ + "sessionTransferToken": ssoCredentials.sessionTransferToken, + "tokenType": ssoCredentials.tokenType, + "expiresIn": ssoCredentials.expiresIn + ] + + // Add optional fields if present + if let idToken = ssoCredentials.idToken { + response["idToken"] = idToken + } + if let refreshToken = ssoCredentials.refreshToken { + response["refreshToken"] = refreshToken + } + + resolve(response) + case .failure(let error): + reject( + NativeBridge.credentialsManagerErrorCode, + error.localizedDescription, + error + ) + } + } + } + @objc public func getDPoPHeaders(url: String, method: String, accessToken: String, tokenType: String, nonce: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { // Validate parameters guard !url.isEmpty else { diff --git a/src/core/interfaces/ICredentialsManager.ts b/src/core/interfaces/ICredentialsManager.ts index dcdcdccc..69306082 100644 --- a/src/core/interfaces/ICredentialsManager.ts +++ b/src/core/interfaces/ICredentialsManager.ts @@ -1,4 +1,4 @@ -import type { Credentials } from '../../types'; +import type { Credentials, SessionTransferCredentials } from '../../types'; /** * Defines the contract for securely managing user credentials on the device. @@ -48,4 +48,45 @@ export interface ICredentialsManager { * @returns A promise that resolves when the credentials have been cleared. */ clearCredentials(): Promise; + + /** + * Obtains session transfer credentials for performing Native to Web SSO. + * + * @remarks + * This method exchanges the stored refresh token for a session transfer token + * that can be used to authenticate in web contexts without requiring the user + * to log in again. The session transfer token can be passed as a cookie or + * query parameter to the `/authorize` endpoint to establish a web session. + * + * Session transfer tokens are short-lived and expire after a few minutes. + * Once expired, they can no longer be used for web SSO. + * + * If Refresh Token Rotation is enabled, this method will also update the stored + * credentials with new tokens (ID token and refresh token) returned from the + * token exchange. + * + * @param parameters Optional additional parameters to pass to the token exchange. + * @param headers Optional additional headers to include in the token exchange request. + * @returns A promise that resolves with the session transfer credentials. + * + * @example + * ```typescript + * // Get session transfer credentials + * const ssoCredentials = await auth0.credentialsManager.getSSOCredentials(); + * + * // Option 1: Use as a cookie + * const cookie = `auth0_session_transfer_token=${ssoCredentials.sessionTransferToken}; path=/; domain=.yourdomain.com; secure; httponly`; + * document.cookie = cookie; + * + * // Option 2: Use as a query parameter + * const authorizeUrl = `https://${domain}/authorize?session_transfer_token=${ssoCredentials.sessionTransferToken}&...`; + * window.location.href = authorizeUrl; + * ``` + * + * @see https://auth0.com/docs/authenticate/login/configure-silent-authentication + */ + getSSOCredentials( + parameters?: Record, + headers?: Record + ): Promise; } diff --git a/src/hooks/Auth0Context.ts b/src/hooks/Auth0Context.ts index 44140373..c95c0cca 100644 --- a/src/hooks/Auth0Context.ts +++ b/src/hooks/Auth0Context.ts @@ -20,6 +20,7 @@ import type { ResetPasswordParameters, MfaChallengeResponse, DPoPHeadersParams, + SessionTransferCredentials, } from '../types'; import type { NativeAuthorizeOptions, @@ -251,6 +252,51 @@ export interface Auth0ContextInterface extends AuthState { getDPoPHeaders: ( params: DPoPHeadersParams ) => Promise>; + + /** + * Obtains session transfer credentials for performing Native to Web SSO. + * + * @remarks + * This method exchanges the stored refresh token for a session transfer token + * that can be used to authenticate in web contexts without requiring the user + * to log in again. The session transfer token can be passed as a cookie or + * query parameter to the `/authorize` endpoint to establish a web session. + * + * Session transfer tokens are short-lived and expire after a few minutes. + * Once expired, they can no longer be used for web SSO. + * + * If Refresh Token Rotation is enabled, this method will also update the stored + * credentials with new tokens (ID token and refresh token) returned from the + * token exchange. + * + * **Platform specific:** This method is only available on native platforms (iOS/Android). + * On web, it will throw an error. + * + * @param parameters Optional additional parameters to pass to the token exchange. + * @param headers Optional additional headers to include in the token exchange request. + * @returns A promise that resolves with the session transfer credentials. + * + * @example + * ```typescript + * // Get session transfer credentials + * const ssoCredentials = await getSSOCredentials(); + * + * // Option 1: Use as a cookie (recommended) + * const cookie = `auth0_session_transfer_token=${ssoCredentials.sessionTransferToken}; path=/; domain=.yourdomain.com; secure; httponly`; + * document.cookie = cookie; + * window.location.href = `https://yourdomain.com/authorize?client_id=${clientId}&...`; + * + * // Option 2: Use as a query parameter + * const authorizeUrl = `https://yourdomain.com/authorize?session_transfer_token=${ssoCredentials.sessionTransferToken}&client_id=${clientId}&...`; + * window.location.href = authorizeUrl; + * ``` + * + * @see https://auth0.com/docs/authenticate/login/configure-silent-authentication + */ + getSSOCredentials: ( + parameters?: Record, + headers?: Record + ) => Promise; } const stub = (): any => { @@ -283,6 +329,7 @@ const initialContext: Auth0ContextInterface = { resetPassword: stub, revokeRefreshToken: stub, getDPoPHeaders: stub, + getSSOCredentials: stub, }; export const Auth0Context = diff --git a/src/hooks/Auth0Provider.tsx b/src/hooks/Auth0Provider.tsx index 889efa4d..81b74d4e 100644 --- a/src/hooks/Auth0Provider.tsx +++ b/src/hooks/Auth0Provider.tsx @@ -213,6 +213,25 @@ export const Auth0Provider = ({ } }, [client]); + const getSSOCredentials = useCallback( + async ( + parameters?: Record, + headers?: Record + ) => { + try { + return await client.credentialsManager.getSSOCredentials( + parameters, + headers + ); + } catch (e) { + const error = e as AuthError; + dispatch({ type: 'ERROR', error }); + throw error; + } + }, + [client] + ); + const cancelWebAuth = useCallback( () => voidFlow(client.webAuth.cancelWebAuth()), [client, voidFlow] @@ -339,6 +358,7 @@ export const Auth0Provider = ({ getCredentials, hasValidCredentials, clearCredentials, + getSSOCredentials, cancelWebAuth, loginWithPasswordRealm, createUser, @@ -364,6 +384,7 @@ export const Auth0Provider = ({ getCredentials, hasValidCredentials, clearCredentials, + getSSOCredentials, cancelWebAuth, loginWithPasswordRealm, createUser, diff --git a/src/hooks/__tests__/Auth0Provider.spec.tsx b/src/hooks/__tests__/Auth0Provider.spec.tsx index 3e32f43d..b7828268 100644 --- a/src/hooks/__tests__/Auth0Provider.spec.tsx +++ b/src/hooks/__tests__/Auth0Provider.spec.tsx @@ -99,6 +99,7 @@ const createMockClient = () => { getCredentials: jest.fn().mockResolvedValue(null), clearCredentials: jest.fn().mockResolvedValue(undefined), saveCredentials: jest.fn().mockResolvedValue(undefined), + getSSOCredentials: jest.fn().mockResolvedValue(null), }, auth: { loginWithPasswordRealm: jest.fn().mockResolvedValue(mockCredentials), @@ -808,6 +809,256 @@ describe('Auth0Provider', () => { }); }); + describe('getSSOCredentials', () => { + const TestGetSSOCredentialsConsumer = () => { + const { getSSOCredentials, error, isLoading } = useAuth0(); + const [ssoCredentials, setSSOCredentials] = React.useState(null); + + const handleGetSSOCredentials = async () => { + try { + const credentials = await getSSOCredentials(); + setSSOCredentials(credentials); + } catch { + // Error will be dispatched to state + } + }; + + if (isLoading) { + return Loading...; + } + + if (error) { + return Error: {error.message}; + } + + return ( + +