> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aveid.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Expo with AuthSession

> Use Ave in an Expo app with expo-auth-session and expo-web-browser. Covers deep links, PKCE, code exchange, token refresh, and the Convex handoff.

Expo is a great fit for Ave's public-client OAuth flow. The clean setup is:

* **`expo-auth-session`** — builds the Ave authorize URL and runs PKCE; **`promptAsync()`** opens the **in-app system browser** (Chrome Custom Tabs on Android, **SFSafariViewController** on iOS) via **`expo-web-browser`**, not a raw `WebView` and not Safari/Chrome leaving your app without a return path.
* **`expo-web-browser`** — session completion (`maybeCompleteAuthSession`), optional **`warmUpAsync` / `coolDownAsync`** on Android
* **`@ave-id/sdk`** — exchange the code, refresh tokens, optional **Ave Session**

Do **not** use `Linking.openURL(authorizeUrl)` alone for login — you lose the secure auth-session handoff. Use **`useAuthRequest`** + **`promptAsync()`** (or `WebBrowser.openAuthSessionAsync` with the same redirect URL you registered).

If you're using Convex too, see [Convex custom auth](/guides/convex-custom-auth). The short version: pass Convex the Ave `id_token`, not `access_token_jwt`.

<Warning>
  Expo apps are public clients. Do not ship a `clientSecret` in the app. Use the authorization code flow with PKCE.
</Warning>

## Install dependencies

```bash theme={null}
bunx expo install expo-auth-session expo-web-browser expo-crypto
bun add @ave-id/sdk
```

Expo's current AuthSession docs note that `expo-crypto` is a peer dependency of `expo-auth-session`.

## Configure the Ave SDK for Expo

**Expo native does not provide Web Crypto for SHA-256** (needed for PKCE code challenge). Use **`expo-crypto`**, not browser `crypto.subtle`, for **`digest`** and **`getRandomValues`**.

### One import (recommended)

```ts theme={null}
import * as ExpoCrypto from "expo-crypto";
import { configureAveSdkForExpo } from "@ave-id/sdk/expo-session";

configureAveSdkForExpo(ExpoCrypto);
```

This is equivalent to:

```ts theme={null}
import { configureCryptoRuntime, createExpoCryptoRuntime } from "@ave-id/sdk";

configureCryptoRuntime(createExpoCryptoRuntime(ExpoCrypto));
```

Put it near app bootstrap, **before** any PKCE helpers (`generateCodeChallenge`, `startPkceLogin`, or AuthSession’s PKCE).

## System browser (`expo-web-browser`)

Ave MUST open in the **Expo-managed browser session** so the redirect returns to your app.

1. **Import at root** (before your first auth prompt):

```ts theme={null}
import * as WebBrowser from "expo-web-browser";
import { initExpoOAuthBrowserSession, warmUpExpoAuthBrowser } from "@ave-id/sdk/expo-session";

initExpoOAuthBrowserSession(WebBrowser);
```

2. **Android**: warm the custom tab while your login screen is mounted (same pattern as Expo’s own AuthSession examples):

```ts theme={null}
useEffect(() => {
  const dispose = warmUpExpoAuthBrowser(WebBrowser);
  return dispose;
}, []);
```

3. **`AuthSession.useAuthRequest`** + **`promptAsync()`** — this is what actually opens the authorization URL in **`expo-web-browser`**. You do not need to call `openBrowserAsync` yourself for the standard flow.

<Warning>
  `verifyJwt()` is not supported on Expo native today. The Ave SDK skips client-side JWT verification in Expo callback helpers so your login flow does not crash, but if you need signature verification you should do it on your server or let a verifier like Convex validate the `id_token`.
</Warning>

## Register your redirect URI

First, give your app a custom scheme:

```json theme={null}
{
  "expo": {
    "scheme": "aveexpo"
  }
}
```

Then build the redirect URI you will use in the app:

```ts theme={null}
import * as AuthSession from "expo-auth-session";

const redirectUri = AuthSession.makeRedirectUri({
  scheme: "aveexpo",
  path: "oauth/callback",
});
```

Register that exact redirect URI in the Ave developer portal for your app.

