Is it possible to use next-auth with a react native app? I originally started out using next.js for everything but wanted to switch to a native app with the REST API side in next.js/vercel.
The FAQ says:
It is not intended to be used in native applications on desktop or mobile applications, which typically use public clients (e.g. with client / secrets embedded in the application).
But it looks like apps are able to also use web browser based OAuth flows. Specifically I am trying to use Expo AuthSession.
I have managed to get the OAuth code using the example here. Logging in with google returns an object like this:
{
"authuser": "0",
"code": "<random code string>",
"prompt": "none",
"scope": "profile openid https://www.googleapis.com/auth/userinfo.profile",
"state": "<random string>",
}
So I was hoping I could then manually make a request to GET /api/auth/callback/google the same way the OAuth redirect would. It looks like the callback endpoint sets the state to the hashed CSRF token so I tried to make the request like this:
fetch("http://localhost:3000/api/auth/csrf")
.then((r) => r.json())
.then((r) => {
return Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
r.csrfToken
);
})
.then((csrf) => {
// response.params is the object from above
const params = { ...response.params, state: csrf };
return fetch(
`http://localhost:3000/api/auth/callback/google?${serialize(
params
)}`
);
});
Which produced a request like this:
http://localhost:3000/api/auth/callback/google?code=<random code string>&scope=profile%20openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&prompt=none&state=<hashed csrf token>
But I get the following errors:
[next-auth][error][OAUTH_GET_ACCESS_TOKEN_ERROR] [
{
statusCode: 400,
data: '{\n' +
' "error": "invalid_grant",\n' +
' "error_description": "Missing code verifier."\n' +
'}'
},
undefined,
undefined
]
https://next-auth.js.org/errors#oauth_get_access_token_error
[next-auth][error][OAUTH_GET_ACCESS_TOKEN_ERROR] [
{
statusCode: 400,
data: '{\n' +
' "error": "invalid_grant",\n' +
' "error_description": "Missing code verifier."\n' +
'}'
},
undefined,
'google',
'<random code string>'
]
https://next-auth.js.org/errors#oauth_get_access_token_error
[next-auth][error][OAUTH_GET_PROFILE_ERROR] [
{
statusCode: 401,
data: '{\n' +
' "error": {\n' +
' "code": 401,\n' +
' "message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",\n' +
' "status": "UNAUTHENTICATED"\n' +
' }\n' +
'}\n'
}
]
I don't have a lot of experience with this so I can't really tell if this is a reasonable / secure approach that is close to working or if I'm just going down completely the wrong path. I'd love to hear your thoughts on whether this is something next-auth can support or if this is out of scope for the project.
Documentation feedback
Documentation refers to searching through online documentation, code comments and issue history. The example project refers to next-auth-example.
Keen to understand if this is possible right now as well. We are evaluating next-auth and we're planning on writing a React Native app and this will be a major factor in our decision making.
Hi there, thank you for the great question!
So the short answer right now is I guess no, at least not out of the box. :-(
The longer answer is yes it's I think it's possible … but we don't have an example of how to do that - and probably things we could do to make this easier, such as not requiring the CSRF Token if it's a native app.
If I understand what you are doing correctly, then technically that seems valid at least for Google according to the docs. It's having an error getting the access token which suggests the provider is not configured correctly for the credentials it's using.
Maybe additional or different options on the provider object need to be configured in this scenario?
https://next-auth.js.org/configuration/providers#oauth-provider-options
Not all the options are documented I think, you might find some more poking around in the providers:
https://github.com/nextauthjs/next-auth/tree/main/src/providers
I'd love to dive into this but don't think I can provide meaningful help or a better answer right now, but I'm happy to leave this open until we do have a better answer. I'm also happy to take feature requests to make using it with React Native easier.
One thing to maybe check is how the OAuth credentials are configured in Google - there are often different ways of configuring an OAuth client and I'm not sure what the relevant options might be in this case.
FWIW, IMO the very best way to support OAuth sign in with an app, if you can do it, it is to run a website that persists the user data and lets people sign in. This has the downside of requiring a web site hosted somewhere but the upside of being more secure and reducing the amount of code in your app allowing you to persist user data across platforms. The only caveat with this approach is that you want to make sure that the callback to your app cannot be intercepted by another app. (Happy to go into that in more detail if anyone is interested).
Awesome, thanks for the detailed response and the links. I'll keep digging into this and update here with whatever I find.
I'm definitely interested in hearing more about that last option. Keeping the website running is not a problem for me.
Ok so a few updates...
1) It turns out I am using [email protected] so the CSRF checks were actually not even happening. I'm going to upgrade my app to 3.1 before I do much more with this. If that causes the CSRF checking to be a problem it looks like you can disable the CSRF checks by adding state: false to the provider. Probably not the right solution long term...
2) The expo authentication was using PKCE which is why I was getting the missing code verifier issue. Disabling that by adding usePKCE: false to useAuthRequest in expo got me past that error. Again, long term probably not a great choice, but at least for now it got me one step further.
3) The expo guide ends up generating a redirect uri of https://auth.expo.io/@<username>/<appname> (https://docs.expo.io/versions/latest/sdk/auth-session/#what-authexpoio-does-for-you) so I started getting a redirect_uri_mismatch error. Updating my google provider config to point to that seemed to solve the problem:
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
params: {
grant_type: "authorization_code",
redirect_uri: "https://auth.expo.io/@<username>/<appname>",
},
}),
Doing that, everything seems to work!
Here's what I ended up with in expo:
const discovery = useAutoDiscovery("https://accounts.google.com");
const [request, response, promptAsync] = useAuthRequest(
{
clientId: "<google client id>.apps.googleusercontent.com",
redirectUri: makeRedirectUri({
native: "<google client id>:/oauthredirect",
useProxy,
}),
usePKCE: false,
scopes: ["openid", "profile"],
prompt: Prompt.SelectAccount,
},
discovery
);
React.useEffect(() => {
if (response?.type === "success") {
fetch(
`http://localhost:3000/api/auth/callback/google?${serialize(response.params)}`
).then(() => {
fetch("http://localhost:3000/api/my_authenticated_route")
.then(r => r.json())
.then(console.log)
.catch(console.log);
});
}
}, [response]);
And with that I see the response body of the my_authenticated_route API. Lots of shortcuts taken so far, but at least it seems like things can work.
Oh congratulations - that's great to hear, thanks for sharing!
I wonder if we can turn something like this into a tutorial, so it's much easier for the next person. I appreciate this must have been quite a bit of digging!
FWIW using state: false is actually fine! It doesn't really come into play with NextAuth.js because it does other things that make it redundant as a security check - like the redirect callback and ignoring the redirect URL returned by the remote service and because it is a server side flow rather than a client side flow
However, the option is still supported and enabled by default for RFC compliance; there is some redundancy in the spec because of all the different flows (PKCE is similar, but is actually relevant in this case).
Disabling PKCE isn't ideal in the longer term, but I think is something we can help with! It is basically the same caveat with the other approach I was talking about above. I'll have a think and try and remember where I last got to with that…
Upgrade to 3.1 was super smooth. Added back the CSRF fetching code from the original post and next-auth seems happy with that (and passing a different value for the state does cause the expected Invalid state returned from oAuth provider error).
One big drawback to this approach is that you can only have either the web log in or the app log in working. If you try to log in to the web version with the above change to the provider.params.redirect_uri you get a redirect_uri_mismatch error because the sign in route doesn't respect that parameter the way the callback one does (https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/signin/oauth.js#L11 vs https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/oauth/callback.js#L210). If I update the sign in code to respect provider.params.redirect_uri (redirect_uri: provider.params.redirect_uri || provider.callbackUrl) then I get an error after logging in through the web ui which I assume is because expo is trying to redirect me back to the app but that fails.

