-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Usage with react native #569
Comments
Keen to understand if this is possible right now as well. We are evaluating |
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? Not all the options are documented I think, you might find some more poking around in the 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...
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 |
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 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 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 It seems like this could be fixed if the 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
|
Quick update regarding
Originally my plan was to have multiple instances of the provider I was using with different configuration settings and different 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! |
@mjewell Did you ever get this working? I want to do something similar |
Yeah, I did! There's quite a bit of code and I'm using expo so there's some stuff specific to that in here too, but if you're not hopefully you can piece things together from here... Here's how: First, we need to set some stuff up at app initialization time. // initializers/hackNextAuth.js
import { DeviceEventEmitter } from "react-native";
// next-auth uses addEventListener, define that so focus/blur trigger next auth listeners
window.addEventListener = (...args) => DeviceEventEmitter.addListener(...args);
let codeVerifier = null;
export function setCodeVerifierHeader(verifier) {
codeVerifier = verifier;
}
console.log(`Using api at: ${process.env.API_URL}`);
let originalFetch = fetch;
// eslint-disable-next-line no-global-assign
fetch = (url, options = {}) => {
// turn relative urls into absolute urls on the API path
const modifiedUrl =
url.startsWith("http://") || url.startsWith("https://")
? url
: `${process.env.API_URL}/${url.startsWith("/") ? url.slice(1) : url}`;
if (codeVerifier) {
options.headers = { "my-app-code-verifier": codeVerifier, ...options.headers };
}
return originalFetch(modifiedUrl, options);
}; // initializers/addPlatformHeaders.js
import Constants from "expo-constants";
import { Platform } from "react-native";
const originalFetch = fetch;
// eslint-disable-next-line no-global-assign
fetch = (url, options = {}) => {
return originalFetch(url, {
...options,
headers: {
...options.headers,
"my-app-ownership": Constants.appOwnership,
"my-app-platform": Platform.OS,
},
});
}; Make sure you have an environment variable Then import these files before you do anything else (at the top of // App.js
import "./initializers/addPlatformHeaders";
import "./initializers/hackNextAuth";
// other imports
// other stuff Add log in buttons to your app. For google this looks something like this: // utils/exchangeToken.ts
import { setCodeVerifierHeader } from "@/initializers/hackNextAuth";
import { useAuthRequest } from "expo-auth-session";
import * as Crypto from "expo-crypto";
import { getCsrfToken } from "next-auth/client";
import qs from "qs";
import { DeviceEventEmitter } from "react-native";
export default async function exchangeToken(
providerId: string,
request: ReturnType<typeof useAuthRequest>[0],
response: ReturnType<typeof useAuthRequest>[1]
) {
setCodeVerifierHeader(request?.codeVerifier);
const csrf = await getCsrfToken();
const state = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
csrf
);
const params = { ...(response as any).params, state };
try {
await fetch(`/api/auth/callback/${providerId}?${qs.stringify(params)}`, {
redirect: "error",
});
} catch (e) {
// Redirects can cause errors that we can ignore
// TODO: distinguish these from failed logins and show an error
console.log(e);
} finally {
setCodeVerifierHeader(null);
}
DeviceEventEmitter.emit("focus");
} // google.tsx
import { Button } from "@/components/Button";
import {
Prompt,
makeRedirectUri,
useAuthRequest,
useAutoDiscovery,
} from "expo-auth-session";
import Constants from "expo-constants";
import React from "react";
import { Platform } from "react-native";
import exchangeToken from "./utils/exchangeToken";
function reverseDomain(domain: string) {
return domain.split(".").reverse().join(".");
}
const clientId =
Constants.appOwnership === "expo"
? process.env.GOOGLE_CLIENT_ID!
: Platform.select({
ios: process.env.GOOGLE_IOS_CLIENT_ID!,
android: process.env.GOOGLE_ANDROID_CLIENT_ID!,
default: process.env.GOOGLE_CLIENT_ID!,
});
const useProxy = Constants.appOwnership === "expo";
const native = Platform.select({
ios: `${reverseDomain(process.env.GOOGLE_IOS_CLIENT_ID!)}:/oauthredirect`,
android: `com.myapp.app:/oauthredirect`,
});
export default function GoogleLogIn() {
const discovery = useAutoDiscovery("https://accounts.google.com");
const [request, response, promptAsync] = useAuthRequest(
{
clientId,
redirectUri: makeRedirectUri({
native,
useProxy,
}),
scopes: ["openid", "profile", "email"],
// Optionally should the user be prompted to select or switch accounts
prompt: Prompt.SelectAccount,
},
discovery
);
React.useEffect(() => {
if (response?.type === "success") {
exchangeToken("google", request, response);
}
}, [response]);
return (
<Button
disabled={!request}
onPress={() => {
promptAsync({ useProxy });
}}
>
Login with Google
</Button>
);
} Remember to emit focus events whenever you sign in/out to make sure next-auth re-requests the token, e.g.: async function signOutAndRefresh() {
await signOut();
DeviceEventEmitter.emit("focus");
} That should be all for the app side of things. Then on the api side you want something like this: // [...nextauth].ts
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
function reverseDomain(domain: string) {
return domain.split(".").reverse().join(".");
}
const getOptions = (
ownership: string | string[] | undefined,
platform: string | string[] | undefined,
codeVerifier: string | string[] | undefined
) => ({
providers: [
ownership === "expo"
? 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/app-name",
},
})
: platform === "ios"
? Providers.Google({
clientId: process.env.GOOGLE_IOS_CLIENT_ID,
params: {
grant_type: "authorization_code",
redirect_uri: `${reverseDomain(
process.env.GOOGLE_IOS_CLIENT_ID!
)}:/oauthredirect`,
code_verifier: codeVerifier,
},
})
: platform === "android"
? Providers.Google({
clientId: process.env.GOOGLE_ANDROID_CLIENT_ID,
params: {
grant_type: "authorization_code",
redirect_uri: "com.myapp.app:/oauthredirect",
code_verifier: codeVerifier,
},
})
: Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
});
const handler = (req: NextApiRequest, res: NextApiResponse) => {
const options = getOptions(
req.headers["my-app-ownership"],
req.headers["my-app-platform"],
req.headers["my-app-code-verifier"]
);
return NextAuth(req, res, options);
};
export default handler; That should allow you to log in through the web, with expo, or in native apps all based on what the headers say. If you don't need all that you can remove the corresponding providers. One final weird thing is that on android if you open the app, press back to close it, then reopen the app, you'll get stuck with no session in a loading state. It looks like this will fix that for you facebook/react-native#13775 (comment). There might be a better solution for that, I'm not really sure what other implications it has. Hopefully that's helpful. I might have forgotten some stuff I've done along the way (it has been a while and I didn't keep great docs about what I chose to do). Let me know if anything doesn't work and I can look into it. |
Thanks @mjewell ! This is extraordinarily helpful. Documenting my progress so far:
I'm still getting the following error; will have to look into it tomorrow, but if you have any thoughts about what might be causing this I would love to hear them
|
Are you using version next-auth@3.1? Also, what provider are you using? |
Yeah, I'm on 3.1. Using Google. |
I'm trying to do something similar - it would be great if we could get a full-blown example in the docs after we pin down the exact steps needed.
|
Did you set up all those environment variables? For me they look like this on the backend:
And you need the three client ids defined in your app environment variables too. Make sure all those URLs look like what you would expect. I haven't hit the import { setCodeVerifierHeader } from "@/initializers/hackNextAuth";
import { useAuthRequest } from "expo-auth-session";
// import * as Crypto from "expo-crypto";
// import { getCsrfToken } from "next-auth/client";
import qs from "qs";
import { DeviceEventEmitter } from "react-native";
import * as Sentry from "sentry-expo";
export default async function exchangeToken(
providerId: string,
request: ReturnType<typeof useAuthRequest>[0],
response: ReturnType<typeof useAuthRequest>[1]
) {
setCodeVerifierHeader(request?.codeVerifier);
// CSRF was inconsistently matching causing intermittent failures, can be ignored
// https://github.com/nextauthjs/next-auth/issues/569#issuecomment-672968577
// const csrf = await getCsrfToken();
// const state = await Crypto.digestStringAsync(
// Crypto.CryptoDigestAlgorithm.SHA256,
// csrf
// );
// const params = { ...(response as any).params, state };
const params = { ...(response as any).params };
try {
const response = await fetch(
`/api/auth/callback/${providerId}?${qs.stringify(params)}`
);
if (response.url.includes("/error")) {
throw new Error(`Authentication Failed: ${response.url}`);
}
} catch (e) {
// Using localhost, we will hit Network Request Failed for this request even when its
// successful on android device/simulator and ios device, but not ios simulator
// TODO: this will also hide legitimate failures locally, find a way to distinguish
const isLocalhostIssue =
process.env.API_URL!.includes("://192.168") &&
e.message === "Network request failed";
if (isLocalhostIssue) {
console.log(e);
// fall through as though we succeeded
} else {
Sentry.Native.captureException(e);
throw e;
}
} finally {
setCodeVerifierHeader(null);
}
DeviceEventEmitter.emit("focus");
} and added
|
Thanks @mjewell ! It turns out that the issue I was having is that Expo doesn't like when you console.log the response from fetch, and I was confusing that for an actual error with the fetch. Would be possible for you to share a snippet where you are using |
I'm actually not sure how it works, it seems like the cookie is correctly being sent by the app. I read that cookies are unreliable in apps but it seems to be working fine for me. I'm not really sure what to even show a snippet of, I just render the |
Okay, I have created a working standalone Expo app which should work as a starting point for anyone who wants to try this in the future I found that my cookies were not being cleared between logins, causing authentication problems because it was sending stale session tokens. This app just clears all cookies before logging in each time which is probably a bad solution but it works as a starting point. Thanks so much for your help @mjewell ! |
@iaincollins Would you mind chiming in here and give your insights about the proposed solution, or what's the best way of achieving a unified authentication process in your opinion? Ideally, I think everyone would love to be able to use the simplicity of a For everyone interested, I've opened a feature request on the expo's side to also get their opinion on that. |
Glad I could help. @Xodarap Can you describe a bit more what the issue was? I have been using it this way for a while and haven't had issues. |
I'm honestly not sure; I've experienced this with normal next auth once or twice as well. Basically, somehow the browser gets multiple copies of the session token cookie, and then next auth only looks at the first one (which I think is the oldest), so it doesn't work. I assume it's supposed to be impossible to have multiple cookies at the same time; I'm not sure how it happens. |
I ended up putting this project on hold but it seemed like it was working fine. I didn't try it without Expo but I assume there shouldn't be too many changes needed. |
Any updates on this? I'd like to have Next.js's API routes as a backend for my React Native app. |
Any updates on this? |
@mjewell Could you provide a link to your project please? Thanks! |
Is this broken in version 4? The provider functions no longer contain |
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:
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:
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: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:
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.
The text was updated successfully, but these errors were encountered: