Skip to content

Instantly share code, notes, and snippets.

@thedewpoint
Last active September 14, 2024 18:04
Show Gist options
  • Save thedewpoint/181281f8cbec10378ecd4bb65c0ae131 to your computer and use it in GitHub Desktop.
Save thedewpoint/181281f8cbec10378ecd4bb65c0ae131 to your computer and use it in GitHub Desktop.
Auth0 with refresh tokens and expo-auth-session
import { SafeAreaProvider } from 'react-native-safe-area-context';
import * as AuthSession from 'expo-auth-session';
import { RefreshTokenRequestConfig, TokenResponse, TokenResponseConfig } from 'expo-auth-session';
import jwtDecode from 'jwt-decode';
import { useEffect, useState } from 'react';
import { Alert, Platform, Text, TouchableOpacity } from 'react-native';
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
import * as React from 'react'
import * as WebBrowser from 'expo-web-browser';
const auth0ClientId = "<client ID>";
const domain = "https://<tenant>.us.auth0.com"
const authorizationEndpoint = `${domain}/authorize`;
const tokenEndpoint = `${domain}/oauth/token`;
const useProxy = Platform.select({ web: false, default: true });
const redirectUri = AuthSession.makeRedirectUri({ useProxy });
// allows the web browser to close correctly when using universal login on mobile
WebBrowser.maybeCompleteAuthSession();
export default function App() {
// storing our user token
const [user, setUser] = useState({});
// caching the token configuration, use secure storage in production app
const { getItem: getCachedToken, setItem: setToken } = useAsyncStorage('jwtToken')
// basic implementation, token response omitted because default auth flow is code.
// do NOT use token response because this starts the implicit flow and we cannot get a refresh token
const [request, result, promptAsync] = AuthSession.useAuthRequest(
{
redirectUri,
clientId: auth0ClientId,
scopes: ['openid', 'profile', 'offline_access'],
extraParams: {
audience: "<api audience>",
access_type: "offline"
},
},
{ authorizationEndpoint }
);
// function for reading token from storage and refreshing it, called from useEffect
const readTokenFromStorage = async () => {
// get the cached token config
const tokenString = await getCachedToken();
const tokenConfig: TokenResponseConfig = JSON.parse(tokenString);
if (tokenConfig) {
// instantiate a new token response object which will allow us to refresh
let tokenResponse = new TokenResponse(tokenConfig);
// shouldRefresh checks the expiration and makes sure there is a refresh token
if (tokenResponse.shouldRefresh()) {
// All we need here is the clientID and refreshToken because the function handles setting our grant type based on
// the type of request configuration (refreshtokenrequestconfig in our example)
const refreshConfig: RefreshTokenRequestConfig = { clientId: auth0ClientId, refreshToken: tokenConfig.refreshToken }
const endpointConfig: Pick<AuthSession.DiscoveryDocument, "tokenEndpoint"> = { tokenEndpoint }
// pass our refresh token and get a new access token and new refresh token
tokenResponse = await tokenResponse.refreshAsync(refreshConfig, endpointConfig);
}
// cache the token for next time
setToken(JSON.stringify(tokenResponse.getRequestConfig()));
// decode the jwt for getting profile information
const decoded = jwtDecode(tokenResponse.accessToken);
// storing token in state
setUser({ jwtToken: tokenResponse.accessToken, decoded })
}
};
useEffect(() => {
// read the refresh token from cache if we have one
readTokenFromStorage()
// boilerplate for promptasync example from expo
if (result) {
if (result.error) {
Alert.alert(
'Authentication error',
result.params.error_description || 'something went wrong'
);
return;
}
if (result.type === 'success') {
// we are using auth code flow, so get the response auth code
const code = result.params.code;
if (code) {
// function for retrieving the access token and refresh token from our code
const getToken = async () => {
const codeRes: TokenResponse = await AuthSession.exchangeCodeAsync(
{
code,
redirectUri,
clientId: auth0ClientId,
extraParams: {
code_verifier: request?.codeVerifier
}
},
{ tokenEndpoint }
)
// get the config from our response to cache for later refresh
const tokenConfig: TokenResponseConfig = codeRes?.getRequestConfig();
// get the access token to use
const jwtToken = tokenConfig.accessToken;
// caching the token for later
setToken(JSON.stringify(tokenConfig));
// decoding the token for getting user profile information
const decoded = jwtDecode(jwtToken);
setUser({ jwtToken, decoded })
}
getToken()
}
}
}
}, [result]);
return (
<SafeAreaProvider>
<TouchableOpacity onPress={() => promptAsync({ useProxy })}>
<Text>
Prompt
</Text>
</TouchableOpacity>
</SafeAreaProvider>
);
}
@robozb
Copy link

robozb commented Sep 7, 2023

Thank you so much for your great explanation!

@bingDBdu
Copy link

Thank you. You saved me a lot of time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment