Base IdP

Base IdP

Frameworks

React Native / Expo

Add Base IdP login to a React Native or Expo app — public client, PKCE, with expo-web-browser handling the system browser session.

React Native and Expo are public clients. The app opens a system browser through expo-web-browser, runs PKCE, and receives the authorization code on a custom URL scheme. The client id ships in the bundle; no secret ever does.

This guide covers the integration end to end: scaffolding, the TypeScript code, the Expo configuration, and what to do with the tokens once you have them.

Scaffold

npx base-idp create \
  --stack react-native \
  --client-id sq_live_yourapp \
  --redirect-uri "myapp://auth/callback"

This writes auth/baseIdp.ts, a .env, and a setup readme into ./base-idp-react-native/. Move the file into your project and follow the configuration steps below.

Install

npm install base-idp expo-web-browser expo-linking

If you are using bare React Native instead of Expo, swap expo-web-browser and expo-linking for react-native-inappbrowser-reborn and react-native-url-polyfill. The Base IdP browser SDK does not care which one you use — you only need a way to open the URL and receive the redirect.

The TypeScript side

auth/baseIdp.ts
import { createBrowserBaseIdpAuth } from "base-idp/browser";

export const auth = createBrowserBaseIdpAuth({
  clientId: process.env.EXPO_PUBLIC_BASE_IDP_CLIENT_ID!,
});

EXPO_PUBLIC_* variables are exposed to the bundle, which is the right place for a client id (not a secret).

.env
EXPO_PUBLIC_BASE_IDP_CLIENT_ID=sq_live_yourapp

Running the login

The browser SDK's loginWithRedirect does not actually open the browser on its own in a mobile context — it returns the URL, and you hand that URL to expo-web-browser. After the redirect, you pass the returned URL back to handleCallback.

auth/signIn.ts
import * as WebBrowser from "expo-web-browser";
import { auth } from "./baseIdp";

const REDIRECT_URI = "myapp://auth/callback";

export async function signIn() {
  const url = await auth.buildLoginUrl({
    redirectUri: REDIRECT_URI,
  });

  const result = await WebBrowser.openAuthSessionAsync(url, REDIRECT_URI);

  if (result.type !== "success" || !result.url) {
    throw new Error("Sign-in was cancelled");
  }

  return await auth.handleCallback(result.url);
}

openAuthSessionAsync opens the system browser, waits for the redirect to REDIRECT_URI, and resolves with the redirect URL. From there, the SDK's handleCallback parses the code, exchanges it for tokens, and returns the verified session.

Configuring the URL scheme

Expo and React Native both need to know your app handles the custom scheme.

Expo

In app.json (or app.config.ts):

app.json
{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "bundleIdentifier": "com.example.myapp",
      "infoPlist": {
        "CFBundleURLTypes": [
          { "CFBundleURLSchemes": ["myapp"] }
        ]
      }
    },
    "android": {
      "package": "com.example.myapp",
      "intentFilters": [
        {
          "action": "VIEW",
          "category": ["DEFAULT", "BROWSABLE"],
          "data": [{ "scheme": "myapp" }]
        }
      ]
    }
  }
}

After changing app.json, run npx expo prebuild so the native projects pick up the new scheme.

Bare React Native

For bare projects, do the same Info.plist and AndroidManifest.xml edits that the Flutter guide shows. The XML is identical.

A sign-in screen

app/sign-in.tsx
import { useState } from "react";
import { View, Text, Button } from "react-native";
import { signIn } from "@/auth/signIn";

export default function SignInScreen() {
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function onPress() {
    setBusy(true);
    setError(null);
    try {
      const session = await signIn();
      console.log("signed in as", session.principal.email);
      // Navigate to your home screen, or send the session to your backend.
    } catch (e: any) {
      setError(e.message ?? "Sign-in failed");
    } finally {
      setBusy(false);
    }
  }

  return (
    <View>
      {error && <Text>{error}</Text>}
      <Button title={busy ? "Signing in..." : "Sign in"} onPress={onPress} disabled={busy} />
    </View>
  );
}

Storing tokens

Use expo-secure-store (Expo) or react-native-keychain (bare RN). Never use AsyncStorage for tokens — it is unencrypted.

auth/tokenStore.ts
import * as SecureStore from "expo-secure-store";

export const tokenStore = {
  async save(access: string, refresh: string) {
    await SecureStore.setItemAsync("access_token", access);
    await SecureStore.setItemAsync("refresh_token", refresh);
  },
  async getAccess() {
    return SecureStore.getItemAsync("access_token");
  },
  async getRefresh() {
    return SecureStore.getItemAsync("refresh_token");
  },
  async clear() {
    await SecureStore.deleteItemAsync("access_token");
    await SecureStore.deleteItemAsync("refresh_token");
  },
};

Sending the session to your backend

Same pattern as Flutter: POST the session to your backend's exchange endpoint and let it issue its own session tokens.

auth/exchangeWithBackend.ts
export async function exchangeWithBackend(session: BaseIdpSession) {
  const response = await fetch("https://api.example.com/auth/idp-exchange", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      access_token: session.tokens.accessToken,
      refresh_token: session.tokens.refreshToken,
      principal: session.principal,
    }),
  });
  return response.json();
}

On the server side, see Express, Go, or Rust.

Refresh and logout

import { auth } from "./baseIdp";
import { tokenStore } from "./tokenStore";

export async function refresh() {
  const refreshToken = await tokenStore.getRefresh();
  if (!refreshToken) return;
  const tokens = await auth.refreshTokens(refreshToken);
  await tokenStore.save(tokens.accessToken, tokens.refreshToken);
}

export async function signOut() {
  const refreshToken = await tokenStore.getRefresh();
  if (refreshToken) {
    await auth.logout(refreshToken);
  }
  await tokenStore.clear();
}

Common problems

"An error occurred during sign-in"

The redirect URI in your code does not match the scheme registered with the OS. Check app.json, run npx expo prebuild, rebuild the app.

Web preview does not redirect back

Expo's web preview does not handle custom URL schemes. Test sign-in on a real device or simulator, not in Expo for web.

result.type === "cancel" immediately

The browser opened a URL with no scheme it recognizes. Check that the URL your code passes to openAuthSessionAsync is the authorize URL from buildLoginUrl, not your redirect URI.

Where to go next

On this page