skip to content

OAuth Sign-In Flows

Native sign-in runs authorization code with PKCE in the system browser (ASWebAuthenticationSession, Custom Tabs) and returns via a redirect URI. Interviewers ask why a WebView login is rejected.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In an Expo app, which expo-web-browser function runs an OAuth sign-in in the system browser, and what does it use on iOS and Android?

level: juniorimportance: must knowfreq 58%

answer

  1. not a WebView inside your app
  2. openAuthSessionAsync(url, redirectUrl)
  3. iOS: ASWebAuthenticationSession
  4. Android: Custom Tab plus a url listener
  5. success carries the redirect URL

basics

~20 s

WebBrowser.openAuthSessionAsync(authUrl, redirectUrl) from expo-web-browser. On iOS it runs ASWebAuthenticationSession; on Android it opens a Custom Tab and waits for the redirect deep link. It resolves with { type: 'success', url } or a cancel or dismiss result.

solid answer

~40 s

The function is `WebBrowser.openAuthSessionAsync(url, redirectUrl, options)` from `expo-web-browser`; `expo-auth-session`'s `promptAsync` calls it for you. On **iOS** it is a thin wrapper over **`ASWebAuthenticationSession`**, which runs the page in a system browser sheet and hands back the callback URL. On **Android** there is no equivalent system API, so the library opens the page in a **Custom Tab** and listens for a `Linking` `url` event that starts with the redirect URL. It resolves with `{ type: 'success', url }` when the redirect arrives; backing out gives `{ type: 'cancel' }` on iOS and `{ type: 'dismiss' }` on Android. The point of all this is that the corporate password is typed into a browser the app cannot script or read, which an embedded `react-native-webview` login cannot promise.

code

typescript · 9 lines
typescript
import * as WebBrowser from 'expo-web-browser';

export async function openCorporateSignIn(authUrl: string, redirectUrl: string): Promise<string | null> {
  const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUrl);
  if (result.type === 'success') {
    return result.url; // e.g. expenses://auth?code=...&state=...
  }
  return null; // 'cancel' on iOS, 'dismiss' on Android: the user backed out
}

go deeper

for a junior

Name the function, openAuthSessionAsync from expo-web-browser, and the rule that credentials never go through a WebView inside the app.

for a middle

Explain the platform split: ASWebAuthenticationSession on iOS, a Custom Tab plus a Linking listener on Android, and the result types each produces.

for a senior

Diagnose redirect problems: prefix mismatches on Android, missing scheme registration, concurrent sessions, and dismiss versus cancel handling.

for a principal

Decide between expo-auth-session and a fully native library such as react-native-app-auth, weighing Expo integration against provider quirks and maintenance.

