diff --git a/EXAMPLES.md b/EXAMPLES.md index 03876d0f..5f56aa2b 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -31,6 +31,12 @@ - [Using MRRT with Hooks](#using-mrrt-with-hooks) - [Using MRRT with Auth0 Class](#using-mrrt-with-auth0-class) - [Web Platform Configuration](#web-platform-configuration) +- [Native to Web SSO (Early Access)](#native-to-web-sso-early-access) + - [Overview](#native-to-web-sso-overview) + - [Prerequisites](#native-to-web-sso-prerequisites) + - [Using Native to Web SSO with Hooks](#using-native-to-web-sso-with-hooks) + - [Using Native to Web SSO with Auth0 Class](#using-native-to-web-sso-with-auth0-class) + - [Sending the Session Transfer Token](#sending-the-session-transfer-token) - [Bot Protection](#bot-protection) - [Domain Switching](#domain-switching) - [Android](#android) @@ -432,6 +438,172 @@ function App() { } ``` +## Native to Web SSO (Early Access) + +> ⚠️ **Early Access Feature**: Native to Web SSO is currently available in Early Access. To use this feature, you must have an Enterprise plan. For more information, see [Product Release Stages](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages). + +### Native to Web SSO Overview + +Native to Web SSO allows authenticated users in your native mobile application to seamlessly transition to your web application without requiring them to log in again. This is achieved by exchanging a refresh token for a Session Transfer Token, which can then be used to establish a session in the web application. + +The Session Transfer Token is: + +- **Short-lived**: Expires after approximately 1 minute +- **Single-use**: Can only be used once to establish a web session +- **Secure**: Can be bound to the user's device through IP address or ASN + +For detailed configuration and implementation guidance, see the [Auth0 Native to Web SSO documentation](https://auth0.com/docs/authenticate/single-sign-on/native-to-web/configure-implement-native-to-web). + +### Native to Web SSO Prerequisites + +Before using Native to Web SSO: + +1. **Enable Native to Web SSO on your Auth0 tenant** - This feature requires an Enterprise plan +2. [**Configure your native application**](https://auth0.com/docs/authenticate/single-sign-on/native-to-web/configure-implement-native-to-web#configure-native-applications) +3. **Request `offline_access` scope during login** to ensure a refresh token is issued + +### Using Native to Web SSO with Hooks + +```tsx +import { useAuth0 } from 'react-native-auth0'; +import { Linking } from 'react-native'; + +function MyComponent() { + const { authorize, getSSOCredentials } = useAuth0(); + + const login = async () => { + // Login with offline_access to get a refresh token + await authorize({ + scope: 'openid profile email offline_access', + }); + }; + + const openWebApp = async () => { + try { + // Get session transfer credentials + const ssoCredentials = await getSSOCredentials(); + + console.log('Session Transfer Token:', ssoCredentials.sessionTransferToken); + console.log('Token Type:', ssoCredentials.tokenType); + console.log('Expires In:', ssoCredentials.expiresIn, 'seconds'); + + // Open web app with session transfer token as query parameter + const webAppUrl = `https://your-web-app.com/login?session_transfer_token=${ssoCredentials.sessionTransferToken}`; + await Linking.openURL(webAppUrl); + } catch (error) { + console.error('Failed to get SSO credentials:', error); + } + }; + + return ( + // Your UI components + ); +} +``` + +### Using Native to Web SSO with Auth0 Class + +```js +import Auth0 from 'react-native-auth0'; +import { Linking } from 'react-native'; + +const auth0 = new Auth0({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', +}); + +// Login with offline_access scope +await auth0.webAuth.authorize({ + scope: 'openid profile email offline_access', +}); + +// Get session transfer credentials +const ssoCredentials = await auth0.credentialsManager.getSSOCredentials(); + +console.log('Session Transfer Token:', ssoCredentials.sessionTransferToken); +console.log('Token Type:', ssoCredentials.tokenType); +console.log('Expires In:', ssoCredentials.expiresIn); + +// Optional: ID Token and Refresh Token may be returned if RTR is enabled +if (ssoCredentials.idToken) { + console.log('ID Token:', ssoCredentials.idToken); +} +if (ssoCredentials.refreshToken) { + console.log('New Refresh Token received (RTR enabled)'); +} + +// Open your web application with the session transfer token +const webAppUrl = `https://your-web-app.com/login?session_transfer_token=${ssoCredentials.sessionTransferToken}`; +await Linking.openURL(webAppUrl); +``` + +### Sending the Session Transfer Token + +There are two ways to send the Session Transfer Token to your web application: + +#### Option 1: As a Query Parameter + +Pass the token as a URL parameter when opening your web application: + +```js +const ssoCredentials = await auth0.credentialsManager.getSSOCredentials(); + +// Your web app should extract the token and pass it to Auth0's /authorize endpoint +const webAppUrl = `https://your-web-app.com/login?session_transfer_token=${ssoCredentials.sessionTransferToken}`; +await Linking.openURL(webAppUrl); +``` + +Your web application should then include the `session_transfer_token` in the `/authorize` request: + +```js +// In your web application +const urlParams = new URLSearchParams(window.location.search); +const sessionTransferToken = urlParams.get('session_transfer_token'); + +if (sessionTransferToken) { + // Include in your authorization request + const authorizeUrl = + `https://YOUR_AUTH0_DOMAIN/authorize?` + + `client_id=YOUR_WEB_CLIENT_ID&` + + `redirect_uri=${encodeURIComponent('https://your-web-app.com/callback')}&` + + `response_type=code&` + + `scope=openid profile email&` + + `session_transfer_token=${sessionTransferToken}`; + + window.location.href = authorizeUrl; +} +``` + +#### Option 2: As a Cookie (WebView only) + +If your application uses a WebView that supports cookie injection: + +```js +import { WebView } from 'react-native-webview'; + +function WebAppView() { + const [cookies, setCookies] = useState(''); + + const prepareWebSession = async () => { + const ssoCredentials = await auth0.credentialsManager.getSSOCredentials(); + + // Set cookie that will be sent to Auth0 + const cookie = `auth0_session_transfer_token=${ssoCredentials.sessionTransferToken}; path=/; domain=.your-auth0-domain.auth0.com; secure`; + setCookies(cookie); + }; + + return ( + + ); +} +``` + +> **Note**: Cookie injection is platform-specific and may require additional configuration. The query parameter method is generally more straightforward and recommended for most use cases. + ## Bot Protection If you are using the [Bot Protection](https://auth0.com/docs/anomaly-detection/bot-protection) feature and performing database login/signup via the Authentication API, you need to handle the `requires_verification` error. It indicates that the request was flagged as suspicious and an additional verification step is necessary to log the user in. That verification step is web-based, so you need to use Universal Login to complete it. diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index eed82a1e..a2c7f352 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -434,6 +434,36 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } } + @ReactMethod + override fun getSSOCredentials(parameters: ReadableMap?, headers: ReadableMap?, promise: Promise) { + val params = mutableMapOf() + parameters?.toHashMap()?.forEach { (key, value) -> + value?.let { params[key] = it.toString() } + } + // header is only for keeping the platform agnostic, only used in iOS + + secureCredentialsManager.getSsoCredentials( + params, + object : com.auth0.android.callback.Callback { + override fun onSuccess(result: com.auth0.android.result.SSOCredentials) { + 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) { + val errorCode = deduceErrorCode(error) + promise.reject(errorCode, error.message, error) + } + } + ) + } + 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 9f8ed786..5d6da063 100644 --- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt +++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt @@ -104,4 +104,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 6050dc21..1ab6952c 100644 --- a/ios/A0Auth0.mm +++ b/ios/A0Auth0.mm @@ -159,6 +159,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 f5e2f3c1..a2858c05 100644 --- a/ios/NativeBridge.swift +++ b/ios/NativeBridge.swift @@ -243,6 +243,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 65f4defc..7f17f880 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'; import { ApiCredentials } from '../models'; /** @@ -50,6 +50,47 @@ export interface ICredentialsManager { */ 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. **iOS only** - this parameter is ignored on Android. + * @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/single-sign-on/native-to-web/configure-implement-native-to-web + */ + getSSOCredentials( + parameters?: Record, + headers?: Record + ): Promise; + /** * Retrieves API-specific credentials for a given audience using the Multi-Resource Refresh Token (MRRT). * diff --git a/src/hooks/Auth0Context.ts b/src/hooks/Auth0Context.ts index 779f8712..824d894a 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 { ApiCredentials } from '../core/models'; import type { @@ -276,6 +277,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. **iOS only** - this parameter is ignored on Android. + * @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/single-sign-on/native-to-web/configure-implement-native-to-web + */ + getSSOCredentials: ( + parameters?: Record, + headers?: Record + ) => Promise; } const stub = (): any => { @@ -310,6 +356,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 eb436073..918f4848 100644 --- a/src/hooks/Auth0Provider.tsx +++ b/src/hooks/Auth0Provider.tsx @@ -214,6 +214,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] @@ -372,6 +391,7 @@ export const Auth0Provider = ({ getCredentials, hasValidCredentials, clearCredentials, + getSSOCredentials, getApiCredentials, clearApiCredentials, cancelWebAuth, @@ -399,6 +419,7 @@ export const Auth0Provider = ({ getCredentials, hasValidCredentials, clearCredentials, + getSSOCredentials, getApiCredentials, clearApiCredentials, cancelWebAuth, diff --git a/src/hooks/__tests__/Auth0Provider.spec.tsx b/src/hooks/__tests__/Auth0Provider.spec.tsx index 68ce4b36..31c60f39 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), @@ -802,6 +803,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 ( + +