Open markybry opened 4 years ago
Yeah I would have thought that everything defined on these objects is spread out, for example audience
but found it didnt work on the config object nor extraParams
. I am also looking for a way to set audience...
I have been looking for an answer for the refresh token
for quite some time now. Ideally I would like to keep using Expo and not have to eject my app. Please let me know if anyone here has solved this issue. The user experience of logging in without a refresh token is quite terrible.
@Johnsgaard Is this possible to bypass AuthSession
altogether and use webbrowser directly? Maybe @EvanBacon has an idea on how to get refresh_token working with AuthSession
?
@Johnsgaard What about this article https://medium.com/@frewin.christopher/auth0-expo-and-react-native-authorization-code-grant-flow-with-pkce-d612d098f5f3
Using PKCE to retrieve a refresh token?
I have to look into documentation on PKCE. I tried to implement it but I don’t know what to do with the code coming back. I am savvy with the jwtdecode approach (id_token).
@msevestre Looking at that article I did that exact approach... with some minor tweaks because btoa
(base64 encoding) needed to use the react-native-base64
library. So with all of that I had a code being generated and the challenge and verification were working but in the end I didn't know what I needed to do with the code
coming back from the response.
It would be nice if I could just get the refresh token
back from the offline_access
added into the scope but that doesn't seem to be a valid solution. I just need a little bit more to get this working with my app I guess?
I created a package to handle this as part of a full-stack generator I'm working on. You can either yarn add cfs-expo-auth0
or copy the code
https://github.com/tiagob/create-full-stack/blob/master/packages/cfs-expo-auth0/src/index.tsx
This handles all three cases (no refresh token, refresh token, refresh token with rotation). You'll need to turn on offline access on your Auth0 API in your Auth0 console. It's also recommended you turn on token rotation in your Auth0 Native Client in your Auth0 console.
Thanks for sharing @tiagob. This is helpful! I would love to take a look at your full-stack generator if you have anything to share yet. What will it be using?
Feel free to try it out! https://github.com/tiagob/create-full-stack
Feedback is hugely appreciated. I'm still making changes to documentation and working on general polish.
Check out https://create-full-stack.com/docs/libraries_and_frameworks for what it's using
This is really great work @tiagob! I had a question about how you are handling the auth flow on the server and storing the access token. it looks like you are attaching the decoded token to req.user and passing it to the GraphQL server context for use in the resolvers. In the resolvers you are using using the sub field from the token as the uid in addition to an id in a lot of the crud operations like below.
await todoRepository.update({ id, uid: context.user.sub }, args);
How do you/ would you structure the data on the backend to store the uid for more complex apps with a User and other data models for example. Would you have a uid and id on every model like User model Todo model etc? I know there are different ways to go handle this, but I was curious how you would. Thanks for doing this work! This will be very useful for a lot of people.
Thank you @ymekuria !
The "user" on apollo-server-express backend is the decoded JWT. You can see the Auth0 docs for the specifics: https://auth0.com/docs/tokens/json-web-tokens/json-web-token-claims
sub
is the auth0 user id.
Yes, using this auth0 user id on other models makes sense. It's essentially a foreign key to the user. You could consider syncing the auth0 user to Postgres via an Auth0 rule and using this auth0 user id as a primary key. Then you'd have the user relationships established in your database (one-to-many, many-to-many) which makes it more stable.
Hey @tiagob
Quite a decent package cfs-expo-auth0
;) love it!
I am trying to use it in my React Native Expo + Auth0 environment
and I encountered this comment in your code:
// Official Expo Auth0 example doesn't handle refresh tokens
// https://github.com/expo/examples/tree/master/with-auth0
// This is adapted to handle no refresh tokens, refresh tokens and refresh
// tokens with rotation enabled
Not sure if I get it correctly, but I assume that it is intended to work anyways ;)
when I fire your hook
const { user, result, login } = useAuth0();
I can see any refresh token at all, all I can see is wired params in user
"params": Object {
"code": "v3XNhlbiV00uoaAG",
"state": "biFsLBwcdB",
},
besides that I am pretty sure that my Auth0 is properly configured to receive refresh_token
would be greatfull if you can share any thoughts or leads on that! I am desperately looking for a solution where I would get an access and refresh token ... thank you & have a great day!
Thanks!
useAuth0
hook provides the access token. You can do:
const { accessToken } = useAuth0();
The refresh token is internal to the package. You could copy the code and modify it yourself to provide that from the hook. https://github.com/tiagob/create-full-stack/blob/master/packages/cfs-expo-auth0/src/index.tsx#L161
Why do you need the refresh token?
@tiagob In my case I want to implement a SSO system for end users so they don’t have to keep logging in after hard closing the app. I haven’t fully had a chance to dive into this case for a while because it is just a personal app in my case. I have found it extremely painful to grab a refresh token and securely store it. I will have a look at your project very soon and I hope it solves my issues. Thank you in advance if it does!
@tiagob thanks for a very quick response ;) I want to use an auth flow that uses an access token and refresh token My understanding is: Access token grants me access to secure API Refresh token does not grant me any access per se, it lets me access a new access token with a new expiry date I hope it makes sense regards!
Ps: Line #161 that if in never triggered in my code Ps2: Did you had a chance to test your code in this particular auth flow?
The hook automatically updates with the new accessToken on expiration. It handles the refresh flow for you. That's why the refresh token is internal.
I tested with all the flows at the time of writing the package.
@tiagob I was looking over this again the other day and had a similar question to @Johnsgaard. From what I gather, the setTimeout function below is instantiated with the data from the Auth0Result after the user initially goes through the login flow, and is executed before the accessToken is expired, which in turn sets a new accessToken and instantiates a new setTimeout to execute when that expires and so on. This is great work that you did to take the pain of implementing a refresh flow. This seems like it will work great when a user has the app open and doesn't close it.
setTimeout(() => {
let refreshTokenData: TokenData | RefreshTokenData = data;
if (token.refresh_token) {
refreshTokenData = {
...data,
refresh_token: token.refresh_token,
grant_type: "refresh_token",
};
}
fetchAccessToken(
refreshTokenData,
domain,
setAccessToken,
setUser,
onTokenRequestFailure
);
}, token.expires_in * 1000 - requestNewAccessTokenBuffer);
My question @tiagob is how would you handle the case that @Johnsgaard mentioned to keep a user authenticated after they hard close/quit the app and then open it again without having to login again. I was thinking you could do something like storing the token using SecureStore before setting it into state after the setTimeout.
// Set state at the same time to trigger a single update on the context
// otherwise components are sent two separate updates
// SetSecureStore Here
setAccessToken(token.access_token);
setUser(userInfo);
And possibly rehydrate the accessToken from SecureStore in the useEffect of the Auth0 provider if there is no Auth0Result(ie no user login interaction) and the accessToken in the provider state is undefined. If the rehydrated token is about to expire, call fetchAccessToken.
useEffect(() => {
async function getToken() {
if (!auth0Result) {
// Rehydrate accessToken from SecureStore if its undefined and set it state to force the context rerender
// Possibly call fetchAccessToken function if token is about expire or force user to reauthenticate
return;
}
)
It seems like I would run into a problem calling fetchAccessToken without the code from a Auth0 result. The only other solution that comes to mind would be to extend the tokenExpiration on the Auth0 dashboard and force the user to reauthenticate once the token from SecureStore is about to expire. Did I miss something in your package that handles this offline persistence auth case? If not, are there anyways you would handle this case. Thanks again for all the work you put into this!
Adding to getToken() seems to work:
if (!auth0Result && !accessToken) {
const refresh_token = await SecureStore.getItemAsync(
"AUTH0_REFRESH_TOKEN"
);
if (!refresh_token) return;
const refreshTokenData: RefreshTokenData = {
client_id: clientId,
redirect_uri: redirectUri,
refresh_token,
grant_type: "refresh_token",
};
fetchAccessToken(
refreshTokenData,
domain,
setAccessToken,
setUser,
onTokenRequestFailure
);
}
and adding to fetchAccessToken
to store the refreshtoken if it is part of the token response:
if (token.refresh_token
{
await SecureStore.setItemAsync(
"AUTH0_REFRESH_TOKEN",
token.refresh_token
);
}
setAccessToken(token.access_token);
setUser(userInfo);
Thanks for this example @tiagob it really helped. I ended up rolling my own based on your example and adding secure storage and checking whether to fetch a new access token whenever the app state changes to active. I remember getting warnings in the past regarding long settimeouts in react native which seem to have disappeared now so possibly no longer an issue. However, I still have concerns that timers will keep android phones awake and cause battery drain.
I wonder what would be best to do with this issue? Ideally, we should get a PR for a new Auth0 example with refresh tokens I would have thought.
Hi @tiagob thanks for this package it really helped us out. My question is how many times the same code
that is obtained through /authorize
endpoint can be used to fetch access_token
through /oauth/token
endpoint. I am getting 403 error code when trying to fetch access_token for second time.
Update:
So tested this out and wasn't able to get the access_token
when calling the same endpoint i.e /oauth/token
twice with the same code. I tried the subsequent request by changing the grant_type: 'refresh_token'
and by sending refresh_token
along with it into the request body only then the response was successful. Due to this I had to enable to refresh token rotation in the dashboard which I wasn't willing to do but I guess that the only possible way. Let me know if there is anything that I might be missing.
@huzaifaali14 I believe you can only use the code once as it is part of the response to the user authenticating with the PKCE flow. The way I understand it is that the code is just an extra bit of security used during the initial authentication process. Subsequent requests would then require you to use the grant_type: 'refresh_token as you have said. It should be possible to set the expiration of a refresh token up to 1 year at which point your users would have to login again. You could alternatively set an inactivity timeout of something like 30 days. There should be no requirement to enable refresh token rotation.
I guess enabling the refresh token adds another layer of security as it invalidates the previous refresh token and assigns a new one on every request made to /oauth/token
. So if we are dealing with refresh token and re-authentication scenarios using refresh token so enabling it would be the right thing to do.
I would agree the current best practice is to have short lived access tokens and rotating refresh tokens that detect and prevent reuse. I think 30 days is standard for refresh tokens. The 30 days is then restarted every time a refresh token is issued.
I created this gist which shows how to do the refresh token flow with Auth0 while still using only the expo-auth-session lib and without having to write a bunch of extra code. I'm using regular async storage for writing the cache but you would want to use the secure storage as stated above. This was just a PoC https://gist.github.com/thedewpoint/181281f8cbec10378ecd4bb65c0ae131
embedding the code here for convenience
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>
);
}
@thedewpoint awesome work, really saved my bacon!
@thedewpoint would you be interested in creating a pull request to get the expo examples repo updated. https://github.com/expo/examples/blob/master/with-auth0/App.js
Refresh tokens and API access
It would be really helpful to have a reference implementation that deomostrates geting an access token for an API which includes a refresh token. Additionally how to use the refresh token with AuthSession (I'm not sure if this even possible at the moment). I would imagine this is a very common use case. Simply adding the offline scope doesn't seem to work.
Example of setting the audience. offline_access does not work however