Skip to content
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

feat: Expo Auth without setting AUTH_URL. #1054

Merged
34 changes: 28 additions & 6 deletions apps/nextjs/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
import type { NextRequest } from "next/server";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";

import { GET as DEFAULT_GET, POST } from "@acme/auth";
import { GET as DEFAULT_GET, POST as DEFAULT_POST } from "@acme/auth";

import { env } from "~/env";

export const runtime = "edge";

const EXPO_COOKIE_NAME = "__acme-expo-redirect-state";
const AUTH_COOKIE_PATTERN = /authjs\.session-token=([^;]+)/;

/**
* Correct request.url for local development so that Expo can work. Does nothing in production.
* @param req The request to modify
* @returns The modified request.
*/
function rewriteRequestUrl(req: NextRequest) {
if (env.VERCEL) {
Wundero marked this conversation as resolved.
Show resolved Hide resolved
return req;
}
const host = req.headers.get("host");
const newURL = new URL(req.url);
newURL.host = host ?? req.nextUrl.host;
return new NextRequest(newURL, req);
Comment on lines +22 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to rewrite if we disable csrf in dev?

Copy link
Contributor Author

@Wundero Wundero Jun 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the generated callback URLs point to localhost:3000 no matter what. NextJS sets the nextUrl to the deployment URL, which is always localhost on development, but we need it to be the IP of the machine, i.e. 192.168.x.y so that it can properly route the request to the oauth provider and back. This function rewrites the URL to be the machine's IP, so that we can force nextauth to use that instead of the preset localhost.

Copy link
Contributor Author

@Wundero Wundero Jun 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having looked through authjs's source code, the callback url is generated by const baseUrl = env.AUTH_URL ?? request.url (pseudocode), so because we don't set AUTH_URL, we need to ensure request.url is set to the proper value.

EDIT: Reference code:

The code above uses the request URL's origin (and the request URL origin gets overwritten with AUTH_URL if it is set) to generate the callback URL, so it must be set to the desired return IP for expo to work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this means we have to set the ip at the oauth provider? Hmm...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just modifying the request URL seems to be sufficient. In my local testing, this has worked great both with and without the auth proxy server.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aight I'll try and test it out asap

}

export const POST = async (req: NextRequest) => {
// First step must be to correct the request URL.
req = rewriteRequestUrl(req);
return DEFAULT_POST(req);
};

export const GET = async (
req: NextRequest,
props: { params: { nextauth: string[] } },
) => {
// First step must be to correct the request URL.
req = rewriteRequestUrl(req);
const nextauthAction = props.params.nextauth[0];
const isExpoSignIn = req.nextUrl.searchParams.get("expo-redirect");
const isExpoCallback = cookies().get(EXPO_COOKIE_NAME);
Expand All @@ -34,7 +58,7 @@ export const GET = async (
const authResponse = await DEFAULT_GET(req);
const setCookie = authResponse.headers
.getSetCookie()
.find((cookie) => cookie.startsWith("authjs.session-token"));
.find((cookie) => AUTH_COOKIE_PATTERN.test(cookie));
const match = setCookie?.match(AUTH_COOKIE_PATTERN)?.[1];

if (!match)
Expand All @@ -51,5 +75,3 @@ export const GET = async (
// Every other request just calls the default handler
return DEFAULT_GET(req);
};

export { POST };
1 change: 1 addition & 0 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"dependencies": {
"@acme/db": "workspace:*",
"@auth/core": "0.31.0",
"@auth/drizzle-adapter": "^1.1.0",
"@t3-oss/env-nextjs": "^0.10.1",
"next": "^14.2.3",
Expand Down
8 changes: 8 additions & 0 deletions packages/auth/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import Discord from "next-auth/providers/discord";

import { skipCSRFCheck } from '@auth/core';

import { db } from "@acme/db/client";
import { Account, Session, User } from "@acme/db/schema";

Expand All @@ -23,8 +25,14 @@ const adapter = DrizzleAdapter(db, {
sessionsTable: Session,
});

// eslint-disable-next-line no-restricted-properties
const isSecureContext = process.env.NODE_ENV !== 'development';

export const authConfig = {
adapter,
// In development, we need to skip checks to allow Expo to work
skipCSRFCheck: isSecureContext ? undefined : skipCSRFCheck,
trustHost: !isSecureContext,
providers: [Discord],
callbacks: {
session: (opts) => {
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.