<Note>
  In Expo development builds and standalone apps, `makeRedirectUri()` uses your app scheme. On web it uses the current website origin. Keep the redirect URI you register in Ave aligned with the platform you are testing.
</Note>

## Ave Session + SecureStore (single-flight refresh)

For **`offline_access`**, token refresh, and optional **`#app_key`** (E2EE) merged from the redirect URL string:

```ts theme={null}
import * as ExpoCrypto from "expo-crypto";
import * as SecureStore from "expo-secure-store";
import {
  AveSession,
  completeExpoOAuthCallback,
  configureAveSdkForExpo,
  createSecureStoreAdapter,
  wireAveSessionToConvex,
} from "@ave-id/sdk/expo-session";
import { ConvexReactClient } from "convex/react";

configureAveSdkForExpo(ExpoCrypto);

const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!);

const session = new AveSession({
  oauth: { clientId: CLIENT_ID, redirectUri },
  storage: createSecureStoreAdapter(SecureStore),
  crossTabSync: false,
});

// After AuthSession success — pass full URL when E2EE may include #app_key on web:
await completeExpoOAuthCallback(
  session,
  { clientId: CLIENT_ID, redirectUri },
  {
    code: response.params.code,
    codeVerifier: request.codeVerifier!,
    expectedState: request.state,
    responseState: response.params.state,
    redirectUrlWithFragment: response.url,
  }
);

wireAveSessionToConvex(convex, session);
```

`completeExpoOAuthCallback` calls **`exchangeCode`** and **`session.setTokensFromResponse`** (no `sessionStorage` / `window`). Use **`onExpoAppForegroundRefresh`** from the same module for background refresh nudges.

## Minimal implementation

This example uses Expo for the browser redirect and PKCE request, then uses `@ave-id/sdk` for Ave's token exchange and refresh helpers.

```tsx theme={null}
import { useEffect, useMemo, useState } from "react";
import { Button, Text, View } from "react-native";
import * as AuthSession from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import { exchangeCode, fetchUserInfo, refreshToken } from "@ave-id/sdk";
import { initExpoOAuthBrowserSession, warmUpExpoAuthBrowser } from "@ave-id/sdk/expo-session";

initExpoOAuthBrowserSession(WebBrowser);

const CLIENT_ID = "YOUR_CLIENT_ID";
const ISSUER = "https://aveid.net";

export default function AveLoginScreen() {
  const [tokens, setTokens] = useState<any | null>(null);
  const [profile, setProfile] = useState<any | null>(null);

  const discovery = AuthSession.useAutoDiscovery(ISSUER);

  const redirectUri = useMemo(
    () =>
      AuthSession.makeRedirectUri({
        scheme: "aveexpo",
        path: "oauth/callback",
      }),
    [],
  );

  const [request, response, promptAsync] = AuthSession.useAuthRequest(
    {
      clientId: CLIENT_ID,
      redirectUri,
      responseType: AuthSession.ResponseType.Code,
      scopes: ["openid", "profile", "email", "offline_access"],
      usePKCE: true,
    },
    discovery,
  );

  useEffect(() => {
    return warmUpExpoAuthBrowser(WebBrowser);
  }, []);

  useEffect(() => {
    async function finishLogin() {
      if (!response || response.type !== "success" || !request) return;

      const code = response.params.code;
      if (!code) {
        throw new Error("Missing authorization code");
      }

      if (response.params.state !== request.state) {
        throw new Error("State mismatch");
      }

      const nextTokens = await exchangeCode(
        {
          clientId: CLIENT_ID,
          redirectUri,
          issuer: ISSUER,
        },
        {
          code,
          codeVerifier: request.codeVerifier,
        },
      );

      setTokens(nextTokens);

      const user = await fetchUserInfo(
        { clientId: CLIENT_ID, redirectUri, issuer: ISSUER },
        nextTokens.access_token,
      );

      setProfile(user);
    }

    finishLogin().catch((error) => {
      console.error("Ave login failed", error);
    });
  }, [redirectUri, request, response]);

  async function handleRefresh() {
    if (!tokens?.refresh_token) return;

    const nextTokens = await refreshToken(
      {
        clientId: CLIENT_ID,
        redirectUri,
        issuer: ISSUER,
      },
      {
        refreshToken: tokens.refresh_token,
      },
    );

    setTokens(nextTokens);
  }

  return (
    <View style={{ gap: 12, padding: 24 }}>
      <Button
        title="Continue with Ave"
        disabled={!request}
        onPress={() => promptAsync()}
      />

      {profile ? (
        <Text>Signed in as {profile.email ?? profile.preferred_username ?? profile.sub}</Text>
      ) : null}

      {tokens?.refresh_token ? (
        <Button title="Refresh session" onPress={handleRefresh} />
      ) : null}
    </View>
  );
}
```