It seems like this could be fixed if the GET /api/auth/callback/:provider route could take an optional redirect_uri query parameter that the app could pass in to override the default, which seems fine to me, but I don't know if that exposes other security risks.
I am going to keep going working on the app in this state and see if I encounter any other issues since this is still a pretty primitive test, then I'll come back and investigate re-adding PKCE. If you have any suggestions about how that might be solved that would be great.
Once I get this fully working I'll try to piece together all the final instructions into a single comment and we can put that into the documentation if everything looks good.
Ok so looks like there are a few more issues using some of the client API in react native. Specifically I am trying to use useSession, but there may be issues with other methods too.
1) In react native it seems like window is defined, but window.addEventListener is not. Importing next-auth/client throws an error that I was able to fix by setting window.addEventListener = function () {}; before that import. This stops the import from failing but obviously means you lose all the functionality that is based on that.
2) next-auth assumes that in a client environment you want to use relative URLs for API calls, but we will need to use absolute URLs.
3) All the client methods that change the session (signIn/signOut/others?) redirect you away and trigger full page refreshes, but those redirects are no-ops in react native. This means that you need to manually refresh the app after you log in / log out to see the updated UI. I tried to trigger re-renders in different parts of my app when the session changed but I couldn't get it to update.
4) localStorage is also not defined so anything that depends on that will not work either, eg: https://github.com/nextauthjs/next-auth/blob/main/src/client/index.js#L319
Quick update regarding
One big drawback to this approach is that you can only have either the web log in or the app log in working
Originally my plan was to have multiple instances of the provider I was using with different configuration settings and different ids, but I ran into issues where logging in with the same account across platforms created separate users in next-auth. I was able to get around this by adding a custom header to each client (in my case by patching fetch):
const originalFetch = fetch;
fetch = (url, options = {}) => {
return originalFetch(url, {
...options,
headers: {
...options.headers,
"next-auth-platform": 'ios',
},
});
};
And then in the next-auth configuration I choose the provider based on that header:
const getOptions = platform => ({
providers: [
platform === "ios"
? Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
params: {
grant_type: "authorization_code",
redirect_uri: "https://auth.expo.io/@<username>/<appname>",
},
})
: Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
})
],
/* database, callbacks, whatever */
});
export default (req: NextApiRequest, res: NextApiResponse) => {
const options = getOptions(req.headers["next-auth-platform"]);
return NextAuth(req, res, options);
};
Hi there! It looks like this issue hasn't had any activity for a while. It will be closed if no further activity occurs. If you think your issue is still relevant, feel free to comment on it to keep ot open. Thanks!
Hi there! It looks like this issue hasn't had any activity for a while. To keep things tidy, I am going to close this issue for now. If you think your issue is still relevant, just leave a comment and I will reopen it. (Read more at #912) Thanks!
Most helpful comment
Awesome, thanks for the detailed response and the links. I'll keep digging into this and update here with whatever I find.
I'm definitely interested in hearing more about that last option. Keeping the website running is not a problem for me.