## The function and its job In an Expo or bare React Native app with `expo-web-browser` installed, OAuth and OpenID Connect sign-in goes through **`WebBrowser.openAuthSessionAsync(url, redirectUrl, options)`**. You give it the identity provider's authorization URL and the redirect URL your app registered; it shows the provider's login page in a system browser surface and resolves once the browser is sent to that redirect URL. Most apps never call it directly: `expo-auth-session` builds the authorization URL, and its `promptAsync` calls `openAuthSessionAsync` underneath. Knowing what happens there is what interviewers probe. ## What runs on each platform | | iOS | Android | |---|---|---| | Browser surface | `ASWebAuthenticationSession`, a system sheet | a **Custom Tab** in the user's browser | | How the app learns it finished | the session's callback receives the redirect URL, matched on its scheme | the redirect arrives as a deep link; the library listens for a `Linking` `url` event that **starts with** the redirect URL | | User backs out | resolves `{ type: 'cancel' }` | resolves `{ type: 'dismiss' }` when the app returns to the foreground without a redirect | | Extra options | `preferEphemeralSession`, `preferUniversalLinks` | the regular browser options, used by the Custom Tab | The Android side is a **polyfill** built from `openBrowserAsync`, an `AppState` listener and a `Linking` listener, because Android has no system call that does the whole job. That is why the redirect must be a URL the app can actually receive, and why a redirect URL that does not match the prefix you passed never produces `success`. ## What it resolves with - `{ type: 'success', url }`: the browser reached the redirect URL; `url` carries the query string with `code` and `state`, or an `error`. - `{ type: 'cancel' }` (iOS): the user closed the sheet or declined. - `{ type: 'dismiss' }`: on Android, the user came back without a redirect; on iOS, the app closed the sheet with `dismissAuthSession()`. It resolves in all of these cases; your code branches on `type`. ## Android options that affect the flow Because the Android side is built on `openBrowserAsync`, the regular browser options apply to the Custom Tab: - `useProxyActivity` (default `true`) launches the browser through a transparent activity with a different task affinity, so the browser is not destroyed when the app goes to the background, which is the app's normal state while the Custom Tab is in front. - `createTask` (default `true`) opens the browser in a new task; `showInRecents` (default `false`) controls whether that task appears in the recents view. - `browserPackage` picks a specific browser, and `warmUpAsync()` asks the Custom Tabs service to warm up the preferred browser before the user taps Sign in. ## Why not a login screen in react-native-webview A React Native app can embed a web page with the community `react-native-webview` component, and a login page in it will render. The problem is that the **host app controls that WebView**: it can inject JavaScript, read what is typed and read cookies. The user has no way to know the corporate password is safe, the web view does not share the user's existing browser session, and many identity providers refuse embedded user agents. The protocol reasoning is standard OAuth guidance for native apps; the React Native takeaway is simply to use `openAuthSessionAsync` (or a native library such as `react-native-app-auth`) and never a WebView for credentials. ## An expense app, concretely 1. The user taps "Sign in with your company account". 2. `expo-auth-session` builds the authorization URL and calls `openAuthSessionAsync` with it and `expenses://auth`. 3. The corporate identity provider's page shows in the system browser; if the employee is already signed in there, it may skip the password. 4. The provider redirects to `expenses://auth?code=...&state=...`; the promise resolves with `success` and that URL. 5. The app exchanges the code for tokens. ## Common mistakes - Starting a second session while one is open: `expo-auth-session` answers with `{ type: 'locked' }`, and `expo-web-browser`'s Android path throws. - Passing a redirect URL that differs from the one the provider will actually use, so Android never matches it. - Treating `dismiss` on Android as an error rather than an ordinary back-out. - Testing in Expo Go, which cannot use your custom scheme for the redirect; use a development build.

  • Why does the Android implementation need a deep link at all when iOS does not?
    `ASWebAuthenticationSession` hands the callback URL straight back to the app. Android has no such system session, so `expo-web-browser` opens a Custom Tab and waits for the provider's redirect to reopen the app as a deep link, catching it with a `Linking` `url` listener. If the scheme is not registered, the redirect never reaches the app.
  • What does `expo-auth-session` add on top of `openAuthSessionAsync`?
    It builds the authorization URL from a config and discovery document, generates and checks `state`, creates the PKCE verifier and challenge, prevents concurrent sessions with a `locked` result, and parses the returned URL into `params` and an `error`.

saying these in an interview costs you the question

  • A react-native-webview login page is fine if it uses HTTPS.
  • openAuthSessionAsync rejects the promise when the user cancels.
  • Android uses ASWebAuthenticationSession through a compatibility layer.
  • The app can read the password typed into the system browser.
  • Expo Go is enough to test the full OAuth redirect.
open as a page

With expo-auth-session, how do useAuthRequest, promptAsync and exchangeCodeAsync fit together in an authorization-code sign-in with PKCE?

level: middleimportance: must knowfreq 55%

basics

~10 s

useAuthRequest builds the request (state plus a PKCE verifier and S256 challenge) and returns [request, response, promptAsync]. promptAsync opens the system browser; on success you pass response.params.code and request.codeVerifier to exchangeCodeAsync to get tokens.

open as a page

In an Expo app, what does expo-auth-session's makeRedirectUri return in a development build versus Expo Go, and why does that matter for OAuth?

level: middleimportance: should knowfreq 40%

basics

~20 s

In a development or production build makeRedirectUri returns your scheme URL, such as expenses://auth; in Expo Go it returns an exp:// URL tied to the dev server's address. Identity providers need a fixed, registered redirect, so OAuth is developed in a development build.

open as a page

In a React Native expense app using expo-auth-session, why does tapping Sign in right after logout sign the same corporate user back in without a password prompt?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Logout cleared the app's tokens, but the identity provider's session cookie still lives in the system browser, so the next authorization request completes silently. Fix it with prompt: Prompt.Login or SelectAccount, an ephemeral iOS session, or ending the provider session.

open as a page