-
-
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> | |
); | |
} |
@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!
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.