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 (
+
+
+ {ssoCredentials && (
+
+ Token: {ssoCredentials.sessionTransferToken}
+
+ )}
+
+ );
+ };
+
+ it('should get SSO credentials successfully', async () => {
+ const mockSSOCredentials = {
+ sessionTransferToken: 'stt_xyz123',
+ tokenType: 'Bearer',
+ expiresIn: 3600,
+ idToken: 'id_token_123',
+ refreshToken: 'refresh_token_789',
+ };
+
+ mockClientInstance.credentialsManager.getSSOCredentials = jest
+ .fn()
+ .mockResolvedValueOnce(mockSSOCredentials);
+
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ const getButton = screen.getByTestId('get-sso-credentials-button');
+ await act(async () => {
+ fireEvent.click(getButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('sso-credentials')).toHaveTextContent(
+ 'Token: stt_xyz123'
+ );
+ });
+
+ expect(
+ mockClientInstance.credentialsManager.getSSOCredentials
+ ).toHaveBeenCalledTimes(1);
+ });
+
+ it('should get SSO credentials with parameters', async () => {
+ const TestGetSSOCredentialsWithParamsConsumer = () => {
+ const { getSSOCredentials, error } = useAuth0();
+
+ const handleGetSSOCredentialsWithParams = () => {
+ const parameters = { audience: 'https://api.example.com' };
+ getSSOCredentials(parameters).catch(() => {});
+ };
+
+ if (error) {
+ return Error: {error.message};
+ }
+
+ return (
+
+ );
+ };
+
+ const mockSSOCredentials = {
+ sessionTransferToken: 'stt_xyz123',
+ tokenType: 'Bearer',
+ expiresIn: 3600,
+ };
+
+ mockClientInstance.credentialsManager.getSSOCredentials = jest
+ .fn()
+ .mockResolvedValueOnce(mockSSOCredentials);
+
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ const getButton = screen.getByTestId(
+ 'get-sso-credentials-with-params-button'
+ );
+ await act(async () => {
+ fireEvent.click(getButton);
+ });
+
+ expect(
+ mockClientInstance.credentialsManager.getSSOCredentials
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ mockClientInstance.credentialsManager.getSSOCredentials
+ ).toHaveBeenCalledWith(
+ { audience: 'https://api.example.com' },
+ undefined
+ );
+ });
+
+ it('should get SSO credentials with headers', async () => {
+ const TestGetSSOCredentialsWithHeadersConsumer = () => {
+ const { getSSOCredentials } = useAuth0();
+
+ const handleGetSSOCredentialsWithHeaders = () => {
+ const headers = { 'X-Custom-Header': 'value' };
+ getSSOCredentials(undefined, headers).catch(() => {});
+ };
+
+ return (
+
+ );
+ };
+
+ const mockSSOCredentials = {
+ sessionTransferToken: 'stt_xyz123',
+ tokenType: 'Bearer',
+ expiresIn: 3600,
+ };
+
+ mockClientInstance.credentialsManager.getSSOCredentials = jest
+ .fn()
+ .mockResolvedValueOnce(mockSSOCredentials);
+
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ const getButton = screen.getByTestId(
+ 'get-sso-credentials-with-headers-button'
+ );
+ await act(async () => {
+ fireEvent.click(getButton);
+ });
+
+ expect(
+ mockClientInstance.credentialsManager.getSSOCredentials
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ mockClientInstance.credentialsManager.getSSOCredentials
+ ).toHaveBeenCalledWith(undefined, { 'X-Custom-Header': 'value' });
+ });
+
+ it('should handle getSSOCredentials error and dispatch to state', async () => {
+ const ssoError = new Error('No valid credentials stored');
+ mockClientInstance.credentialsManager.getSSOCredentials = jest
+ .fn()
+ .mockRejectedValueOnce(ssoError);
+
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ const getButton = screen.getByTestId('get-sso-credentials-button');
+ await act(async () => {
+ fireEvent.click(getButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('error')).toHaveTextContent(
+ 'Error: No valid credentials stored'
+ );
+ });
+
+ expect(
+ mockClientInstance.credentialsManager.getSSOCredentials
+ ).toHaveBeenCalledTimes(1);
+ });
+
+ it('should get minimal SSO credentials without optional tokens', async () => {
+ const minimalSSOCredentials = {
+ sessionTransferToken: 'stt_minimal',
+ tokenType: 'Bearer',
+ expiresIn: 1800,
+ };
+
+ mockClientInstance.credentialsManager.getSSOCredentials = jest
+ .fn()
+ .mockResolvedValueOnce(minimalSSOCredentials);
+
+ await act(async () => {
+ render(
+
+
+
+ );
+ });
+
+ const getButton = screen.getByTestId('get-sso-credentials-button');
+ await act(async () => {
+ fireEvent.click(getButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('sso-credentials')).toHaveTextContent(
+ 'Token: stt_minimal'
+ );
+ });
+
+ expect(
+ mockClientInstance.credentialsManager.getSSOCredentials
+ ).toHaveBeenCalledTimes(1);
+ });
+ });
+
// Web Platform Method Tests
describe('Web Platform Methods', () => {
it('should verify webAuth methods exist and are callable', () => {
diff --git a/src/platforms/native/adapters/NativeCredentialsManager.ts b/src/platforms/native/adapters/NativeCredentialsManager.ts
index 82c4f3bc..60feecc0 100644
--- a/src/platforms/native/adapters/NativeCredentialsManager.ts
+++ b/src/platforms/native/adapters/NativeCredentialsManager.ts
@@ -4,6 +4,7 @@ import { CredentialsManagerError } from '../../../core/models';
import type {
ApiCredentials as IApiCredentials,
Credentials,
+ SessionTransferCredentials,
} from '../../../types';
import type { INativeBridge } from '../bridge';
@@ -72,4 +73,11 @@ export class NativeCredentialsManager implements ICredentialsManager {
clearApiCredentials(audience: string): Promise {
return this.handleError(this.bridge.clearApiCredentials(audience));
}
+
+ getSSOCredentials(
+ parameters?: Record,
+ headers?: Record
+ ): Promise {
+ return this.handleError(this.bridge.getSSOCredentials(parameters, headers));
+ }
}
diff --git a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts
index d223cfbb..53d04145 100644
--- a/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts
+++ b/src/platforms/native/adapters/__tests__/NativeCredentialsManager.spec.ts
@@ -10,6 +10,7 @@ const mockBridge: jest.Mocked = {
hasValidCredentials: jest.fn(),
clearCredentials: jest.fn(),
clearDPoPKey: jest.fn(),
+ getSSOCredentials: jest.fn(),
getApiCredentials: jest.fn(),
clearApiCredentials: jest.fn(),
// Add stubs for other INativeBridge methods to satisfy the type.
@@ -135,6 +136,114 @@ describe('NativeCredentialsManager', () => {
});
});
+ describe('getSSOCredentials', () => {
+ const validSSOCredentials = {
+ sessionTransferToken: 'stt_xyz123',
+ tokenType: 'Bearer',
+ expiresIn: 3600,
+ idToken: 'id_token_123',
+ refreshToken: 'refresh_token_789',
+ };
+
+ it('should call the bridge to get SSO credentials without parameters', async () => {
+ mockBridge.getSSOCredentials.mockResolvedValueOnce(validSSOCredentials);
+
+ const result = await manager.getSSOCredentials();
+
+ expect(mockBridge.getSSOCredentials).toHaveBeenCalledTimes(1);
+ expect(mockBridge.getSSOCredentials).toHaveBeenCalledWith(
+ undefined,
+ undefined
+ );
+ expect(result).toEqual(validSSOCredentials);
+ });
+
+ it('should call the bridge to get SSO credentials with parameters', async () => {
+ const parameters = { audience: 'https://api.example.com' };
+ mockBridge.getSSOCredentials.mockResolvedValueOnce(validSSOCredentials);
+
+ const result = await manager.getSSOCredentials(parameters);
+
+ expect(mockBridge.getSSOCredentials).toHaveBeenCalledTimes(1);
+ expect(mockBridge.getSSOCredentials).toHaveBeenCalledWith(
+ parameters,
+ undefined
+ );
+ expect(result).toEqual(validSSOCredentials);
+ });
+
+ it('should call the bridge to get SSO credentials with headers', async () => {
+ const headers = { 'X-Custom-Header': 'value' };
+ mockBridge.getSSOCredentials.mockResolvedValueOnce(validSSOCredentials);
+
+ const result = await manager.getSSOCredentials(undefined, headers);
+
+ expect(mockBridge.getSSOCredentials).toHaveBeenCalledTimes(1);
+ expect(mockBridge.getSSOCredentials).toHaveBeenCalledWith(
+ undefined,
+ headers
+ );
+ expect(result).toEqual(validSSOCredentials);
+ });
+
+ it('should call the bridge to get SSO credentials with both parameters and headers', async () => {
+ const parameters = {
+ audience: 'https://api.example.com',
+ scope: 'openid profile',
+ };
+ const headers = { 'X-Custom-Header': 'value' };
+ mockBridge.getSSOCredentials.mockResolvedValueOnce(validSSOCredentials);
+
+ const result = await manager.getSSOCredentials(parameters, headers);
+
+ expect(mockBridge.getSSOCredentials).toHaveBeenCalledTimes(1);
+ expect(mockBridge.getSSOCredentials).toHaveBeenCalledWith(
+ parameters,
+ headers
+ );
+ expect(result).toEqual(validSSOCredentials);
+ });
+
+ it('should return SSO credentials without optional tokens', async () => {
+ const minimalSSOCredentials = {
+ sessionTransferToken: 'stt_xyz123',
+ tokenType: 'Bearer',
+ expiresIn: 3600,
+ };
+ mockBridge.getSSOCredentials.mockResolvedValueOnce(minimalSSOCredentials);
+
+ const result = await manager.getSSOCredentials();
+
+ expect(result).toEqual(minimalSSOCredentials);
+ expect(result.idToken).toBeUndefined();
+ expect(result.refreshToken).toBeUndefined();
+ });
+
+ it('should propagate errors from the bridge', async () => {
+ const ssoError = new Error(
+ 'Failed to get SSO credentials from native SDK.'
+ );
+ mockBridge.getSSOCredentials.mockRejectedValueOnce(ssoError);
+
+ await expect(manager.getSSOCredentials()).rejects.toThrow(ssoError);
+ });
+
+ it('should handle authentication errors', async () => {
+ const authError = new Error('No valid credentials stored');
+ mockBridge.getSSOCredentials.mockRejectedValueOnce(authError);
+
+ await expect(manager.getSSOCredentials()).rejects.toThrow(authError);
+ });
+
+ it('should handle network errors', async () => {
+ const networkError = new Error('Network request failed');
+ mockBridge.getSSOCredentials.mockRejectedValueOnce(networkError);
+
+ await expect(
+ manager.getSSOCredentials({ audience: 'https://api.example.com' })
+ ).rejects.toThrow(networkError);
+ });
+ });
describe('getApiCredentials', () => {
it('should throw CredentialsManagerError on NO_CREDENTIALS error', async () => {
const authError = new AuthError('NO_CREDENTIALS', 'No credentials', {
diff --git a/src/platforms/native/bridge/INativeBridge.ts b/src/platforms/native/bridge/INativeBridge.ts
index 03938500..b7cc038c 100644
--- a/src/platforms/native/bridge/INativeBridge.ts
+++ b/src/platforms/native/bridge/INativeBridge.ts
@@ -4,6 +4,7 @@ import type {
WebAuthorizeParameters,
ClearSessionParameters,
DPoPHeadersParams,
+ SessionTransferCredentials,
} from '../../../types';
import type {
LocalAuthenticationOptions,
@@ -150,4 +151,28 @@ export interface INativeBridge {
* This should be called during logout to ensure the key is removed.
*/
clearDPoPKey(): 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 is short-lived and expires after
+ * a few minutes.
+ *
+ * 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.
+ *
+ * @see https://auth0.com/docs/authenticate/single-sign-on/native-to-web/configure-implement-native-to-web
+ */
+ getSSOCredentials(
+ parameters?: Record,
+ headers?: Record
+ ): Promise;
}
diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts
index 29b2837d..5a200df4 100644
--- a/src/platforms/native/bridge/NativeBridgeManager.ts
+++ b/src/platforms/native/bridge/NativeBridgeManager.ts
@@ -6,6 +6,7 @@ import type {
ClearSessionParameters,
NativeClearSessionOptions,
DPoPHeadersParams,
+ SessionTransferCredentials,
} from '../../../types';
import {
SafariViewControllerPresentationStyle,
@@ -207,4 +208,17 @@ export class NativeBridgeManager implements INativeBridge {
async clearDPoPKey(): Promise {
return this.a0_call(Auth0NativeModule.clearDPoPKey.bind(Auth0NativeModule));
}
+
+ async getSSOCredentials(
+ parameters?: Record,
+ headers?: Record
+ ): Promise {
+ const params = parameters ?? {};
+ const hdrs = headers ?? {};
+ return this.a0_call(
+ Auth0NativeModule.getSSOCredentials.bind(Auth0NativeModule),
+ params,
+ hdrs
+ );
+ }
}
diff --git a/src/platforms/web/adapters/WebCredentialsManager.ts b/src/platforms/web/adapters/WebCredentialsManager.ts
index 126279d9..6d55e63f 100644
--- a/src/platforms/web/adapters/WebCredentialsManager.ts
+++ b/src/platforms/web/adapters/WebCredentialsManager.ts
@@ -1,5 +1,5 @@
import type { ICredentialsManager } from '../../../core/interfaces';
-import type { Credentials } from '../../../types';
+import type { Credentials, SessionTransferCredentials } from '../../../types';
import {
AuthError,
CredentialsManagerError,
@@ -110,6 +110,18 @@ export class WebCredentialsManager implements ICredentialsManager {
}
}
+ async getSSOCredentials(
+ _parameters?: Record,
+ _headers?: Record
+ ): Promise {
+ const authError = new AuthError(
+ 'UnsupportedOperation',
+ 'Native to Web SSO is only supported on native platforms (iOS/Android). This feature is not available in web environments.',
+ { code: 'unsupported_operation' }
+ );
+ throw new CredentialsManagerError(authError);
+ }
+
async clearApiCredentials(audience: string): Promise {
console.warn(
`'clearApiCredentials' for audience ${audience} is a no-op on the web. @auth0/auth0-spa-js handles credential storage automatically.`
diff --git a/src/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts
index da43d253..882fcb78 100644
--- a/src/specs/NativeA0Auth0.ts
+++ b/src/specs/NativeA0Auth0.ts
@@ -124,6 +124,20 @@ export interface Spec extends TurboModule {
* This method clears the DPoP key from the native module.
*/
clearDPoPKey(): Promise;
+
+ /**
+ * Get session transfer credentials for Native to Web SSO
+ */
+ getSSOCredentials(
+ parameters: Object,
+ headers: Object
+ ): Promise<{
+ sessionTransferToken: string;
+ tokenType: string;
+ expiresIn: Int32;
+ idToken?: string;
+ refreshToken?: string;
+ }>;
}
export default TurboModuleRegistry.getEnforcing('A0Auth0');
diff --git a/src/types/common.ts b/src/types/common.ts
index 512a0452..cd11d720 100644
--- a/src/types/common.ts
+++ b/src/types/common.ts
@@ -34,6 +34,36 @@ export type Credentials = {
[key: string]: any;
};
+/**
+ * Represents the session transfer credentials used for Native to Web SSO.
+ * These credentials are obtained by exchanging a refresh token and can be used
+ * to authenticate in web contexts without requiring the user to log in again.
+ *
+ * @remarks
+ * Session transfer tokens are short-lived and expire after a few minutes.
+ * Once expired, they can no longer be used for web SSO.
+ *
+ * @see https://auth0.com/docs/authenticate/single-sign-on/native-to-web/configure-implement-native-to-web
+ */
+export type SessionTransferCredentials = {
+ /** The session transfer token used for web SSO. */
+ sessionTransferToken: string;
+ /** The type of the token issued */
+ tokenType: string;
+ /** The expiration time of the session transfer token in seconds. */
+ expiresIn: number;
+ /**
+ * A new ID token, if one was issued during the token exchange.
+ * This is typically present when Refresh Token Rotation is enabled.
+ */
+ idToken?: string;
+ /**
+ * A new refresh token, if one was issued during the token exchange.
+ * This is present when Refresh Token Rotation is enabled.
+ */
+ refreshToken?: string;
+};
+
/**
* Represents API-specific credentials, primarily containing an access token.
* This is returned when requesting tokens for a specific API (audience).