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>
);
}
@kishorevarma
Copy link

Hi there,

Thanks for the gist. its nice and easy to understand.

I have a doubt regarding promptAsync. As per my understanding every time app loads we check the storage.
if storage doesn't have the tokenString, then only we need to call promptAsync and show the login page.(this happens only for the first time user ever login)

but as per your code, prompt text will be shown even though there is a token available in storage. is it for testing purpose.

and also result will be null until we call promptAsync. In a normal use case we shouldn't call promptAsync anytime except when user login for first time or refresh token expired.

am I right?

@jdthorpe
Copy link

Super helpful gist! A couple of security notes:

  • useAsyncStorage is un-encrypted, so better to use a store like expo-secure-store

    import { setItemAsync, getItemAsync } from 'expo-secure-store';
    const AUTH_STORAGE_KEY = 'jwtToken'
    const getCachedToken = async (token: string) => setItemAsync(AUTH_STORAGE_KEY, token)
    const setToken = async () => getItemAsync(AUTH_STORAGE_KEY)
  • The useProxy option to promtAsync has been deprecated for security reasons, so you'll want to use another option.

@robpearmain
Copy link

Please could you update the example now useProxy has been deprecated

The useProxy option to promtAsync has been deprecated for security reasons, so you'll want to use another option.

@jdthorpe
Copy link

jdthorpe commented Apr 13, 2023

@robpearmain While your ask sounds a lot like "please do several hours of work for me for free", here's a gist that I created to work around the issues I highlighted above. Note that I had an additional requirement of making the tokens globally available in the app, so it also uses the awesome zustand library for that (which makes my example a little longer and may or may not meet your needs)

@robpearmain
Copy link

robpearmain commented Apr 13, 2023

Please could you update the example now useProxy has been deprecated

The useProxy option to promtAsync has been deprecated for security reasons, so you'll want to use another option.

Fair point @jdthorpe, thought it was a quick change, sorry.

so I took a look this afternoon and got it working..

If using Auth0 you need to pass in a path for the return url as well as the scheme.

// app.json scheme is robapp
// expo 47 const redirectUri = AuthSession.makeRedirectUri({ useProxy });
// expo 48
const redirectUri = AuthSession.makeRedirectUri({ scheme: 'robapp', path: 'root' });

in auth0 allowed return url I added

robapp://root

@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