Skip to content
Torii docs

Native (Capacitor) auth

Inside a Capacitor (or other hybrid-native) WebView, the browser cookie that backs a Torii session is unreliable: the WebView and the system browser don’t share a cookie jar, and OAuth has to leave the WebView to a real browser and come back. Native mode makes the SDK cookie-less instead. You give <ToriiProvider> a tokenCache (a small adapter over your app’s secure storage), and the SDK keeps the session token there, replaying it in an X-Torii-Session-Token header rather than relying on a cookie.

This mirrors the token-cache model you may know from other native auth SDKs. It is opt-in: a normal web app should not set tokenCache and should keep using first-party cookies.

When tokenCache is set, the SDK:

  • Stores the opaque session token in your tokenCache (never a cookie) and sends it in the X-Torii-Session-Token header on refresh and logout.
  • Marks those requests ?_is_native=1 and sends them with credentials: 'omit'. Native origins are allow-listed without credentials, so a cookie would be blocked anyway.
  • On cold start, reads the stored token, refreshes it into a short-lived access token, and loads the user, with no boot-probe cookie round-trip.
  • Runs OAuth through the system browser with a deep-link return instead of a same-tab redirect (see OAuth below).

The access token (JWT) is still what authenticates your /_torii/** API calls; tokenCache only holds the long-lived session token used to mint new access tokens.

Native API calls are allow-listed by their WebView Origin, non-credentialed (no cookie). The default Capacitor/Ionic origins work out of the box: capacitor://localhost (iOS), https://localhost and http://localhost (Android), and ionic://localhost. If you override server.hostname or iosScheme in capacitor.config (e.g. capacitor://app.example.com), any host on the capacitor:// and ionic:// schemes is still accepted. A custom Android https:// host is an ordinary web origin instead: add it to Allowed Origins in the dashboard. The Origin is not a security boundary in native mode (tenant scoping is by host, auth is by the session token), so this controls whether your WebView can read the response, not who may call the API.

  1. Implement tokenCache over your app’s secure storage. All three methods may be sync or async, and should not throw.

    import { Preferences } from '@capacitor/preferences';
    import type { ToriiTokenCache } from '@torii-js/torii-react';
    const KEY = 'torii_session';
    const tokenCache: ToriiTokenCache = {
    getToken: async () => (await Preferences.get({ key: KEY })).value,
    saveToken: (token) => Preferences.set({ key: KEY, value: token }),
    clearToken: () => Preferences.remove({ key: KEY }),
    };

    For production, prefer a Keychain/Keystore-backed plugin over Preferences so the token is encrypted at rest.

  2. Pass it to <ToriiProvider>. That single prop switches the SDK into native mode.

    <ToriiProvider publishableKey="pk_live_…" tokenCache={tokenCache}>
    <App />
    </ToriiProvider>
  3. Use the SDK as usual. <SignIn>, <SignUp>, useAuth(), getToken() all work unchanged; the cookie-less transport is internal.

OAuth can’t complete inside the WebView: the provider’s consent page must run in the real system browser, then hand control back to your app via a deep link. You wire two more props and the SDK orchestrates the rest (including mandatory PKCE).

  1. Register a deep-link target for your app. Prefer a claimed https link (Android App Link / iOS Universal Link): the OS verifies domain ownership, so only your app can receive the return and another app cannot hijack it. A custom scheme (com.example.app://oauth-callback) also works and is handy in development, but any app can register the same scheme, so the return is interceptable; the mandatory PKCE check is what makes an intercepted return useless to an attacker. Use a claimed https link in production.

  2. Allow-list it. In the dashboard, open Project settings → Allowed native redirects and add the exact target. The authorize call is rejected with 403 if the redirect isn’t listed (an empty list disables native OAuth entirely).

  3. Provide a nativeOAuth config (the system-browser browser bridge plus its deep-link redirect). The bridge opens url in the system browser and resolves with the deep-link URL your app is reopened with (or null if the user dismisses it).

    import { Browser } from '@capacitor/browser';
    import { App } from '@capacitor/app';
    const nativeOAuth = {
    // Prefer a claimed https link in production; a com.example.app:// custom
    // scheme also works (handy in dev, but interceptable; see the note above).
    redirect: 'https://app.example.com/oauth-callback',
    browser: {
    openOAuth: (url: string) =>
    new Promise<string | null>((resolve) => {
    App.addListener('appUrlOpen', async ({ url: returnUrl }) => {
    await Browser.close();
    resolve(returnUrl);
    }).then((handle) => {
    // Remove the listener once we've resolved, to avoid leaks across
    // repeated sign-in attempts. (Omitted here for brevity.)
    void handle;
    });
    void Browser.open({ url });
    }),
    },
    };
    <ToriiProvider publishableKey="pk_live_…" tokenCache={tokenCache} nativeOAuth={nativeOAuth}>
    <App />
    </ToriiProvider>

The SDK builds the PKCE pair, opens the authorize URL through your bridge, parses the returned deep link, and establishes the session. It surfaces any provider error through the same oauthError channel the web flow uses, and shows the legal-consent interrupt when a sign-up defers consent.

Both are optional and have no default; supplying tokenCache is what turns native mode on, and nativeOAuth adds OAuth on top.

NameTypeDescription
tokenCacheToriiTokenCacheSecure-storage adapter ({ getToken, saveToken, clearToken }, sync or async). Supplying it switches the SDK to cookie-less native mode.
nativeOAuth{ browser: { openOAuth: (url: string) => Promise<string | null> }; redirect: string }Native OAuth config. browser.openOAuth opens url in the system browser and resolves with the deep-link return URL (or null if cancelled); redirect is the deep-link target, which must be in the environment’s Allowed native redirects. Required (as a whole) for OAuth in native mode.