diff --git a/EXAMPLES.md b/EXAMPLES.md index feb0aefca..8d9478e78 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -45,6 +45,7 @@ - [Using Custom Token Exchange with Hooks](#using-custom-token-exchange-with-hooks) - [Using Custom Token Exchange with Auth0 Class](#using-custom-token-exchange-with-auth0-class) - [With Organization Context](#with-organization-context) + - [Delegation & Impersonation (Actor Token)](#delegation--impersonation-actor-token) - [Subject Token Type Requirements](#subject-token-type-requirements) - [Valid Token Type Patterns](#valid-token-type-patterns) - [Reserved Namespaces (Forbidden)](#reserved-namespaces-forbidden) @@ -1061,6 +1062,45 @@ const credentials = await customTokenExchange({ }); ``` +### Delegation & Impersonation (Actor Token) + +For delegation and impersonation scenarios — where an actor (such as an AI agent, support representative, or service) acts on behalf of the subject — pass an actor token alongside the subject token. This follows [RFC 8693 Section 2.1](https://datatracker.ietf.org/doc/html/rfc8693#section-2.1). + +```typescript +const credentials = await customTokenExchange({ + subjectToken: 'subject-provider-token', + subjectTokenType: 'urn:acme:legacy-system-token', + actorToken: 'actor-id-token', + actorTokenType: 'http://corporate-idp/id-token', +}); +``` + +`actorTokenType` follows the same URI validation rules as `subjectTokenType` and accepts any developer-defined URI. + +> ℹ️ **Prerequisites**: This flow requires the `cte_actor_token` feature flag enabled on your tenant and an [Auth0 Action](https://auth0.com/docs/customize/actions) that calls `api.authentication.setActor()` to set the `act` claim on the issued tokens. + +**Accessing the `act` claim** + +When an Action sets the actor, the issued ID token carries an `act` claim describing the acting party (which may be nested to represent a delegation chain). It is available on the parsed user profile: + +```typescript +import { parseIdToken } from 'react-native-auth0'; + +const credentials = await customTokenExchange({ + subjectToken: 'subject-provider-token', + subjectTokenType: 'urn:acme:legacy-system-token', + actorToken: 'actor-id-token', + actorTokenType: 'http://corporate-idp/id-token', +}); + +const user = parseIdToken(credentials.idToken); +console.log(user.act); // The acting party claim +``` + +> ⚠️ **Refresh token suppression**: When an actor token is present, Auth0 will **not** issue a refresh token, regardless of whether `offline_access` is in the requested scope. `credentials.refreshToken` will be `undefined` in this case. +> +> ⚠️ **Paired parameters**: `actorToken` and `actorTokenType` must be provided together. Supplying only one throws an `AuthError` with code `invalid_actor_token_parameters` before any network request is made. + ### Subject Token Type Requirements The `subjectTokenType` parameter must be a **unique profile token type URI** starting with `https://` or `urn:`. diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index e19a0d4c1..e034a2698 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.LifecycleOwner import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.request.ActorToken import com.auth0.android.authentication.storage.CredentialsManagerException import com.auth0.android.authentication.storage.LocalAuthenticationOptions import com.auth0.android.authentication.storage.SecureCredentialsManager @@ -620,19 +621,28 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 audience: String?, scope: String?, organization: String?, + actorToken: String?, + actorTokenType: String?, promise: Promise ) { val authClient = AuthenticationAPIClient(auth0!!) if (useDPoP) { authClient.useDPoP(reactContext) } - + val finalScope = scope ?: "openid profile email" - + + val actor = if (actorToken != null && actorTokenType != null) { + ActorToken(token = actorToken, tokenType = actorTokenType) + } else { + null + } + val request = authClient.customTokenExchange( subjectTokenType = subjectTokenType, subjectToken = subjectToken, - organization = organization + organization = organization, + actorToken = actor ) // Set audience and scope using the request builder methods diff --git a/example/src/App.web.tsx b/example/src/App.web.tsx index 469adeba2..0d9f9ccae 100644 --- a/example/src/App.web.tsx +++ b/example/src/App.web.tsx @@ -55,6 +55,7 @@ const HooksAuthContent = (): React.JSX.Element => { createUser, resetPassword, loginWithPasswordRealm, + customTokenExchange, mfa, users, } = useAuth0(); @@ -64,6 +65,30 @@ const HooksAuthContent = (): React.JSX.Element => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + // Custom Token Exchange (RFC 8693) state + const [subjectToken, setSubjectToken] = useState(''); + const [subjectTokenType, setSubjectTokenType] = useState( + 'urn:acme:external-idp-token' + ); + const [actorToken, setActorToken] = useState(''); + const [actorTokenType, setActorTokenType] = useState( + 'urn:ietf:params:oauth:token-type:id_token' + ); + + const fillActorTokenFromSession = async () => { + setApiError(null); + try { + const credentials = await getCredentials(); + if (credentials?.idToken) { + setActorToken(credentials.idToken); + } else { + setApiError(new Error('No ID token available in the current session.')); + } + } catch (e) { + setApiError(e as Error); + } + }; + // MFA wizard state const [mfaToken, setMfaToken] = useState(''); const [mfaStep, setMfaStep] = useState('idle'); @@ -288,6 +313,71 @@ const HooksAuthContent = (): React.JSX.Element => { disabled={!result?.accessToken} />