Sign-in
How do I add OAuth 2.0 sign-in to a React Native app?
Use useAuthRequest from expo-auth-session to open the provider's sign-in page with PKCE and get an authorization code back. Once you have the code, exchange it for tokens (on your server if the provider needs a client secret), store them with expo-secure-store, and send the access token to your REST API as a Bearer header.
Updated October 31, 2026
OAuth 2.0 tutorials tend to open with a diagram full of boxes and arrows, which makes it feel bigger than it is. In a mobile app it comes down to three steps: open the provider’s sign-in page, get a one-time code back, and trade that code for an access token your REST API accepts. Here’s how we’d set it up in an Expo app with expo-auth-session.
How it works
Mobile apps use the authorization code flow with PKCE. Your app opens a browser to the provider’s authorize URL with your client ID and a redirect URI, the person signs in there, and the browser is sent back to your app’s URL scheme with a code. That code goes to the provider’s token endpoint and comes back as an access token, along with a refresh token if the provider issues one. From then on, every request to your REST API carries Authorization: Bearer <access token>, and the API checks that token before answering.
PKCE (Proof Key for Code Exchange) is a random secret created for each sign-in, and it’s on by default in expo-auth-session, so only the app that started the sign-in can exchange its code. Expo’s guide calls the older implicit flow, which returned a token straight in the redirect, no longer recommended.
How to add it
-
Install the packages:
npx expo install expo-auth-session expo-web-browser expo-secure-store -
Give your app a scheme in
app.json, such as"scheme": "myapp", and register the exact redirect URI with your provider. Test in a development build, because your app’s own scheme isn’t available in Expo Go, so the redirect has no way back to it. -
Start the sign-in and exchange the code:
import { exchangeCodeAsync, makeRedirectUri, useAuthRequest } from "expo-auth-session"; import * as SecureStore from "expo-secure-store"; import * as WebBrowser from "expo-web-browser"; import { useEffect } from "react"; import { Button } from "react-native"; WebBrowser.maybeCompleteAuthSession(); const clientId = "YOUR_CLIENT_ID"; const discovery = { authorizationEndpoint: "https://auth.example.com/oauth/authorize", tokenEndpoint: "https://auth.example.com/oauth/token", }; const redirectUri = makeRedirectUri({ scheme: "myapp", path: "auth" }); export function SignInButton() { const [request, response, promptAsync] = useAuthRequest( { clientId, redirectUri, scopes: ["openid", "profile"] }, discovery, ); useEffect(() => { if (response?.type !== "success" || !request?.codeVerifier) return; exchangeCodeAsync( { clientId, code: response.params.code, redirectUri, extraParams: { code_verifier: request.codeVerifier }, }, discovery, ).then((tokens) => SecureStore.setItemAsync("accessToken", tokens.accessToken)); }, [request, response]); return <Button title="Sign in" disabled={!request} onPress={() => promptAsync()} />; }Keep the button disabled until
requestis ready, as Expo’s guide recommends. -
Exchange the code on your server if the provider requires a client secret. Send the
code,request.codeVerifier, andredirectUrito an endpoint you control, and let it call the token endpoint with the secret. Never put a client secret in the app, because anyone can pull it out of the app bundle. -
Call your REST API with the token:
const token = await SecureStore.getItemAsync("accessToken"); const res = await fetch("https://api.example.com/me", { headers: { Authorization: `Bearer ${token}` }, });When the access token expires, use the refresh token with
refreshAsync(or your server) to get a new one, and store it the same way.
How you know it works
Tapping Sign in opens the provider’s page, signing in brings you straight back to the app, and your API call returns data instead of 401 Unauthorized.
If it doesn’t come back to the app
Check that the redirect URI registered with the provider matches the one in your code exactly, including the path, and that the same value goes into both the request and the exchange. Without a scheme in app.json, the sign-in completes but the result can’t get back into your app, so it shows up as canceled. An invalid_grant error from the token endpoint means the code was rejected, because it was already used, it expired, or the redirect URI or code_verifier didn’t match, so start a fresh sign-in.
Ask the agent
If your app is in Draftbit, paste this into the agent, and it can read your code and make the change for you.
Add sign-in with our OAuth 2.0 provider using expo-auth-session and PKCE. Store the tokens in secure storage, send the access token to our REST API on every request, and keep the client secret on the server, not in the app.