The session token exchange flow enables native applications to obtain session tokens after completing browser-based authentication flows. This is essential for mobile and desktop apps that use system browsers for login.
Why token exchange?
Native applications cannot use HTTP-only cookies for session management because:
System browsers don’t share cookies with the app
Embedded webviews are insecure and deprecated by OAuth providers
Native apps need to make authenticated API requests
Solution: The token exchange flow allows apps to exchange a one-time code for a session token after authentication.
Flow overview
Initialize flow
The native app starts a registration or login flow and receives an init_code.
Open system browser
The app opens the system browser with the flow URL and includes a return_to parameter pointing to a custom URL scheme.
User authenticates
User completes authentication in the browser.
Return to app
Browser redirects to the custom URL scheme with a return_to_code.
Exchange codes
App exchanges both codes for a session token via the token exchange endpoint.
Store token
App stores the token securely and uses it for authenticated requests.
Implementation
Step 1: Initialize the flow
Create a login or registration flow via API:
Login flow
Registration flow
curl -X GET "https://kratos-public/self-service/login/api" \
-H "Accept: application/json"
Response includes init_code:
{
"id" : "9f425a8d-7efc-4768-8f23-7647a74fdf13" ,
"type" : "api" ,
"session_token_exchange_code" : "ory_code_init_abc123..." ,
"ui" : {
"action" : "https://kratos-public/self-service/login?flow=9f425a8d..." ,
"nodes" : [ ... ]
}
}
Step 2: Open system browser
Construct the browser URL with your custom URL scheme:
React Native
iOS Native
Android Native
import { Linking } from 'react-native' ;
import InAppBrowser from 'react-native-inappbrowser-reborn' ;
const initCode = flow . session_token_exchange_code ;
const returnToUrl = 'myapp://auth/callback' ;
const flowUrl = ` ${ flow . ui . action } &return_to= ${ encodeURIComponent ( returnToUrl ) } ` ;
// Open system browser
await InAppBrowser . open ( flowUrl , {
dismissButtonStyle: 'close' ,
preferredBarTintColor: '#453AA4' ,
preferredControlTintColor: 'white' ,
readerMode: false ,
animated: true ,
modalEnabled: true ,
enableBarCollapsing: false ,
});
Step 3: Handle the callback
Register a custom URL scheme and handle the redirect:
React Native
iOS Native
Android Native
import { Linking } from 'react-native' ;
import { parse } from 'query-string' ;
// Listen for deep links
Linking . addEventListener ( 'url' , async ( event ) => {
const { url } = event ;
if ( url . startsWith ( 'myapp://auth/callback' )) {
// Close browser
await InAppBrowser . close ();
// Extract return_to_code
const queryParams = parse ( url . split ( '?' )[ 1 ]);
const returnToCode = queryParams . return_to_code ;
// Exchange codes for session token
await exchangeCodesForSession ( initCode , returnToCode );
}
});
Step 4: Exchange codes for session token
Call the token exchange endpoint:
GET /sessions/token-exchange?init_code={init_code} & return_to_code = { return_to_code}
JavaScript/TypeScript
Swift
Kotlin
async function exchangeCodesForSession ( initCode : string , returnToCode : string ) {
const response = await fetch (
`https://kratos-public/sessions/token-exchange?` +
`init_code= ${ encodeURIComponent ( initCode ) } &` +
`return_to_code= ${ encodeURIComponent ( returnToCode ) } `
);
if ( ! response . ok ) {
throw new Error ( 'Token exchange failed' );
}
const data = await response . json ();
// Store session token securely
await secureStorage . setItem ( 'session_token' , data . session_token );
// Session object is also available
console . log ( 'Logged in as:' , data . session . identity . traits . email );
return data ;
}
{
"session_token" : "ory_st_MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj" ,
"session" : {
"id" : "9f425a8d-7efc-4768-8f23-7647a74fdf13" ,
"active" : true ,
"expires_at" : "2024-02-15T09:30:00Z" ,
"authenticated_at" : "2024-01-15T09:30:00Z" ,
"authenticator_assurance_level" : "aal1" ,
"authentication_methods" : [
{
"method" : "password" ,
"aal" : "aal1" ,
"completed_at" : "2024-01-15T09:30:00Z"
}
],
"issued_at" : "2024-01-15T09:30:00Z" ,
"identity" : {
"id" : "7b9f3e2a-5c1d-4f8e-9a3b-2d6c8e4f7a9b" ,
"traits" : {
"email" : "[email protected] "
}
}
}
}
Using the session token
Once obtained, use the token for authenticated requests:
curl "https://kratos-public/sessions/whoami" \
-H "Authorization: Bearer ory_st_MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj"
Code storage and expiry
The token exchange mechanism uses a database table:
selfservice/sessiontokenexchange/persistence.go
type Exchanger struct {
ID uuid . UUID
NID uuid . UUID
FlowID uuid . UUID
SessionID uuid . NullUUID
InitCode string
ReturnToCode string
CreatedAt time . Time
UpdatedAt time . Time
}
Code lifecycle
Flow initialization
init_code and return_to_code are generated and stored.
Flow completion
When the flow completes, session_id is attached to the exchanger.
Token exchange
Both codes are validated, session token is returned, exchanger can be reused.
Expiration
Exchangers are deleted after a configured TTL (typically 10 minutes).
Codes are single-use for flow initialization but can be exchanged multiple times after the session is created. Store the session token securely after first exchange.
Error handling
Common errors
403 Forbidden - Invalid codes
{
"error" : {
"id" : "forbidden" ,
"code" : 403 ,
"status" : "Forbidden" ,
"reason" : "no session yet for this code"
}
}
Causes:
Invalid init_code or return_to_code
User hasn’t completed the flow yet
Codes have expired
404 Not Found - No session
{
"error" : {
"id" : "not_found" ,
"code" : 404 ,
"status" : "Not Found" ,
"reason" : "no session yet for this code"
}
}
Causes:
Flow was not completed successfully
Session was deleted before exchange
400 Bad Request - Missing parameters
{
"error" : {
"id" : "bad_request" ,
"code" : 400 ,
"status" : "Bad Request" ,
"reason" : "init_code and return_to_code query params must be set"
}
}
Security considerations
Security best practices:
Use HTTPS for all requests
Store session tokens in secure storage (Keychain/Keystore)
Never log session tokens
Use system browser (not webview) for authentication
Implement PKCE if building custom OAuth flows
Validate the return URL scheme to prevent hijacking
Set short expiry times for exchange codes
Custom URL schemes
Register your custom URL scheme:
Android (AndroidManifest.xml)
iOS (Info.plist)
React Native (app.json)
< activity android:name = ".MainActivity" >
< intent-filter >
< action android:name = "android.intent.action.VIEW" />
< category android:name = "android.intent.category.DEFAULT" />
< category android:name = "android.intent.category.BROWSABLE" />
< data android:scheme = "myapp" android:host = "auth" />
</ intent-filter >
</ activity >
Complete example
Here’s a full React Native implementation:
Complete React Native Flow
import { useEffect , useState } from 'react' ;
import { Linking } from 'react-native' ;
import InAppBrowser from 'react-native-inappbrowser-reborn' ;
import * as SecureStore from 'expo-secure-store' ;
const KRATOS_URL = 'https://kratos-public' ;
const RETURN_TO_URL = 'myapp://auth/callback' ;
export function useAuth () {
const [ session , setSession ] = useState ( null );
const [ loading , setLoading ] = useState ( false );
useEffect (() => {
// Handle deep links
const subscription = Linking . addEventListener ( 'url' , handleDeepLink );
return () => subscription . remove ();
}, []);
async function handleDeepLink ( event ) {
const { url } = event ;
if ( url . startsWith ( RETURN_TO_URL )) {
await InAppBrowser . close ();
const params = new URLSearchParams ( url . split ( '?' )[ 1 ]);
const returnToCode = params . get ( 'return_to_code' );
const initCode = await SecureStore . getItemAsync ( 'init_code' );
if ( initCode && returnToCode ) {
await exchangeCodesForSession ( initCode , returnToCode );
}
}
}
async function startLogin () {
setLoading ( true );
try {
// Initialize login flow
const response = await fetch ( ` ${ KRATOS_URL } /self-service/login/api` , {
headers: { 'Accept' : 'application/json' }
});
const flow = await response . json ();
// Store init code
await SecureStore . setItemAsync ( 'init_code' , flow . session_token_exchange_code );
// Open browser
const flowUrl = ` ${ flow . ui . action } &return_to= ${ encodeURIComponent ( RETURN_TO_URL ) } ` ;
await InAppBrowser . open ( flowUrl );
} catch ( error ) {
console . error ( 'Login failed:' , error );
setLoading ( false );
}
}
async function exchangeCodesForSession ( initCode , returnToCode ) {
try {
const response = await fetch (
` ${ KRATOS_URL } /sessions/token-exchange?` +
`init_code= ${ encodeURIComponent ( initCode ) } &` +
`return_to_code= ${ encodeURIComponent ( returnToCode ) } `
);
const data = await response . json ();
// Store session token
await SecureStore . setItemAsync ( 'session_token' , data . session_token );
setSession ( data . session );
setLoading ( false );
} catch ( error ) {
console . error ( 'Token exchange failed:' , error );
setLoading ( false );
}
}
async function logout () {
await SecureStore . deleteItemAsync ( 'session_token' );
setSession ( null );
}
return { session , loading , startLogin , logout };
}