## What this flow does

1. `useAuthRequest()` creates the authorization request and PKCE verifier/challenge
2. `promptAsync()` opens the system browser via Expo's browser session support
3. Ave redirects back to your Expo deep link with an authorization `code`
4. You exchange that code with `exchangeCode(...)`, passing `request.codeVerifier`
5. Ave returns the normal token set: `access_token`, `access_token_jwt`, `id_token`, and optionally `refresh_token`

<Tip>
  Request `openid` whenever you need `id_token`. Without `openid`, Ave will not return an ID token.
</Tip>

## Token handling

Ave's token response has a few fields Expo developers usually care about:

| Field              | Use                                                                     |
| ------------------ | ----------------------------------------------------------------------- |
| `access_token`     | Bearer token for Ave API endpoints like `userinfo`                      |
| `access_token_jwt` | Signed JWT for Ave resource authorization                               |
| `id_token`         | OIDC identity token for your app and for Convex                         |
| `refresh_token`    | Used to rotate into a fresh token set when `offline_access` was granted |

<Warning>
  Refresh tokens rotate on every use. After a successful refresh, store the new `refresh_token` immediately and discard the old one.
</Warning>

On Expo native, do not call `verifyJwt()` in the client. Use the tokens directly for app state, and move JWT signature verification to your backend if you need it.

## Convex handoff

If your Expo app talks to Convex, give Convex the Ave `id_token`:

```ts theme={null}
convex.setAuth(async () => tokens.id_token);
```

Do not pass `access_token_jwt` to Convex. Its audience is `https://aveid.net`, while Convex expects the `id_token` audience to match your Ave `clientId`.

The full setup is documented in [Convex custom auth](/guides/convex-custom-auth).

## Common pitfalls

<AccordionGroup>
  <Accordion title="The browser completes but the app never resumes">
    Your app scheme or redirect URI is misconfigured. Make sure the scheme in Expo config matches the scheme used in `makeRedirectUri()`, and register that exact redirect URI in the Ave developer portal.
  </Accordion>

  <Accordion title="The code exchange fails with PKCE or invalid_grant errors">
    The authorization code must be exchanged with the exact `request.codeVerifier` created by `useAuthRequest()`. Do not generate a second verifier yourself during the callback.
  </Accordion>

  <Accordion title="The flow works on native but fails on web">
    Expo documents that `WebBrowser.maybeCompleteAuthSession()` should run on web so the popup can finish cleanly. Also make sure the auth flow starts and finishes on the same web origin.
  </Accordion>

  <Accordion title="Convex rejects the token">
    Pass `id_token`, not `access_token_jwt`. See [Convex custom auth](/guides/convex-custom-auth).
  </Accordion>
</AccordionGroup>

## Notes on Expo APIs

* Expo's AuthSession docs say `usePKCE` defaults to `true`, but it is still worth setting explicitly in auth code so the flow is obvious in your app.
* Expo's WebBrowser docs recommend `openAuthSessionAsync` for auth-style browser flows and document `warmUpAsync()` / `coolDownAsync()` as Android performance helpers.
* On web, Expo documents that `maybeCompleteAuthSession()` is what closes the popup after the redirect page loads.

## References

* [Expo AuthSession docs](https://docs.expo.dev/versions/latest/sdk/auth-session/)
* [Expo WebBrowser docs](https://docs.expo.dev/versions/latest/sdk/webbrowser/)
* [Ave SDK OAuth reference](/sdk/sdk-oauth)
* [PKCE for public clients](/guides/pkce-for-public-clients)
* [Convex custom auth](/guides/convex-custom-auth)
