From 15b4b60c9c61d90488e00ad521c2cf6aa8654563 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Thu, 29 May 2025 16:19:41 -0400 Subject: [PATCH] add functions to middleware for cross-sit authentication (#138) * add functions to middleware for cross-sit authentication * use constants for cross site auth route names --- actions/getIdentityData.ts | 4 +- app/api/auth/logout/route.ts | 5 +++ middleware.ts | 85 ++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) diff --git a/actions/getIdentityData.ts b/actions/getIdentityData.ts index fe1f56b4..7d8bc602 100644 --- a/actions/getIdentityData.ts +++ b/actions/getIdentityData.ts @@ -5,7 +5,9 @@ import { supabaseServerClient } from "supabase/serverClient"; export async function getIdentityData() { let cookieStore = await cookies(); - let auth_token = cookieStore.get("auth_token")?.value; + let auth_token = + cookieStore.get("auth_token")?.value || + cookieStore.get("external_auth_token"); let auth_res = auth_token ? await supabaseServerClient .from("email_auth_tokens") diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts index 5114fa4b..570801cc 100644 --- a/app/api/auth/logout/route.ts +++ b/app/api/auth/logout/route.ts @@ -1,6 +1,8 @@ import { NextRequest } from "next/server"; +import { supabaseServerClient } from "supabase/serverClient"; export const runtime = "edge"; export const preferredRegion = []; +export const dynamic = "force-dynamic"; export async function GET(req: NextRequest) { const host = req.headers.get("host"); @@ -13,6 +15,9 @@ export async function GET(req: NextRequest) { // Get the base domain from the host const domain = host?.includes(":") ? host.split(":")[0] : host; + let token = req.cookies.get("auth_token"); + if (token) + supabaseServerClient.from("email_auth_tokens").delete().eq("id", token); // Clear the auth_token cookie on both the base domain and the domain with a leading dot response.headers.append( diff --git a/middleware.ts b/middleware.ts index 5df60560..37288306 100644 --- a/middleware.ts +++ b/middleware.ts @@ -20,8 +20,15 @@ let supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, process.env.SUPABASE_SERVICE_ROLE_KEY as string, ); + +const auth_callback_route = "/auth_callback"; +const receive_auth_callback_route = "/receive_auth_callback"; export default async function middleware(req: NextRequest) { let hostname = req.headers.get("host")!; + if (req.nextUrl.pathname === auth_callback_route) return authCallback(req); + if (req.nextUrl.pathname === receive_auth_callback_route) + return receiveAuthCallback(req); + if (hostname === "leaflet.pub") return; if (req.nextUrl.pathname === "/not-found") return; let { data: routes } = await supabase @@ -34,6 +41,10 @@ export default async function middleware(req: NextRequest) { let pub = routes?.publication_domains[0]?.publications; if (pub) { + let cookie = req.cookies.get("external_auth_token"); + if (!cookie) { + return initiateAuthCallback(req); + } let aturi = new AtUri(pub?.uri); return NextResponse.rewrite( new URL( @@ -55,3 +66,77 @@ export default async function middleware(req: NextRequest) { } } } + +type CROSS_SITE_AUTH_REQUEST = { redirect: string }; +type CROSS_SITE_AUTH_RESPONSE = { redirect: string; auth_token: string | null }; +async function initiateAuthCallback(req: NextRequest) { + let token: CROSS_SITE_AUTH_REQUEST = { redirect: req.url }; + let payload = btoa(JSON.stringify(token)); + let signature = signCrossSiteToken(payload); + return NextResponse.redirect( + `https://leaflet.pub${auth_callback_route}?payload=${payload}&signature=${signature}`, + ); +} + +async function authCallback(req: NextRequest) { + let payload = req.nextUrl.searchParams.get("payload"); + let signature = req.nextUrl.searchParams.get("signature"); + + if (typeof payload !== "string") + return new NextResponse(null, { status: 401 }); + + let verifySig = await signCrossSiteToken(payload); + if (verifySig !== signature) return new NextResponse(null, { status: 401 }); + + let token: CROSS_SITE_AUTH_REQUEST = JSON.parse(atob(payload)); + let auth_token = req.cookies.get("auth_token")?.value || null; + let redirect_url = new URL(token.redirect); + let response_token: CROSS_SITE_AUTH_RESPONSE = { + redirect: token.redirect, + auth_token, + }; + + let response_payload = btoa(JSON.stringify(response_token)); + let sig = signCrossSiteToken(response_payload); + return NextResponse.redirect( + `https://${redirect_url.host}${receive_auth_callback_route}?payload=${response_payload}&signature=${sig}`, + ); +} + +async function receiveAuthCallback(req: NextRequest) { + let payload = req.nextUrl.searchParams.get("payload"); + let signature = req.nextUrl.searchParams.get("signature"); + + if (typeof payload !== "string") + return new NextResponse(null, { status: 401 }); + + let verifySig = await signCrossSiteToken(payload); + if (verifySig !== signature) return new NextResponse(null, { status: 401 }); + + let token: CROSS_SITE_AUTH_RESPONSE = JSON.parse(atob(payload)); + + let response = NextResponse.redirect(token.redirect); + response.cookies.set("external_auth_token", token.auth_token || "null"); + return response; +} + +const signCrossSiteToken = async (input: string) => { + if (!process.env.CROSS_SITE_AUTH_SECRET) + throw new Error("Environment variable CROSS_SITE_AUTH_SECRET not set "); + const encoder = new TextEncoder(); + const data = encoder.encode(input); + const secretKey = process.env.CROSS_SITE_AUTH_SECRET; + const keyData = encoder.encode(secretKey); + + const key = await crypto.subtle.importKey( + "raw", + keyData, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + + const signature = await crypto.subtle.sign("HMAC", key, data); + + return btoa(String.fromCharCode(...new Uint8Array(signature))); +}; -- 2.51.2