-
-
Save thedewpoint/181281f8cbec10378ecd4bb65c0ae131 to your computer and use it in GitHub Desktop.
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> | |
); | |
} |
Super helpful gist! A couple of security notes:
-
useAsyncStorage
is un-encrypted, so better to use a store likeexpo-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.
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.
@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)
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
Thank you so much for your great explanation!
Thank you. You saved me a lot of time!
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?