I am using NextAuth.js in my Next.js app to authenticate users via Google Provider and magic link. This works perfectly in a browser but not so well in a native wrapper using Capacitor JS – https://capacitorjs.com/.
To provide some context, here is my setup for authentication. I’m using MongoDB to store user accounts:
// [...nextauth.js]
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import EmailProvider from "next-auth/providers/email";
import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
import clientPromise from "@/lib/mongodb";
import { AuthVerification } from "@/emails/sign-in";
const THIRTY_DAYS = 30 * 24 * 60 * 60;
const THIRTY_MINUTES = 30 * 60;
const adapterOptions = {
databaseName: "accounts",
};
export const authOptions = (req) => ({
pages: {
verifyRequest: "/auth/verify-request",
},
secret: process.env.NEXTAUTH_SECRET,
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
EmailProvider({
sendVerificationRequest({ identifier, url, provider }) {
AuthVerification({ identifier, url, provider });
},
}),
],
callbacks: {
async session({ session, user }) {
if (session?.user) {
session.user.id = user.id;
session.user.filters = user.filters;
session.user.admin = user.admin;
session.user.stripeCustomerId = user.stripeCustomerId;
session.user.plus = user.plus;
}
return session;
},
},
adapter: MongoDBAdapter(clientPromise, adapterOptions),
});
export default async function auth(req, res) {
return await NextAuth(req, res, authOptions(req), {
debug: true,
});
}
I am triggering the sign in functions via:
// Google Provider
signIn('google', {
callbackUrl: process.env.NEXT_PUBLIC_BASE_URL,
});
// Email Provider
signIn("email", {
redirect: false,
callbackUrl: process.env.NEXT_PUBLIC_BASE_URL,
email,
})
The issue stems from the fact that in Capacitor, the Google Provider opens in a new browser window so the session-token
cookie required for authentication wasn’t passed back to the app.
I decided to setup deep linking and tried to make it work through the Capacitor Browser plugin (https://capacitorjs.com/docs/apis/browser) using this custom signIn
function:
async function signIn() {
const { url } = await fetch(
`${
process.env.NEXT_PUBLIC_BASE_URL
}/api/auth/signin/google`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
json: "true",
callbackUrl: process.env.NEXT_PUBLIC_BASE_URL,
}),
redirect: "follow",
credentials: "include",
},
);
await Browser.open({ url: url });
}
This generated the correct auth URL and opened in the in-app browser, however once authenticated it just loaded the callbackUrl
inside the in-app browser rather than deep-linking back to the app.
After much trial and error, I gave up and ended up focusing solely on the email provider route.
The email generated a URL that successfully deep linked into the app so I caught the link containing the email and token using the Capacitor listener appUrlOpen
and hit the api/auth/callback/email
route manually:
async function authUser(token, email) {
try {
const response = await fetch(
`${
process.env.NEXT_PUBLIC_BASE_URL
}/api/auth/callback/email?token=${token}&email=${email}`,
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
json: "true",
}),
credentials: "include",
},
);
if (response.ok) {
router.reload();
}
} catch (error) {
console.error("Error during authentication:", error);
}
}
useEffect(() => {
App.addListener("appUrlOpen", (event: URLOpenListenerEvent) => {
const authUrl = new URL(event.url);
const params = new URLSearchParams(authUrl.search);
const tokenRegex = /token=([^&]+)/;
const match = tokenRegex.exec(event.url);
if (match && match[1]) {
const token = match[1];
authUser(token, params.get("email"));
}
});
return () => {
App.removeAllListeners();
};
}, [router]);
This worked great – On success it reloaded the router and the user is authenticated. It also remained authenticated when I exited and reloaded the app so all seemed to be working perfect.
If I inspect the cookies, I can see that the three cookies are all present as they should be:
__Host-next-auth.csrf-token
__Secure-next-auth.callback-url
__Secure-next-auth.session-token
However, the issue comes when I logout. The session-token
cookie disappears as it should but then if I try to log back in it just reloads the router but remains unauthenticated.
If I reload the app then the session-token
cookie sometimes appears again but I still can’t login. If I manually delete the cookies and try again then it works so I’m thinking that the session-token
cookie isn’t getting properly removed on signOut
maybe?
For reference, I am obtaining the session info on the client-side using the following way:
import { useSession } from "next-auth/react";
const { data: session } = useSession();
This issue doesn’t happen in Xcode simulator by the way, only when testing on a connected iOS device or when distributed via TestFlight.
Any help or guidance would be greatly appreciated – I’ve been banging my head against the wall for days on this!
Thanks.