-
-
Save lawrencecchen/75fee3be74a8cc56aedae66b17d6d83c to your computer and use it in GitHub Desktop.
next-auth with remix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app/routes/api/auth/$.ts | |
import NextAuth from "~/lib/next-auth/index.server"; | |
export const { action, loader } = NextAuth({ | |
providers: [ | |
GoogleProvider({ | |
clientId: env.GOOGLE_CLIENT_ID, | |
clientSecret: env.GOOGLE_CLIENT_SECRET, | |
}), | |
], | |
secret: env.NEXTAUTH_SECRET, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app/lib/next-auth/index.server.ts | |
import { DataFunctionArgs, json, redirect } from "@remix-run/node"; | |
import cookie from "cookie"; | |
import { IncomingRequest, NextAuthOptions, Session } from "next-auth"; | |
import type { OutgoingResponse } from "next-auth/core"; | |
import { NextAuthHandler } from "next-auth/core"; | |
import type { NextAuthAction } from "next-auth/lib/types"; | |
import invariant from "tiny-invariant"; | |
import { nextAuthOptions } from "./options.server"; | |
async function toRemixResponse(nextAuthResponse: OutgoingResponse<any>) { | |
const { | |
headers: nextAuthHeaders, | |
cookies, | |
body: nextAuthBody, | |
redirect: nextAuthRedirect, | |
status = 200, | |
} = nextAuthResponse; | |
const headers = new Headers(); | |
nextAuthHeaders?.forEach((header) => { | |
headers.append(header.key, header.value); | |
}); | |
for (const item of cookies ?? []) { | |
headers.append( | |
"Set-Cookie", | |
cookie.serialize(item.name, item.value, item.options) | |
); | |
} | |
if (nextAuthRedirect) { | |
if (!nextAuthBody) { | |
throw redirect(nextAuthRedirect, { | |
status: 302, | |
headers, | |
}); | |
} else { | |
return json({ url: nextAuthRedirect, headers }); | |
} | |
} | |
if (headers.get("Content-Type") === "application/json") { | |
return json(nextAuthBody, { status, headers }); | |
} | |
return new Response(nextAuthBody, { status, headers }); | |
} | |
const NEXTAUTH_URL = process.env.VERCEL_URL ?? process.env.NEXTAUTH_URL; | |
async function RemixNextAuthHandler( | |
{ request, params }: DataFunctionArgs, | |
options: NextAuthOptions | |
) { | |
const url = new URL(request.url); | |
invariant(params["*"], "nextauth is required"); | |
const nextauth = params["*"].split("/"); | |
let body = {}; | |
try { | |
body = Object.fromEntries(await request.formData()); | |
} catch { | |
// no formData passed | |
} | |
const req: IncomingRequest = { | |
host: NEXTAUTH_URL, | |
body, | |
query: Object.fromEntries(url.searchParams), | |
headers: request.headers, | |
method: request.method, | |
cookies: cookie.parse(request.headers.get("cookie") ?? ""), | |
action: nextauth[0] as NextAuthAction, | |
providerId: nextauth?.[1], | |
error: nextauth?.[1], | |
}; | |
const response = await NextAuthHandler({ | |
req, | |
options, | |
}); | |
return toRemixResponse(response); | |
} | |
export default function NextAuth(options: NextAuthOptions) { | |
return { | |
loader: (args: DataFunctionArgs) => RemixNextAuthHandler(args, options), | |
action: (args: DataFunctionArgs) => RemixNextAuthHandler(args, options), | |
}; | |
} | |
export function createGetServerSession<T extends Session>( | |
options: NextAuthOptions | |
) { | |
async function getServerSession(request: Request): Promise<T | null> { | |
const session = await NextAuthHandler<T>({ | |
req: { | |
host: NEXTAUTH_URL, | |
action: "session", | |
method: "GET", | |
cookies: cookie.parse(request.headers.get("cookie") ?? ""), | |
headers: request.headers, | |
}, | |
options, | |
}); | |
const { body } = session; | |
if (body && Object.keys(body).length) return body as T; | |
return null; | |
} | |
return getServerSession; | |
} | |
export const getServerSession = createGetServerSession(nextAuthOptions); | |
export function getCurrentPath(request: Request) { | |
return new URL(request.url).pathname; | |
} | |
export function makeRedirectToFromHere(request: Request) { | |
return new URLSearchParams([["callbackUrl", getCurrentPath(request)]]); | |
} | |
export async function requireAuthSession( | |
request: Request, | |
options: { | |
onFailRedirectTo?: string; | |
} = {} | |
): Promise<Session> { | |
const session = await getServerSession(request); | |
if (!session) { | |
if (options.onFailRedirectTo) { | |
throw redirect( | |
`${options.onFailRedirectTo}?${makeRedirectToFromHere(request)}` | |
); | |
} | |
throw redirect(`/api/auth/signin?${makeRedirectToFromHere(request)}`); | |
} | |
return session; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is quite a cute implementation to wrap NextJS with Remix! 👏 Taking some ideas from this one....