Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
- [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)
- [SSO Exchange via Authentication API](#sso-exchange-via-authentication-api)
- [Using SSO Exchange with Hooks](#using-sso-exchange-with-hooks)
- [Using SSO Exchange with Auth0 Class](#using-sso-exchange-with-auth0-class)
- [Sending the Session Transfer Token](#sending-the-session-transfer-token)
- [Bot Protection](#bot-protection)
- [Domain Switching](#domain-switching)
Expand Down Expand Up @@ -1104,6 +1107,75 @@ const webAppUrl = `https://your-web-app.com/login?session_transfer_token=${ssoCr
await Linking.openURL(webAppUrl);
```

### SSO Exchange via Authentication API

If your app manages tokens independently (without using the Credentials Manager), you can use `auth.ssoExchange()` to exchange a refresh token for a session transfer token directly via the Authentication API.

This is useful when:

- Your app stores tokens outside of the built-in Credentials Manager
- You need more control over the token exchange process
- You want to perform the exchange as a standalone API call

> **Note:** This method is only available on native platforms (iOS/Android). It is not supported on the web platform.

#### Using SSO Exchange with Hooks

```js
import { useAuth0 } from 'react-native-auth0';
import { Linking, Alert } from 'react-native';
Comment thread
subhankarmaiti marked this conversation as resolved.
Outdated

function SSOExchangeScreen() {
const { ssoExchange } = useAuth0();

const handleSSOExchange = async (refreshToken) => {
try {
const ssoCredentials = await ssoExchange({ refreshToken });

console.log('Session Transfer Token:', ssoCredentials.sessionTransferToken);
console.log('Token Type:', ssoCredentials.tokenType);
console.log('Expires In:', ssoCredentials.expiresIn);

// 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);
} catch (error) {
console.error('SSO Exchange failed:', error);
}
};

return (
// Your UI components
);
}
```

#### Using SSO Exchange 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',
});

// You must already have a refresh token (e.g., from a previous login with offline_access scope)
const refreshToken = 'YOUR_REFRESH_TOKEN';

// Exchange the refresh token for a session transfer token
const ssoCredentials = await auth0.auth.ssoExchange({ refreshToken });

console.log('Session Transfer Token:', ssoCredentials.sessionTransferToken);
console.log('Token Type:', ssoCredentials.tokenType);
console.log('Expires In:', ssoCredentials.expiresIn);

// 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:
Expand Down
6 changes: 6 additions & 0 deletions src/core/interfaces/IAuthenticationProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
Credentials,
SessionTransferCredentials,
User,
MfaChallengeResponse,
PasswordRealmParameters,
Expand All @@ -8,6 +9,7 @@ import type {
RevokeOptions,
ExchangeParameters,
ExchangeNativeSocialParameters,
SSOExchangeParameters,
PasswordlessEmailParameters,
PasswordlessSmsParameters,
LoginEmailParameters,
Expand Down Expand Up @@ -48,4 +50,8 @@ export interface IAuthenticationProvider {
exchangeNativeSocial(
parameters: ExchangeNativeSocialParameters
): Promise<Credentials>;

ssoExchange(
parameters: SSOExchangeParameters
): Promise<SessionTransferCredentials>;
}
39 changes: 39 additions & 0 deletions src/core/models/SSOCredentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {
SessionTransferCredentials,
SSOCredentialsResponse,
} from '../../types';

/**
* A class representation of SSO credentials returned by the session transfer token exchange.
* Maps the raw API response (snake_case) to the SDK's SessionTransferCredentials type (camelCase).
*/
export class SSOCredentials implements SessionTransferCredentials {
public sessionTransferToken: string;
public tokenType: string;
public expiresIn: number;
public idToken?: string;
public refreshToken?: string;

constructor(params: SessionTransferCredentials) {
this.sessionTransferToken = params.sessionTransferToken;
this.tokenType = params.tokenType;
this.expiresIn = params.expiresIn;
this.idToken = params.idToken;
this.refreshToken = params.refreshToken;
}

/**
* Creates an SSOCredentials instance from a raw /oauth/token response.
* The API returns `access_token` as the session transfer token and
* `issued_token_type` as the token type.
*/
static fromResponse(response: SSOCredentialsResponse): SSOCredentials {
return new SSOCredentials({
sessionTransferToken: response.access_token,
tokenType: response.issued_token_type,
expiresIn: response.expires_in,
idToken: response.id_token,
refreshToken: response.refresh_token,
});
}
}
75 changes: 75 additions & 0 deletions src/core/models/__tests__/SSOCredentials.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { SSOCredentials } from '../SSOCredentials';
import type { SSOCredentialsResponse } from '../../../types';

describe('SSOCredentials', () => {
describe('constructor', () => {
it('should correctly assign all properties from the input object', () => {
const data = {
sessionTransferToken: 'stt_token_123',
tokenType: 'urn:auth0:params:oauth:token-type:session_transfer_token',
expiresIn: 120,
idToken: 'id_token_456',
refreshToken: 'refresh_token_789',
};

const creds = new SSOCredentials(data);

expect(creds.sessionTransferToken).toBe(data.sessionTransferToken);
expect(creds.tokenType).toBe(data.tokenType);
expect(creds.expiresIn).toBe(data.expiresIn);
expect(creds.idToken).toBe(data.idToken);
expect(creds.refreshToken).toBe(data.refreshToken);
});

it('should handle missing optional properties', () => {
const data = {
sessionTransferToken: 'stt_token_123',
tokenType: 'urn:auth0:params:oauth:token-type:session_transfer_token',
expiresIn: 120,
};

const creds = new SSOCredentials(data);

expect(creds.idToken).toBeUndefined();
expect(creds.refreshToken).toBeUndefined();
});
});

describe('fromResponse', () => {
it('should correctly map snake_case API response to camelCase properties', () => {
const response: SSOCredentialsResponse = {
access_token: 'stt_token_123',
issued_token_type:
'urn:auth0:params:oauth:token-type:session_transfer_token',
token_type: 'N_A',
expires_in: 120,
id_token: 'id_token_456',
refresh_token: 'refresh_token_789',
};

const creds = SSOCredentials.fromResponse(response);

expect(creds).toBeInstanceOf(SSOCredentials);
expect(creds.sessionTransferToken).toBe(response.access_token);
expect(creds.tokenType).toBe(response.issued_token_type);
expect(creds.expiresIn).toBe(response.expires_in);
expect(creds.idToken).toBe(response.id_token);
expect(creds.refreshToken).toBe(response.refresh_token);
});

it('should handle a response with no optional fields', () => {
const response: SSOCredentialsResponse = {
access_token: 'stt_token_123',
issued_token_type:
'urn:auth0:params:oauth:token-type:session_transfer_token',
token_type: 'N_A',
expires_in: 60,
};

const creds = SSOCredentials.fromResponse(response);

expect(creds.idToken).toBeUndefined();
expect(creds.refreshToken).toBeUndefined();
});
});
});
1 change: 1 addition & 0 deletions src/core/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { AuthError } from './AuthError';
export { Credentials } from './Credentials';
export { SSOCredentials } from './SSOCredentials';
export { Auth0User } from './Auth0User';
export { ApiCredentials } from './ApiCredentials';
export {
Expand Down
25 changes: 25 additions & 0 deletions src/core/services/AuthenticationOrchestrator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IAuthenticationProvider } from '../interfaces';
import type {
Credentials,
SessionTransferCredentials,
User,
MfaChallengeResponse,
PasswordRealmParameters,
Expand All @@ -9,6 +10,7 @@ import type {
RevokeOptions,
ExchangeParameters,
ExchangeNativeSocialParameters,
SSOExchangeParameters,
PasswordlessEmailParameters,
PasswordlessSmsParameters,
LoginEmailParameters,
Expand All @@ -20,11 +22,13 @@ import type {
ResetPasswordParameters,
CreateUserParameters,
NativeCredentialsResponse,
SSOCredentialsResponse,
AuthorizeUrlParameters,
LogoutUrlParameters,
} from '../../types';
import {
Credentials as CredentialsModel,
SSOCredentials,
Auth0User,
AuthError,
} from '../models';
Expand Down Expand Up @@ -457,4 +461,25 @@ export class AuthenticationOrchestrator implements IAuthenticationProvider {
// The signup endpoint returns a snake_cased user profile.
return deepCamelCase<Partial<User>>(json);
}

async ssoExchange(
parameters: SSOExchangeParameters
): Promise<SessionTransferCredentials> {
validateParameters(parameters, ['refreshToken']);
const { headers, ...payload } = parameters;
const domain = new URL(this.baseUrl).host;
const body = {
client_id: this.clientId,
grant_type: 'refresh_token',
audience: `urn:${domain}:session_transfer`,
refresh_token: payload.refreshToken,
};
const { json, response } = await this.client.post<SSOCredentialsResponse>(
'/oauth/token',
body,
headers
);
if (!response.ok) throw AuthError.fromResponse(response, json);
return SSOCredentials.fromResponse(json);
}
}
88 changes: 88 additions & 0 deletions src/core/services/__tests__/AuthenticationOrchestrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
AuthError,
Auth0User,
Credentials as CredentialsModel,
SSOCredentials,
} from '../../models';

// Mock HttpClient but preserve getAuthHeader
Expand Down Expand Up @@ -116,6 +117,7 @@ describe('AuthenticationOrchestrator', () => {
orchestrator = new AuthenticationOrchestrator({
clientId,
httpClient: mockHttpClientInstance as unknown as HttpClient,
baseUrl: baseUrl,
});
});

Expand Down Expand Up @@ -860,4 +862,90 @@ describe('AuthenticationOrchestrator', () => {
);
});
});

describe('sso exchange', () => {
const ssoResponse = {
access_token: 'session_transfer_token_value',
issued_token_type:
'urn:auth0:params:oauth:token-type:session_transfer_token',
token_type: 'N_A',
expires_in: 120,
id_token: validIdToken,
refresh_token: 'new_refresh_token',
};

const parameters = {
refreshToken: 'a refresh token of a user',
};

it('should send correct payload with session_transfer audience', async () => {
mockHttpClientInstance.post.mockResolvedValueOnce({
json: ssoResponse,
response: new Response(null, { status: 200 }),
});
await orchestrator.ssoExchange(parameters);

expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
'/oauth/token',
expect.objectContaining({
grant_type: 'refresh_token',
client_id: clientId,
refresh_token: parameters.refreshToken,
audience: 'urn:samples.auth0.com:session_transfer',
}),
undefined
);
});

it('should return SSOCredentials instance', async () => {
mockHttpClientInstance.post.mockResolvedValueOnce({
json: ssoResponse,
response: new Response(null, { status: 200 }),
});

const result = await orchestrator.ssoExchange(parameters);

expect(result).toBeInstanceOf(SSOCredentials);
expect(result.sessionTransferToken).toBe(ssoResponse.access_token);
expect(result.tokenType).toBe(ssoResponse.issued_token_type);
expect(result.expiresIn).toBe(ssoResponse.expires_in);
expect(result.idToken).toBe(ssoResponse.id_token);
expect(result.refreshToken).toBe(ssoResponse.refresh_token);
});

it('should handle oauth error', async () => {
mockHttpClientInstance.post.mockResolvedValueOnce({
json: oauthErrorResponse,
response: new Response(null, { status: 400 }),
});

await expect(orchestrator.ssoExchange(parameters)).rejects.toThrow(
AuthError
);
});

it('should pass custom headers', async () => {
mockHttpClientInstance.post.mockResolvedValueOnce({
json: ssoResponse,
response: new Response(null, { status: 200 }),
});
await orchestrator.ssoExchange({
refreshToken: 'a refresh token',
headers: { 'X-Custom-Header': 'custom-value' },
});

expect(mockHttpClientInstance.post).toHaveBeenCalledWith(
'/oauth/token',
expect.objectContaining({
grant_type: 'refresh_token',
refresh_token: 'a refresh token',
}),
{ 'X-Custom-Header': 'custom-value' }
);
});

it('should throw when refreshToken is missing', async () => {
await expect(orchestrator.ssoExchange({} as any)).rejects.toThrow();
});
});
});
Loading
Loading