Signing In With a Provider

Running the authorization code flow with PKCE through expo-auth-session, plus native Apple and Google sign-in.

Syntaxconst [request, response, promptAsync] = useAuthRequest(config, discovery)

Signing in with a provider means handing the user to that provider's own page, letting them authenticate there, and receiving a short-lived code your app trades for tokens. expo-auth-session runs that flow in the system browser and returns the result to the component that started it.

useAutoDiscovery(issuer) is the shorthand for writing the provider's endpoints yourself. The longhand is a plain object, { authorizationEndpoint, tokenEndpoint }; the hook fetches the same values from the issuer's well-known metadata. Either can be passed as the second argument to useAuthRequest.

PKCE

A mobile app cannot keep a client secret, so the code flow is used with PKCE. The app invents a random verifier, sends only its hash with the authorization request, and sends the verifier itself with the exchange. Only the app that opened the browser can finish the sign-in. usePKCE defaults to true, so the generation already happened; your job is passing request.codeVerifier into exchangeCodeAsync.

The redirect URI

This is where the time goes. makeRedirectUri() builds a URI from your app's scheme, and the provider's console must hold that exact string, character for character. It differs between Expo Go, a development build and a production build, so register each one you use.

Native sheets

Apple requires its own button wherever other providers appear; expo-apple-authentication supplies it, with a config plugin and a build. Google's native sheet comes from @react-native-google-signin/google-signin, which also needs a config plugin and a development build. Both hand back a token your server verifies.

Example

Example · tsx
import {
  exchangeCodeAsync,
  makeRedirectUri,
  useAuthRequest,
  useAutoDiscovery,
} from "expo-auth-session";
import { useEffect, useState } from "react";
import { Button, StyleSheet, Text, View } from "react-native";

const clientId = "my-native-client";
// Must match the provider's registered redirect URI exactly.
const redirectUri = makeRedirectUri({ scheme: "myapp", path: "redirect" });

export default function SignInScreen() {
  const discovery = useAutoDiscovery("https://auth.example.com/realms/app");
  const [status, setStatus] = useState("Not signed in");

  // responseType defaults to "code" and usePKCE defaults to true.
  const [request, response, promptAsync] = useAuthRequest(
    { clientId, redirectUri, scopes: ["openid", "profile", "offline_access"] },
    discovery
  );

  useEffect(() => {
    if (response?.type !== "success" || !discovery || !request?.codeVerifier) return;
    exchangeCodeAsync(
      {
        clientId,
        code: response.params.code,
        redirectUri,
        // The verifier proves this app opened the browser.
        extraParams: { code_verifier: request.codeVerifier },
      },
      discovery
    ).then((token) => setStatus(`Access token length: ${token.accessToken.length}`));
  }, [response, discovery, request]);

  return (
    <View style={styles.container}>
      <Text>{status}</Text>
      {/* request is null until the challenge is generated, so the button waits. */}
      <Button title="Continue with Example" disabled={!request} onPress={() => promptAsync()} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { alignItems: "center", flex: 1, gap: 16, justifyContent: "center" },
});

When to use it

  • Let users sign in with a company identity provider instead of a new password.
  • Add Sign in with Apple, which Apple requires once any other provider is offered.
  • Exchange an authorization code for tokens without shipping a client secret in the app.
  • Refresh a short-lived access token in the background before an API call.

More examples

Native Apple sign-in

A system-drawn black Sign in with Apple button; tapping it raises Apple's sheet, and on success the credential goes to your server. Assumes a sendToServer function elsewhere in the project. Apple returns fullName and email only on the user's first authorization of your app; on every later sign-in they are null, so persist them server-side the first time. AppleAuthenticationButton draws itself natively, which is why style cannot set backgroundColor or borderRadius.

Example · tsx
import * as AppleAuthentication from "expo-apple-authentication";
import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";

export default function AppleSignIn() {
  const [available, setAvailable] = useState(false);

  useEffect(() => {
    // False on Android and on older iOS versions.
    AppleAuthentication.isAvailableAsync().then(setAvailable);
  }, []);

  async function handlePress() {
    try {
      const credential = await AppleAuthentication.signInAsync({
        requestedScopes: [
          AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
          AppleAuthentication.AppleAuthenticationScope.EMAIL,
        ],
      });
      // identityToken is the JWT your server verifies against Apple's keys.
      await sendToServer(credential.identityToken, credential.fullName?.givenName ?? null);
    } catch {
      // ERR_REQUEST_CANCELED when the sheet is dismissed.
    }
  }

  if (!available) {
    return (
      <View style={styles.row}>
        <Text>Apple sign-in is not available on this device</Text>
      </View>
    );
  }

  return (
    <AppleAuthentication.AppleAuthenticationButton
      buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
      buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
      cornerRadius={8}
      onPress={handlePress}
      style={styles.button}
    />
  );
}

const styles = StyleSheet.create({
  button: { height: 44, width: 240 },
  row: { alignItems: "center", padding: 16 },
});

Refreshing an access token

Call this before a request that needs a valid access token, rather than waiting for a 401 and retrying. refreshAsync needs only the tokenEndpoint, so a hand-written discovery object is enough and saves the network round trip that useAutoDiscovery would make. The result is a new TokenResponse to persist; assume the refresh token rotated unless the provider documents otherwise.

Example · tsx
import { refreshAsync, type TokenResponse } from "expo-auth-session";

const clientId = "my-native-client";
const discovery = { tokenEndpoint: "https://auth.example.com/oauth/token" };

export async function ensureFresh(tokens: TokenResponse): Promise<TokenResponse> {
  // shouldRefresh() compares issuedAt + expiresIn against now, with a margin.
  if (!tokens.shouldRefresh() || !tokens.refreshToken) {
    return tokens;
  }
  return refreshAsync({ clientId, refreshToken: tokens.refreshToken }, discovery);
}

App config for provider sign-in

The scheme is what makeRedirectUri turns into myapp://redirect, so this file and the provider's console have to agree. Adding expo-apple-authentication to plugins writes the iOS entitlement during the build; usesAppleSignIn is the equivalent for a project that is not using the config plugin. Every change here needs a new development build, which is the usual reason a redirect that worked yesterday stops working after a scheme rename.

Example · json
{
  "expo": {
    "scheme": "myapp",
    "plugins": ["expo-apple-authentication"],
    "ios": {
      "bundleIdentifier": "com.example.myapp",
      "usesAppleSignIn": true
    },
    "android": {
      "package": "com.example.myapp"
    }
  }
}

Discussion

  • Be the first to comment on this lesson.