a tool for shared writing and social publishing

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

authored by awarm.space and committed by

GitHub 15b4b60c a8116a5f

+93 -1
+3 -1
actions/getIdentityData.ts
··· 5 5 6 6 export async function getIdentityData() { 7 7 let cookieStore = await cookies(); 8 - let auth_token = cookieStore.get("auth_token")?.value; 8 + let auth_token = 9 + cookieStore.get("auth_token")?.value || 10 + cookieStore.get("external_auth_token"); 9 11 let auth_res = auth_token 10 12 ? await supabaseServerClient 11 13 .from("email_auth_tokens")
+5
app/api/auth/logout/route.ts
··· 1 1 import { NextRequest } from "next/server"; 2 + import { supabaseServerClient } from "supabase/serverClient"; 2 3 export const runtime = "edge"; 3 4 export const preferredRegion = []; 5 + export const dynamic = "force-dynamic"; 4 6 5 7 export async function GET(req: NextRequest) { 6 8 const host = req.headers.get("host"); ··· 13 15 14 16 // Get the base domain from the host 15 17 const domain = host?.includes(":") ? host.split(":")[0] : host; 18 + let token = req.cookies.get("auth_token"); 19 + if (token) 20 + supabaseServerClient.from("email_auth_tokens").delete().eq("id", token); 16 21 17 22 // Clear the auth_token cookie on both the base domain and the domain with a leading dot 18 23 response.headers.append(
+85
middleware.ts
··· 20 20 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, 21 21 process.env.SUPABASE_SERVICE_ROLE_KEY as string, 22 22 ); 23 + 24 + const auth_callback_route = "/auth_callback"; 25 + const receive_auth_callback_route = "/receive_auth_callback"; 23 26 export default async function middleware(req: NextRequest) { 24 27 let hostname = req.headers.get("host")!; 28 + if (req.nextUrl.pathname === auth_callback_route) return authCallback(req); 29 + if (req.nextUrl.pathname === receive_auth_callback_route) 30 + return receiveAuthCallback(req); 31 + 25 32 if (hostname === "leaflet.pub") return; 26 33 if (req.nextUrl.pathname === "/not-found") return; 27 34 let { data: routes } = await supabase ··· 34 41 35 42 let pub = routes?.publication_domains[0]?.publications; 36 43 if (pub) { 44 + let cookie = req.cookies.get("external_auth_token"); 45 + if (!cookie) { 46 + return initiateAuthCallback(req); 47 + } 37 48 let aturi = new AtUri(pub?.uri); 38 49 return NextResponse.rewrite( 39 50 new URL( ··· 55 66 } 56 67 } 57 68 } 69 + 70 + type CROSS_SITE_AUTH_REQUEST = { redirect: string }; 71 + type CROSS_SITE_AUTH_RESPONSE = { redirect: string; auth_token: string | null }; 72 + async function initiateAuthCallback(req: NextRequest) { 73 + let token: CROSS_SITE_AUTH_REQUEST = { redirect: req.url }; 74 + let payload = btoa(JSON.stringify(token)); 75 + let signature = signCrossSiteToken(payload); 76 + return NextResponse.redirect( 77 + `https://leaflet.pub${auth_callback_route}?payload=${payload}&signature=${signature}`, 78 + ); 79 + } 80 + 81 + async function authCallback(req: NextRequest) { 82 + let payload = req.nextUrl.searchParams.get("payload"); 83 + let signature = req.nextUrl.searchParams.get("signature"); 84 + 85 + if (typeof payload !== "string") 86 + return new NextResponse(null, { status: 401 }); 87 + 88 + let verifySig = await signCrossSiteToken(payload); 89 + if (verifySig !== signature) return new NextResponse(null, { status: 401 }); 90 + 91 + let token: CROSS_SITE_AUTH_REQUEST = JSON.parse(atob(payload)); 92 + let auth_token = req.cookies.get("auth_token")?.value || null; 93 + let redirect_url = new URL(token.redirect); 94 + let response_token: CROSS_SITE_AUTH_RESPONSE = { 95 + redirect: token.redirect, 96 + auth_token, 97 + }; 98 + 99 + let response_payload = btoa(JSON.stringify(response_token)); 100 + let sig = signCrossSiteToken(response_payload); 101 + return NextResponse.redirect( 102 + `https://${redirect_url.host}${receive_auth_callback_route}?payload=${response_payload}&signature=${sig}`, 103 + ); 104 + } 105 + 106 + async function receiveAuthCallback(req: NextRequest) { 107 + let payload = req.nextUrl.searchParams.get("payload"); 108 + let signature = req.nextUrl.searchParams.get("signature"); 109 + 110 + if (typeof payload !== "string") 111 + return new NextResponse(null, { status: 401 }); 112 + 113 + let verifySig = await signCrossSiteToken(payload); 114 + if (verifySig !== signature) return new NextResponse(null, { status: 401 }); 115 + 116 + let token: CROSS_SITE_AUTH_RESPONSE = JSON.parse(atob(payload)); 117 + 118 + let response = NextResponse.redirect(token.redirect); 119 + response.cookies.set("external_auth_token", token.auth_token || "null"); 120 + return response; 121 + } 122 + 123 + const signCrossSiteToken = async (input: string) => { 124 + if (!process.env.CROSS_SITE_AUTH_SECRET) 125 + throw new Error("Environment variable CROSS_SITE_AUTH_SECRET not set "); 126 + const encoder = new TextEncoder(); 127 + const data = encoder.encode(input); 128 + const secretKey = process.env.CROSS_SITE_AUTH_SECRET; 129 + const keyData = encoder.encode(secretKey); 130 + 131 + const key = await crypto.subtle.importKey( 132 + "raw", 133 + keyData, 134 + { name: "HMAC", hash: "SHA-256" }, 135 + false, 136 + ["sign"], 137 + ); 138 + 139 + const signature = await crypto.subtle.sign("HMAC", key, data); 140 + 141 + return btoa(String.fromCharCode(...new Uint8Array(signature))); 142 + };