Auth session
Add “Sign in with …” to your app through your identity provider’s web page: the app opens a
browser tab, the user signs in, the provider redirects back with a code, and you exchange it for a
token. @symbiote-native/auth-session ports
expo-auth-session (a
PKCE-based OAuth2 and OpenID Connect flow) to every SymbioteNative adapter, not just React, with
the lifecycle hooks in each framework’s own idiom.
Unlike most Expo packages, expo-auth-session ships no native code: it is a pure-JS flow built
on other Expo JS APIs. So this package ports its logic and depends on the packages that already
cover its building blocks: @symbiote-native/web-browser for the
authorization tab, @symbiote-native/crypto for PKCE, and
@symbiote-native/application for the default redirect scheme.
There is no expo-modules-core dependency and no native-link.json.
| OS platform | Support |
|---|---|
| iOS | live |
| Android | live |
| Framework adapter | Support |
|---|---|
| React | live |
| Vue | live |
| Angular | live |
| Svelte | live |
| Solid | live |
Installation
Section titled “Installation”npm install @symbiote-native/auth-sessionScaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --auth-session (or
add --auth-session in an existing app) installs and wires this for you - see
@symbiote-native/cli.
It pulls in @symbiote-native/web-browser, @symbiote-native/crypto and
@symbiote-native/application as regular dependencies. Those packages’ native autolinking wiring
still applies: follow How to: wire up an Expo native
module once per app if you have not.
Register a URL scheme for the redirect
Section titled “Register a URL scheme for the redirect”The provider redirects back into your app through a custom URL scheme, so the app has to own one
(for example myapp://). Register it in your iOS and Android projects, then rebuild. Without a
scheme the sign-in still completes, but the result cannot get back to your app and the user has to
close the browser tab by hand, which reads as a cancelled event. See the
navigation linking guide and the login-flow section of
Web browser.
Allow-list the same redirect URI at your provider. That list is what stops another app from posing as yours.
The hooks load the discovery document, build the request, and give you a promptAsync function.
Each returns [request, result, promptAsync], with request and result in the adapter’s own
reactive box: values in React, a ShallowRef in Vue, an accessor in Solid, { current } in Svelte,
a Signal in Angular. Arguments are plain values or refs in Vue and getters in Solid, Svelte and
Angular.
import { makeRedirectUri, useAuthRequest, useAutoDiscovery,} from '@symbiote-native/auth-session/react';
export default function SignIn() { const discovery = useAutoDiscovery('https://accounts.example.com'); const [request, result, promptAsync] = useAuthRequest( { clientId: 'my-client-id', redirectUri: makeRedirectUri({ scheme: 'myapp' }), scopes: ['openid', 'profile'], }, discovery, );
return ( <view> <text>{result?.type ?? 'idle'}</text> <button title="Sign in" disabled={!request} onPress={() => promptAsync()} /> </view> );}<script setup lang="ts">import { makeRedirectUri, useAuthRequest, useAutoDiscovery,} from '@symbiote-native/auth-session/vue';
const discovery = useAutoDiscovery('https://accounts.example.com');const [request, result, promptAsync] = useAuthRequest( { clientId: 'my-client-id', redirectUri: makeRedirectUri({ scheme: 'myapp' }), scopes: ['openid', 'profile'], }, discovery,);</script>
<template> <view> <text>{{ result?.type ?? 'idle' }}</text> <button title="Sign in" :disabled="!request" @press="promptAsync()" /> </view></template>import { Component } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { injectAuthRequest, injectAutoDiscovery, makeRedirectUri,} from '@symbiote-native/auth-session/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` <view> <text>{{ result()?.type ?? 'idle' }}</text> <button title="Sign in" [disabled]="!request()" (press)="promptAsync()" /> </view> `,})export class SignIn { private readonly discovery = injectAutoDiscovery(() => 'https://accounts.example.com'); private readonly hook = injectAuthRequest( () => ({ clientId: 'my-client-id', redirectUri: makeRedirectUri({ scheme: 'myapp' }), scopes: ['openid', 'profile'], }), () => this.discovery(), );
readonly request = this.hook[0]; readonly result = this.hook[1]; readonly promptAsync = this.hook[2];}Call the inject* functions in a field initializer (an injection context). They take
functions that read signals, and return signals.
<script lang="ts"> import { makeRedirectUri, useAuthRequest, useAutoDiscovery, } from '@symbiote-native/auth-session/svelte';
const discovery = useAutoDiscovery(() => 'https://accounts.example.com'); const [request, result, promptAsync] = useAuthRequest( () => ({ clientId: 'my-client-id', redirectUri: makeRedirectUri({ scheme: 'myapp' }), scopes: ['openid', 'profile'], }), () => discovery.current, );</script>
<view> <text>{result.current?.type ?? 'idle'}</text> <button title="Sign in" disabled={!request.current} onPress={() => promptAsync()} /></view>import { makeRedirectUri, useAuthRequest, useAutoDiscovery,} from '@symbiote-native/auth-session/solid';
export function SignIn() { const discovery = useAutoDiscovery(() => 'https://accounts.example.com'); const [request, result, promptAsync] = useAuthRequest( () => ({ clientId: 'my-client-id', redirectUri: makeRedirectUri({ scheme: 'myapp' }), scopes: ['openid', 'profile'], }), discovery, );
return ( <view> <text>{result()?.type ?? 'idle'}</text> <button title="Sign in" disabled={!request()} onPress={() => promptAsync()} /> </view> );}Call the accessors: request(), result().
When result.type is 'success', exchange result.params.code for a token (see below).
Without a hook: the plain classes
Section titled “Without a hook: the plain classes”The same flow, step by step, works anywhere:
import { AuthRequest, exchangeCodeAsync, makeRedirectUri } from '@symbiote-native/auth-session';
const discovery = { authorizationEndpoint: 'https://example.com/oauth/authorize', tokenEndpoint: 'https://example.com/oauth/token',};
const request = new AuthRequest({ clientId: 'my-client-id', redirectUri: makeRedirectUri({ scheme: 'myapp' }), scopes: ['openid', 'profile'],});
const result = await request.promptAsync(discovery);if (result.type === 'success') { const token = await exchangeCodeAsync( { clientId: 'my-client-id', code: result.params.code, redirectUri: request.redirectUri }, discovery, );}Google and Facebook
Section titled “Google and Facebook”useGoogleAuthRequest, useGoogleIdTokenAuthRequest and useFacebookAuthRequest (and the
inject* forms on Angular) preconfigure the endpoints for those providers. The Google hook
exchanges the code for a token itself when the code flow is used; the id token is then in
result.params.id_token. Off the web, useGoogleIdTokenAuthRequest follows the default code flow,
as upstream does.
| Hook (React, Vue, Solid, Svelte) | Angular | Description |
|---|---|---|
useAutoDiscovery(issuer) |
injectAutoDiscovery |
Fetches the provider’s discovery document from its issuer URL |
useLoadedAuthRequest(config, discovery, RequestClass) |
injectLoadedAuthRequest |
Builds a request object once the discovery document has loaded |
useAuthRequestResult(request, discovery, options?) |
injectAuthRequestResult |
Provides promptAsync and the latest result for a request |
useAuthRequest(config, discovery) |
injectAuthRequest |
The combined form: [request, result, promptAsync] |
useGoogleAuthRequest / useGoogleIdTokenAuthRequest |
injectGoogleAuthRequest / injectGoogleIdTokenAuthRequest |
Google code flow and id-token flow |
useFacebookAuthRequest |
injectFacebookAuthRequest |
Facebook sign-in |
Core exports
Section titled “Core exports”| Signature | Description |
|---|---|
makeRedirectUri(options?): string |
Builds the redirect URI. Pass native (a full URI, used off the web) or scheme. Throws if neither is given |
AuthRequest |
A PKCE authorization request. promptAsync(discovery) opens the tab and resolves the result; makeAuthUrlAsync(discovery) builds the URL; parseReturnUrl(url) parses a redirect; redirectUri holds the redirect |
exchangeCodeAsync(config, discovery) |
Exchanges an authorization code for tokens |
refreshAsync(config, discovery) |
Exchanges a refresh token for new tokens |
revokeAsync(config, discovery) |
Revokes a token |
fetchUserInfoAsync(config, discovery) |
Fetches the user info from the provider’s userinfo endpoint |
TokenResponse |
The parsed token response, with TokenResponse.isTokenFresh(token) and token.shouldRefresh() |
AccessTokenRequest, RefreshTokenRequest, RevokeTokenRequest |
The token endpoint request classes behind the functions above |
fetchDiscoveryAsync(issuer), resolveDiscoveryAsync(issuerOrDiscovery) |
Fetch a discovery document, or resolve an issuer-or-document value |
loadAsync(config, issuerOrDiscovery) |
Loads an AuthRequest for a config and an issuer or discovery document |
dismiss() |
Closes the browser tab |
AuthError, ResponseError, TokenError |
Error classes for authorization, response and token failures |
ResponseType, Prompt, CodeChallengeMethod, GrantType, TokenTypeHint |
Enums for request and token options |
makeRedirectUri options
Section titled “makeRedirectUri options”| Option | Type | Description |
|---|---|---|
native |
string | undefined |
A full redirect URI, used as-is on non-web platforms. Use it for production builds |
scheme |
string | undefined |
The URL scheme to build the URI from, such as myapp |
path |
string | undefined |
A path appended after the scheme |
queryParams |
Record<string, string> |
Query parameters added to the URI. Values that are null are left out |
isTripleSlashed |
boolean | undefined |
Use scheme:///path instead of scheme://path |
preferLocalhost |
boolean | undefined |
Replace an IPv4 address host with localhost |
makeRedirectUrinever reads an app manifest. Upstream infers a scheme from the Expo manifest; this project has no manifest. Passnativeorscheme, or it throws rather than guessing.- Never put secret keys in app code. A client secret in a mobile app is not secret. Keep it on your server, and expose an endpoint that makes the call for the client. PKCE exists so a public client needs no secret.
- Filter
AuthSessionredirects out of your own link handlers. An auth redirect is one more deep link. Ignore URLs your ownLinkinghandler or router sees that belong to the auth flow, for example by putting a recognizable marker in your ownredirectUri. - The Expo Go and
auth.expo.ioproxy flow is not ported. It rests onexpo-constants’ app-manifest concept, which this bare, Metro-only project does not have. - Upstream’s
Requestbase class is namedBaseRequestinternally. This avoids shadowing the globalRequesttype used byfetch. - Sign-in can only be verified on a device or simulator against a real provider. The headless tests cover the request, token, discovery, base64 and query-string logic.
Common questions
Section titled “Common questions”- Which redirect URI do I register with the provider? Print
makeRedirectUri({ scheme }). In a development build it ismy-scheme://redirect; Expo Go usesexp://.... Register exactly that string. - Can I omit the scheme? No. Upstream auto-detects it from
Constants.expoConfig, which@symbiote-native/constantsdoes not provide, so always passschemeexplicitly. requestisnulland the button does nothing.useAuthRequestloads the request asynchronously; disable the button untilrequestis set.- PKCE? On by default (
usePKCE). Passrequest.codeVerifierasextraParamswhen you exchange the code for tokens. - Redirect not returning to the app on iOS. See the Spotify report in the sources and check the scheme is registered in the native project.
Sources: Expo docs: AuthSession, Expo guide: authentication, expo/expo#10514, Auth0 PKCE with AuthSession.
How the wrapper works
Section titled “How the wrapper works”The upstream request, token and discovery logic is hand-ported into this package’s core/
(AuthRequest, the TokenRequest family, PKCE, base64, query params, the Google and Facebook
providers, and the hook controllers shared through createAuthRequestHooks). Each adapter supplies
only its lifecycle bindings for the hooks. Native pieces come from the packages it depends on, so
there is nothing to autolink for this package itself.