Learn how to utilize the expo-auth-session library to implement authentication with OAuth or OpenID providers.
Expo can be used to login to many popular providers on Android, iOS, and web. Most of these guides utilize the pure JS AuthSession
API, refer to those docs for more information on the API.
Here are some important rules that apply to all authentication providers:
WebBrowser.maybeCompleteAuthSession()
to dismiss the web popup. If you forget to add this then the popup window will not close.AuthSession.makeRedirectUri()
this does a lot of the heavy lifting involved with universal platform support. Behind the scenes, it uses expo-linking
.AuthSession.useAuthRequest()
, the hook allows for async setup which means mobile browsers won't block the authentication.request
is defined.promptAsync
in user interaction on the web.AuthSession can be used for any OAuth or OpenID Connect provider, we've assembled guides for using the most requested services! If you'd like to see more, you can open a PR or vote on canny.
OAuth 2 | OpenID
OAuth 2 | OpenID
OAuth 2 | OpenID
iOS Only
OAuth 2 | OpenID
OAuth 2 | OpenID
OAuth 2
OAuth 2 | OpenID
OAuth 2
OAuth 2
OAuth 2
Recaptcha
OAuth 2
OAuth 2 | OpenID
OAuth 2
OAuth 2 | OpenID
OAuth 2 | OpenID
OAuth 2
OAuth 2
OAuth 2
OAuth 2
OAuth 2
OAuth 2
OAuth 2
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
More Info | OpenID | Required | Available |
offline_access
isn't included then no refresh token will be returned.import * as React from 'react';
import { Button, Text, View } from 'react-native';
import * as AuthSession from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
WebBrowser.maybeCompleteAuthSession();
const redirectUri = AuthSession.makeRedirectUri();
export default function App() {
const discovery = AuthSession.useAutoDiscovery('https://demo.identityserver.io');
// Create and load an auth request
const [request, result, promptAsync] = AuthSession.useAuthRequest(
{
clientId: 'native.code',
redirectUri,
scopes: ['openid', 'profile', 'email', 'offline_access'],
},
discovery
);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="Login!" disabled={!request} onPress={() => promptAsync()} />
{result && <Text>{JSON.stringify(result, null, 2)}</Text>}
</View>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OpenID | Supported | Available |
Public Client
option in the console.import { useState, useEffect } from 'react';
import { StyleSheet, Text, View, Button, Alert } from 'react-native';
import * as AuthSession from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import jwtDecode from "jwt-decode";
WebBrowser.maybeCompleteAuthSession();
const redirectUri = AuthSession.makeRedirectUri();
const CLIENT_ID = "YOUR_CLIENT_ID";
export default function App() {
const discovery = AuthSession.useAutoDiscovery('https://api.asgardeo.io/t/<YOUR_ORG_NAME>/oauth2/token');
const [tokenResponse, setTokenResponse] = useState({});
const [decodedIdToken, setDecodedIdToken] = useState({});
const [request, result, promptAsync] = AuthSession.useAuthRequest(
{
redirectUri,
clientId: CLIENT_ID,
responseType: "code",
scopes: ["openid", "profile", "email"]
},
discovery
);
const getAccessToken = () => {
if (result?.params?.code) {
fetch(
"https://api.asgardeo.io/t/iamapptesting/oauth2/token",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `grant_type=authorization_code&code=${result?.params?.code}&redirect_uri=${redirectUri}&client_id=${CLIENT_ID}&code_verifier=${request?.codeVerifier}`
}).then((response) => {
return response.json();
}).then((data) => {
setTokenResponse(data);
setDecodedIdToken(jwtDecode(data.id_token));
}).catch((err) => {
console.log(err);
});
}
}
useEffect(() => {
(async function setResult() {
if (result) {
if (result.error) {
Alert.alert(
"Authentication error",
result.params.error_description || "something went wrong"
);
return;
}
if (result.type === "success") {
getAccessToken();
}
}
})();
}, [result]);
return (
<View style={styles.container}>
<Button title="Login" disabled={!request} onPress={() => promptAsync()} />
{decodedIdToken && <Text>Welcome {decodedIdToken.given_name || ""}!</Text>}
{decodedIdToken && <Text>{decodedIdToken.email}</Text>}
<View style={styles.accessTokenBlock}>
decodedToken && <Text>Access Token: {tokenResponse.access_token}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
accessTokenBlock: {
width: 300,
height: 500,
overflow: "scroll"
}
});
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OpenID | Supported | Available |
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import {
exchangeCodeAsync,
makeRedirectUri,
useAuthRequest,
useAutoDiscovery,
} from 'expo-auth-session';
import { Button, Text, SafeAreaView } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
export default function App() {
// Endpoint
const discovery = useAutoDiscovery(
'https://login.microsoftonline.com/<TENANT_ID>/v2.0',
);
const redirectUri = makeRedirectUri({
scheme: undefined,
path: 'auth',
});
const clientId = '<CLIENT_ID>';
// We store the JWT in here
const [token, setToken] = React.useState<string | null>(null);
// Request
const [request, , promptAsync] = useAuthRequest(
{
clientId,
scopes: ['openid', 'profile', 'email', 'offline_access'],
redirectUri,
},
discovery,
);
return (
<SafeAreaView>
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync().then((codeResponse) => {
if (request && codeResponse?.type === 'success' && discovery) {
exchangeCodeAsync(
{
clientId,
code: codeResponse.params.code,
extraParams: request.codeVerifier
? { code_verifier: request.codeVerifier }
: undefined,
redirectUri,
},
discovery,
).then((res) => {
setToken(res.accessToken);
});
}
});
}}
/>
<Text>{token}</Text>
</SafeAreaView>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get your config | OpenID | Supported | Available |
Set your Beyond Identity Authenticator Config's Invocation Type to Automatic.
If Automatic is selected, Beyond Identity will automatically redirect to your application using the Invoke URL (the App Scheme or Univeral URL pointing to your application).
import { useEffect } from 'react';
import { makeRedirectUri, useAuthRequest, useAutoDiscovery } from 'expo-auth-session';
import { Button } from 'react-native';
import { Embedded } from '@beyondidentity/bi-sdk-react-native';
export default function App() {
// Endpoint
const discovery = useAutoDiscovery(
`https://auth-${region}.beyondidentity.com/v1/tenants/${tenant_id}/realms/${realm_id}/applications/${application_id}`
);
// Request
const [request, response, promptAsync] = useAuthRequest(
{
clientId: `${client_id}`,
scopes: ['openid'],
redirectUri: makeRedirectUri({
scheme: 'your.app',
}),
},
discovery
);
useEffect(() => {
const authenticate = async url => {
// Display UI for user to select a passwordless passkey
const passkeys = await Embedded.getPasskeys();
if (await Embedded.isAuthenticateUrl(url)) {
// Pass url and a selected passkey ID into the Beyond Identity Embedded SDK authenticate function
const { redirectUrl } = await Embedded.authenticate(url, passkeys[0].id);
}
};
if (response?.url) {
authenticate(url);
}
}, [response]);
return (
<Button
disabled={!request}
title="Passwordless Login"
onPress={() => {
promptAsync();
}}
/>
);
}
import React from 'react';
import { Button } from 'react-native';
import { Embedded } from '@beyondidentity/bi-sdk-react-native';
export default function App() {
async function authenticate() {
const BeyondIdentityAuthUrl = `https://auth-${region}.beyondidentity.com/v1/tenants/${tenant_od}/realms/${realm_id}/applications/${application_id}/authorize?response_type=code&client_id=${client_id}&redirect_uri=${uri_encoded_redirect_uri}&scope=openid&state=${state}&code_challenge_method=S256&code_challenge=${pkce_code_challenge}`;
let response = await fetch(BeyondIdentityAuthUrl, {
method: 'GET',
headers: new Headers({
'Content-Type': 'application/json',
}),
});
const data = await response.json();
// Display UI for user to select a passwordless passkey
const passkeys = await Embedded.getPasskeys();
if (await Embedded.isAuthenticateUrl(data.authenticate_url)) {
// Pass url and selected Passkey ID into the Beyond Identity Embedded SDK authenticate function
const { redirectUrl } = await Embedded.authenticate(data.authenticate_url, passkeys[0].id);
}
}
return (
<Button
title="Passwordless Login"
onPress={authenticate}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OpenID | Supported | Not Available |
/token
endpoint requires a code_verifier
parameter which you can retrieve from the request before calling exchangeCodeAsync()
:extraParams: {
code_verifier: request.codeVerifier,
}
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { useAuthRequest, exchangeCodeAsync, revokeAsync, ResponseType } from 'expo-auth-session';
import { Button, Alert } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
const clientId = '<your-client-id-here>';
const userPoolUrl =
'https://<your-user-pool-domain>.auth.<your-region>.amazoncognito.com';
const redirectUri = 'your-redirect-uri';
export default function App() {
const [authTokens, setAuthTokens] = React.useState(null);
const discoveryDocument = React.useMemo(() => ({
authorizationEndpoint: userPoolUrl + '/oauth2/authorize',
tokenEndpoint: userPoolUrl + '/oauth2/token',
revocationEndpoint: userPoolUrl + '/oauth2/revoke',
}), []);
const [request, response, promptAsync] = useAuthRequest(
{
clientId,
responseType: ResponseType.Code,
redirectUri,
usePKCE: true,
},
discoveryDocument
);
React.useEffect(() => {
const exchangeFn = async (exchangeTokenReq) => {
try {
const exchangeTokenResponse = await exchangeCodeAsync(
exchangeTokenReq,
discoveryDocument
);
setAuthTokens(exchangeTokenResponse);
} catch (error) {
console.error(error);
}
};
if (response) {
if (response.error) {
Alert.alert(
'Authentication error',
response.params.error_description || 'something went wrong'
);
return;
}
if (response.type === 'success') {
exchangeFn({
clientId,
code: response.params.code,
redirectUri,
extraParams: {
code_verifier: request.codeVerifier,
},
});
}
}
}, [discoveryDocument, request, response]);
const logout = async () => {
const revokeResponse = await revokeAsync(
{
clientId: clientId,
token: authTokens.refreshToken,
},
discoveryDocument
);
if (revokeResponse) {
setAuthTokens(null);
}
};
console.log('authTokens: ' + JSON.stringify(authTokens));
return authTokens ? (
<Button title="Logout" onPress={() => logout()} />
) : (
<Button disabled={!request} title="Login" onPress={() => promptAsync()} />
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Supported | Not Available |
redirectUri
requires 2 slashes (://
).https://localhost:19006
expo start --web --https
to run with https, auth won't work otherwise.your-scheme://
expo.scheme: 'your-scheme'
, then added to the app code with makeRedirectUri({ native: 'your-scheme://' })
)https://yourwebsite.com
import {
exchangeCodeAsync,
makeRedirectUri,
TokenResponse,
useAuthRequest,
} from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import * as React from "react";
import { Button } from "react-native";
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: "https://www.coinbase.com/oauth/authorize",
tokenEndpoint: "https://api.coinbase.com/oauth/token",
revocationEndpoint: "https://api.coinbase.com/oauth/revoke",
};
const redirectUri = makeRedirectUri({ scheme: 'your.app'});
const CLIENT_ID = "CLIENT_ID";
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: CLIENT_ID,
scopes: ["wallet:accounts:read"],
redirectUri,
},
discovery
);
const {
// The token will be auto exchanged after auth completes.
token,
exchangeError,
} = useAutoExchange(
response?.type === "success" ? response.params.code : null
);
React.useEffect(() => {
if (token) {
console.log("My Token:", token.accessToken);
}
}, [token]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
type State = {
token: TokenResponse | null;
exchangeError: Error | null;
};
// A hook to automatically exchange the auth token for an access token.
// this should be performed in a server and not here in the application.
// For educational purposes only:
function useAutoExchange(code?: string): State {
const [state, setState] = React.useReducer(
(state: State, action: Partial<State>) => ({ ...state, ...action }),
{ token: null, exchangeError: null }
);
const isMounted = useMounted();
React.useEffect(() => {
if (!code) {
setState({ token: null, exchangeError: null });
return;
}
exchangeCodeAsync(
{
clientId: CLIENT_ID,
clientSecret: "CLIENT_SECRET",
code,
redirectUri,
},
discovery
)
.then((token) => {
if (isMounted.current) {
setState({ token, exchangeError: null });
}
})
.catch((exchangeError) => {
if (isMounted.current) {
setState({ exchangeError, token: null });
}
});
}, [code]);
return state;
}
function useMounted() {
const isMounted = React.useRef(true);
React.useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OpenID | Supported | Available |
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import * as AuthSession from 'expo-auth-session';
import { Button, View } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
const descopeProjectId = '<Descope Project ID>'; // Replace this with your Descope Project ID
const descopeUrl = `https://api.descope.com/${descopeProjectId}`;
const redirectUri = AuthSession.makeRedirectUri();
export default function App() {
const [authTokens, setAuthTokens] = React.useState(null);
const discovery = AuthSession.useAutoDiscovery(descopeUrl);
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: descopeProjectId,
responseType: AuthSession.ResponseType.Code,
redirectUri,
usePKCE: true,
scopes: ['openid', 'profile', 'email'],
},
discovery
);
React.useEffect(() => {
if (response) {
if (response.error) {
console.error(
'Authentication error',
response.params.error_description || 'something went wrong'
);
return;
}
if (response.type === 'success') {
const exchangeFn = async (exchangeTokenReq) => {
try {
const exchangeTokenResponse = await AuthSession.exchangeCodeAsync(
exchangeTokenReq,
discovery
);
setAuthTokens(exchangeTokenResponse);
} catch (error) {
console.error(error);
}
};
exchangeFn({
clientId: descopeProjectId,
code: response.params.code,
redirectUri,
extraParams: {
code_verifier: request.codeVerifier,
},
});
}
}
}, [discovery, request, response]);
const logout = async () => {
const revokeResponse = await AuthSession.revokeAsync(
{
clientId: descopeProjectId,
token: authTokens.refreshToken,
},
discovery
);
if (revokeResponse) {
setAuthTokens(null);
}
};
return (
<View>
{authTokens ? (
<Button title="Logout" onPress={logout} />
) : (
<Button
disabled={!request}
title="Login"
onPress={promptAsync}
/>
)}
</View>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Not Supported | Not Available |
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button, Platform } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://www.dropbox.com/oauth2/authorize',
tokenEndpoint: 'https://www.dropbox.com/oauth2/token',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
// There are no scopes so just pass an empty array
scopes: [],
redirectUri: makeRedirectUri({
scheme: 'your.app',
}),
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Supported | Not Available |
com.your.app://*
https://yourwebsite.com/*
redirectUri
requires 2 slashes (://
).import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button, Platform } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://www.fitbit.com/oauth2/authorize',
tokenEndpoint: 'https://api.fitbit.com/oauth2/token',
revocationEndpoint: 'https://api.fitbit.com/oauth2/revoke',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['activity', 'sleep'],
redirectUri: makeRedirectUri({
scheme: 'your.app'
}),
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Supported | Not Available |
com.your.app://*
https://yourwebsite.com/*
redirectUri
requires 2 slashes (://
).revocationEndpoint
is dynamic and requires your config.clientId
.import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://github.com/login/oauth/authorize',
tokenEndpoint: 'https://github.com/login/oauth/access_token',
revocationEndpoint: 'https://github.com/settings/connections/applications/<CLIENT_ID>',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['identity'],
redirectUri: makeRedirectUri({
scheme: 'your.app'
}),
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Supported | Not Available |
clientId
).import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button, Platform } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
const discovery = {
authorizationEndpoint: 'https://api.imgur.com/oauth2/authorize',
tokenEndpoint: 'https://api.imgur.com/oauth2/token',
};
export default function App() {
// Request
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
clientSecret: 'CLIENT_SECRET',
redirectUri: makeRedirectUri({
scheme: 'your.app',
}),
// imgur requires an empty array
scopes: [],
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
- | OpenID | Required | Available |
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest, useAutoDiscovery } from 'expo-auth-session';
import { Button, Text, View } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
export default function App() {
const discovery = useAutoDiscovery('https://YOUR_KEYCLOAK/realms/YOUR_REALM');
// Create and load an auth request
const [request, result, promptAsync] = useAuthRequest(
{
clientId: 'YOUR_CLIENT_NAME',
redirectUri: makeRedirectUri({
scheme: 'YOUR_SCHEME'
}),
scopes: ['openid', 'profile'],
},
discovery
);
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Button title="Login!" disabled={!request} onPress={() => promptAsync()} />
{result && <Text>{JSON.stringify(result, null, 2)}</Text>}
</View>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Sign-up > Applications | OpenID | Supported | Available |
redirectUri
, Okta will provide you with one.import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest, useAutoDiscovery } from 'expo-auth-session';
import { Button, Platform } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
export default function App() {
// Endpoint
const discovery = useAutoDiscovery('https://<OKTA_DOMAIN>.com/oauth2/default');
// Request
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['openid', 'profile'],
redirectUri: makeRedirectUri({
native: 'com.okta.<OKTA_DOMAIN>:/callback',
}),
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Supported | Not Available |
com.your.app://*
https://yourwebsite.com/*
redirectUri
requires 2 slashes (://
).import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://www.reddit.com/api/v1/authorize.compact',
tokenEndpoint: 'https://www.reddit.com/api/v1/access_token',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['identity'],
redirectUri: makeRedirectUri({
native: 'your.app://redirect',
}),
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Supported | Not Available |
redirectUri
requires 2 slashes (://
).redirectUri
can be defined under the "OAuth & Permissions" section of the website.clientId
and clientSecret
can be found in the "App Credentials" section.revocationEndpoint
is not available.import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://slack.com/oauth/authorize',
tokenEndpoint: 'https://slack.com/api/oauth.access',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['emoji:read'],
redirectUri: makeRedirectUri({
scheme: 'your.app'
}),
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Supported | Not Available |
https://localhost:19006
makeRedirectUri({ path: '/' })
.expo start --web --https
to run with https, auth won't work otherwise.your-scheme://
expo.scheme: 'your-scheme'
, then added to the app code with makeRedirectUri({ native: 'your-scheme://' })
)https://yourwebsite.com
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
tokenEndpoint: 'https://accounts.spotify.com/api/token',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['user-read-email', 'playlist-modify-public'],
// To follow the "Authorization Code Flow" to fetch token after authorizationEndpoint
// this must be set to false
usePKCE: false,
redirectUri: makeRedirectUri({
scheme: 'your.app'
}),
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Supported | Not Available |
com.bacon.myapp://redirect
the domain would be redirect
.import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://www.strava.com/oauth/mobile/authorize',
tokenEndpoint: 'https://www.strava.com/oauth/token',
revocationEndpoint: 'https://www.strava.com/oauth/deauthorize',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['activity:read_all'],
redirectUri: makeRedirectUri({
// the "redirect" must match your "Authorization Callback Domain" in the Strava dev console.
native: 'your.app://redirect',
}),
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Strava doesn't provide an implicit auth flow, you should send the code to a server or serverless function to perform the access token exchange. For debugging purposes, you can perform the exchange client-side using the following method:
const { accessToken } = await AuthSession.exchangeCodeAsync(
{
clientId: request?.clientId,
redirectUri,
code: result.params.code,
extraParams: {
// You must use the extraParams variation of clientSecret.
// Never store your client secret on the client.
client_secret: 'CLIENT_SECRET',
},
},
{ tokenEndpoint: 'https://www.strava.com/oauth/token' }
);
Website | Provider | PKCE | Auto Discovery | Scopes |
---|---|---|---|---|
Get your Config | OAuth | Supported | Not Available | Info |
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://id.twitch.tv/oauth2/authorize',
tokenEndpoint: 'https://id.twitch.tv/oauth2/token',
revocationEndpoint: 'https://id.twitch.tv/oauth2/revoke',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
redirectUri: makeRedirectUri({
scheme: 'your.app'
}),
scopes: ['user:read:email', 'analytics:read:games'],
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery | Scopes |
---|---|---|---|---|
Get your Config | OAuth | Supported | Not Available | Info |
response
of useAuthRequest
to always be {type: 'dismiss'}
.com.your.app://
expo start --https
): https://localhost:19006
(no ending slash)redirectUri
requires 2 slashes (://
).import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button, Platform } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: "https://twitter.com/i/oauth2/authorize",
tokenEndpoint: "https://twitter.com/i/oauth2/token",
revocationEndpoint: "https://twitter.com/i/oauth2/revoke",
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
redirectUri: makeRedirectUri({
scheme: 'your.app',
}),
usePKCE: true,
scopes: [
"tweet.read",
],
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Website | Provider | PKCE | Auto Discovery |
---|---|---|---|
Get Your Config | OAuth 2.0 | Supported | Not Available |
redirectUri
requires 2 slashes (://
).scopes
can be difficult to get approved.import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest } from 'expo-auth-session';
import { Button } from 'react-native';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://login.uber.com/oauth/v2/authorize',
tokenEndpoint: 'https://login.uber.com/oauth/v2/token',
revocationEndpoint: 'https://login.uber.com/oauth/v2/revoke',
};
export default function App() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'CLIENT_ID',
scopes: ['profile', 'delivery'],
redirectUri: makeRedirectUri({
scheme: 'your.app'
}),
},
discovery
);
React.useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params;
}
}, [response]);
return (
<Button
disabled={!request}
title="Login"
onPress={() => {
promptAsync();
}}
/>
);
}
Here are a few examples of some common redirect URI patterns you may end up using.
yourscheme://path
In some cases there will be anywhere between 1 to 3 slashes (/
).
Environment:
npx expo prebuild
eas build
or npx expo run:android
eas build
or npx expo run:ios
Create: Use AuthSession.makeRedirectUri({ native: '<YOUR_URI>' })
to select native when running in the correct environment.
your.app://redirect
-> makeRedirectUri({ scheme: 'your.app', path: 'redirect' })
your.app:///
-> makeRedirectUri({ scheme: 'your.app', isTripleSlashed: true })
your.app:/authorize
-> makeRedirectUri({ native: 'your.app:/authorize' })
your.app://auth?foo=bar
-> makeRedirectUri({ scheme: 'your.app', path: 'auth', queryParams: { foo: 'bar' } })
exp://u.expo.dev/[project-id]?channel-name=[channel-name]&runtime-version=[runtime-version]
-> makeRedirectUri()
scheme
property at least. The entire URL can be overridden in custom apps by passing the native
property. Often this will be used for providers like Google or Okta which require you to use a custom native URI redirect. You can add, list, and open URI schemes using npx uri-scheme
.expo.scheme
after ejecting then you'll need to use the expo apply
command to apply the changes to your native project, then rebuild them (yarn ios
, yarn android
).Usage: promptAsync({ redirectUri })
The "login flow" is an important thing to get right, in a lot of cases this is where the user will commit to using your app again. A bad experience can cause users to give up on your app before they've really gotten to use it.
Here are a few tips you can use to make authentication quick, easy, and secure for your users!
On Android you can optionally warm up the web browser before it's used. This allows the browser app to pre-initialize itself in the background. Doing this can significantly speed up prompting the user for authentication.
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
function App() {
React.useEffect(() => {
WebBrowser.warmUpAsync();
return () => {
WebBrowser.coolDownAsync();
};
}, []);
// Do authentication ...
}
Because there was no secure way to do this to store client secrets in your app bundle, historically, many providers have offered an "Implicit flow" which enables you to request an access token without the client secret. This is no longer recommended due to inherent security risks, including the risk of access token injection. Instead, most providers now support the authorization code with PKCE (Proof Key for Code Exchange) extension to securely exchange an authorization code for an access token within your client app code. Learn more about transitioning from Implicit flow to authorization code with PKCE.
expo-auth-session
still supports Implicit flow for legacy code purposes. Below is an example implementation of the Implicit flow.
import * as React from 'react';
import * as WebBrowser from 'expo-web-browser';
import { makeRedirectUri, useAuthRequest, ResponseType } from 'expo-auth-session';
WebBrowser.maybeCompleteAuthSession();
// Endpoint
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
};
function App() {
const [request, response, promptAsync] = useAuthRequest(
{
responseType: ResponseType.Token,
clientId: 'CLIENT_ID',
scopes: ['user-read-email', 'playlist-modify-public'],
redirectUri: makeRedirectUri({
scheme: 'your.app'
}),
},
discovery
);
React.useEffect(() => {
if (response && response.type === 'success') {
const token = response.params.access_token;
}
}, [response]);
return <Button disabled={!request} onPress={() => promptAsync()} title="Login" />;
}
On native platforms like iOS, and Android you can secure things like access tokens locally using a package called expo-secure-store
(This is different to AsyncStorage
which is not secure). This package provides native access to keychain services on iOS and encrypted SharedPreferences
on Android. There is no web equivalent to this functionality.
You can store your authentication results and rehydrate them later to avoid having to prompt the user to login again.
import * as SecureStore from 'expo-secure-store';
const MY_SECURE_AUTH_STATE_KEY = 'MySecureAuthStateKey';
function App() {
const [, response] = useAuthRequest({});
React.useEffect(() => {
if (response && response.type === 'success') {
const auth = response.params;
const storageValue = JSON.stringify(auth);
if (Platform.OS !== 'web') {
// Securely store the auth on your device
SecureStore.setItemAsync(MY_SECURE_AUTH_STATE_KEY, storageValue);
}
}
}, [response]);
// More login code...
}