Use Own Auth sessions in React Native
A React Native application should treat an Own Auth session as an opaque application credential. The application backend verifies credentials, creates the session, and returns only the raw session token plus safe user fields. The device stores that token in protected platform storage and presents it in the authorization header for later API requests.
Create a native sign in endpoint
Use a dedicated application endpoint for native sign in instead of depending on browser cookies. Call auth.signInEmailPassword on the backend and narrow the status before reading the user, session, or session token. A user with MFA enabled receives a challenge and no session.
export async function nativeSignIn(request: Request) {
const { email, password } = await request.json();
const result = await auth.signInEmailPassword({ email, password });
if (result.status === "mfa_required") {
return Response.json({
status: result.status,
challengeToken: result.challengeToken,
methods: result.methods,
expiresAt: result.expiresAt.toISOString(),
});
}
return Response.json({
status: "complete",
sessionToken: result.sessionToken,
user: {
id: result.user.id,
email: result.user.email,
name: result.user.name,
imageUrl: result.user.imageUrl,
},
expiresAt: result.session.expiresAt.toISOString(),
});
}Do not serialize the complete Own Auth User or Session objects. Internal records include protected credential fields and revocation metadata. The MFA challenge token is also a credential. Keep it in memory only, submit it to a matching native MFA endpoint, and replace it with the completed session token.
Store the session in protected device storage
AsyncStorage is not the right place for a raw session token. The following example uses react-native-keychain, which stores string credentials through the iOS Keychain and Android protected storage. The device only copy does not migrate to another device backup.
import * as Keychain from "react-native-keychain";
const service = "com.example.app.own-auth-session";
export async function saveSession(token: string) {
await Keychain.setGenericPassword("session", token, {
service,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
}
export async function loadSession() {
const stored = await Keychain.getGenericPassword({ service });
return stored ? stored.password : null;
}
export async function clearSession() {
await Keychain.resetGenericPassword({ service });
}Never print the stored value, add it to crash metadata, or place it in a deep link. A local biometric prompt can protect access to the stored token, but it does not replace server session verification. The server remains authoritative for expiry, revocation, and account disablement.
Send the bearer token to the backend
import { clearSession, loadSession } from "./auth/session-store";
export async function apiFetch(path: string, init: RequestInit = {}) {
const token = await loadSession();
const headers = new Headers(init.headers);
if (token) headers.set("authorization", "Bearer " + token);
const response = await fetch("https://api.example.com" + path, {
...init,
headers,
});
if (response.status === 401) await clearSession();
return response;
}Every protected backend route extracts the bearer value and calls auth.getCurrentSession. A successful result identifies the current user. The route must still reload organisation membership and authorize the requested application record. A user ID or organisation ID sent in JSON is never a substitute for the verified session.
const authorization = request.headers.get("authorization");
const token = authorization?.startsWith("Bearer ")
? authorization.slice("Bearer ".length)
: null;
const current = token ? await auth.getCurrentSession(token) : null;
if (!current) return new Response("Unauthorized", { status: 401 });
return Response.json({
id: current.user.id,
email: current.user.email,
name: current.user.name,
imageUrl: current.user.imageUrl,
});Separate application sessions from provider tokens
A Google or Apple SDK token proves something to the provider integration. It is not the credential for every application API request. Verify the provider credential on the backend, normalize its subject and verified email, then call auth.signInWithVerifiedExternalIdentity. Store the resulting Own Auth session token, not the provider identity token, as the application session.
Complete MFA without browser cookies
A native endpoint using the direct Own Auth API may return the short lived challenge token to the application. Keep it in memory while the MFA screen is active. Submit it with the TOTP or recovery code to a backend route that calls auth.completeMfaWithTotp or auth.completeMfaWithRecoveryCode, then replace it with the returned session token.
Do not store the challenge beside the long lived session. Clear it when the application leaves the MFA screen, the challenge expires, or the server rejects the attempt limit. A fresh first factor creates a fresh challenge.
Coordinate refresh and concurrent requests
Own Auth database sessions do not require a client refresh token. Each API request presents the same opaque session token until absolute expiry, idle expiry, sign out, or revocation. Avoid building a mobile token refresh protocol around the session. When several requests receive 401 together, clear the protected value once and move the application to its signed out state.
Queueing every request while the application reads protected storage can cause unnecessary delay. Load the token once when the application session service starts, keep it only in process memory, and update that value when sign in or sign out changes the protected record. Reload it after a cold start.
Protect navigation and background work
A locally stored token can decide which loading screen appears, but it cannot prove that the session remains valid. Verify it with the backend before showing protected data. Background tasks should request the token only when they need it and handle revocation without repeatedly retrying an unauthorized request.
Exclude authorization headers and response bodies from mobile network debugging in release builds. Crash reporting should record only the route name, safe status, and a generated request identifier. The backend can use that identifier to find sanitized operational logs without copying the credential into the report.
Sign out should call the backend so auth.signOut revokes the database session, then clear the protected device value even when the network request reports that the session was already absent. Also clear the local token after a 401. Test restart persistence, offline sign out, session expiry, administrator revocation, MFA completion, and account disablement on both iOS and Android.