UNPKG

cosmic-authentication

Version:

Authentication library for cosmic.new. Designed to be used and deployed on cosmic.new

226 lines (225 loc) 10.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAuthHandler = createAuthHandler; const server_1 = require("next/server"); const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); const auth_1 = require("../lib/auth"); // Determine cookie settings based on environment const getCookieSettings = () => { const secureFlag = process.env.NODE_ENV === 'production'; const sameSiteFlag = secureFlag ? 'none' : 'lax'; return { secureFlag, sameSiteFlag }; }; // Generate a unique request ID for tracking let requestCounter = 0; function createAuthHandler() { return { GET: async (request) => { const { pathname } = new URL(request.url); if (pathname.endsWith('/callback')) { return handleCallback(request); } else if (pathname.endsWith('/status')) { return handleStatus(request); } return server_1.NextResponse.json({ error: 'Not found' }, { status: 404 }); }, POST: async (request) => { const { pathname } = new URL(request.url); if (pathname.endsWith('/signout')) { return handleSignout(request); } else if (pathname.endsWith('/clear-return-url')) { return handleClearReturnUrl(request); } return server_1.NextResponse.json({ error: 'Not found' }, { status: 404 }); } }; } async function handleCallback(request) { var _a; const { searchParams } = request.nextUrl; const accessToken = searchParams.get('accessToken'); const refreshToken = searchParams.get('refreshToken'); const step = searchParams.get('step'); const { secureFlag, sameSiteFlag } = getCookieSettings(); // Step 2: Set access token and do final redirect if (step === '2' && accessToken) { // Get the return URL from the cookie or default to home page const returnUrl = ((_a = request.cookies.get(auth_1.RETURN_URL_COOKIE)) === null || _a === void 0 ? void 0 : _a.value) || '/'; const redirectUrl = new URL(returnUrl, process.env.NEXT_PUBLIC_BASE_URL); const response = server_1.NextResponse.redirect(redirectUrl); // Set access token response.cookies.set('accessToken', accessToken, { httpOnly: true, secure: secureFlag, maxAge: 5 * 60, // 5 minutes path: '/', sameSite: sameSiteFlag }); // Clear the return URL cookie response.cookies.delete(auth_1.RETURN_URL_COOKIE); response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate'); response.headers.set('Pragma', 'no-cache'); response.headers.set('Expires', '0'); return response; } // Step 1: Validate tokens and set refresh token if (!accessToken || !refreshToken) { return server_1.NextResponse.redirect(new URL('/', process.env.NEXT_PUBLIC_BASE_URL)); } // Verify server config if (!process.env.COSMICAUTH_SECRET) { return server_1.NextResponse.json({ error: 'Server configuration error' }, { status: 500 }); } try { const decoded = jsonwebtoken_1.default.verify(accessToken, process.env.COSMICAUTH_SECRET); const uid = decoded.uid; if (!uid) throw new Error('UID not found in token'); // Create a redirect to step 2 // This two-step process works around a Next.js bug where multiple cookies // cannot be reliably set in a single redirect response when behind a proxy const step2Url = new URL('/api/auth/callback', process.env.NEXT_PUBLIC_BASE_URL); step2Url.searchParams.set('accessToken', accessToken); step2Url.searchParams.set('step', '2'); const response = server_1.NextResponse.redirect(step2Url); // Set refresh token in step 1 response.cookies.set('refreshToken', refreshToken, { httpOnly: true, secure: secureFlag, maxAge: 7 * 24 * 60 * 60, // 7 days path: '/', sameSite: sameSiteFlag }); response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate'); response.headers.set('Pragma', 'no-cache'); response.headers.set('Expires', '0'); return response; } catch (tokenError) { console.error('Token validation failed:', tokenError); return server_1.NextResponse.json({ error: 'Invalid token' }, { status: 401 }); } } async function handleSignout(request) { const { searchParams } = request.nextUrl; const step = searchParams.get('step'); const { secureFlag, sameSiteFlag } = getCookieSettings(); // Define common cookie options based on environment const cookieOptions = { httpOnly: true, secure: secureFlag, path: '/', sameSite: sameSiteFlag }; // Step 2: Delete access token and return success if (step === '2') { const response = server_1.NextResponse.json({ success: true, message: 'Signed out successfully' }); // Delete access token in step 2 response.cookies.delete(Object.assign({ name: 'accessToken' }, cookieOptions)); return response; } // Step 1: Delete refresh token first // This two-step process works around a Next.js bug where multiple cookies // cannot be reliably deleted in a single response when behind a proxy const step2Url = new URL('/api/auth/signout', process.env.NEXT_PUBLIC_BASE_URL); step2Url.searchParams.set('step', '2'); // Instead of redirecting, we'll return a response that tells the client to make another request const response = server_1.NextResponse.json({ nextStep: step2Url.toString() }); // Delete refresh token in step 1 (most important to delete first) response.cookies.delete(Object.assign({ name: 'refreshToken' }, cookieOptions)); return response; } async function handleStatus(request) { var _a, _b; const requestId = ++requestCounter; const { secureFlag, sameSiteFlag } = getCookieSettings(); const accessToken = (_a = request.cookies.get('accessToken')) === null || _a === void 0 ? void 0 : _a.value; const clearResponse = () => { const resp = server_1.NextResponse.json({ authenticated: false }, { status: 401 }); // Ensure delete uses the same flags as set for the environment const cookieOpts = { httpOnly: true, secure: secureFlag, path: '/', sameSite: sameSiteFlag }; resp.cookies.delete(Object.assign({ name: 'accessToken' }, cookieOpts)); resp.cookies.delete(Object.assign({ name: 'refreshToken' }, cookieOpts)); return resp; }; // Validate existing access token if (accessToken) { try { if (!process.env.COSMICAUTH_SECRET) throw new Error('COSMICAUTH_SECRET is not configured'); // Decode the token and assume its payload matches UserData const decoded = jsonwebtoken_1.default.verify(accessToken, process.env.COSMICAUTH_SECRET); // Return the full decoded payload as the user object return server_1.NextResponse.json({ authenticated: true, user: decoded }); } catch (err) { console.error(`[${requestId}] Access token invalid, will attempt refresh:`, err); } } // Attempt refresh const refreshToken = (_b = request.cookies.get('refreshToken')) === null || _b === void 0 ? void 0 : _b.value; if (refreshToken) { try { const refreshRes = await fetch(`https://auth.cosmic.new/api/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }), }); if (refreshRes.ok) { const { accessToken: newAT } = await refreshRes.json(); // Decode the *new* access token to get the full user data if (!process.env.COSMICAUTH_SECRET) throw new Error('COSMICAUTH_SECRET is not configured post-refresh'); const decodedNew = jsonwebtoken_1.default.verify(newAT, process.env.COSMICAUTH_SECRET); const resp = server_1.NextResponse.json({ authenticated: true, user: decodedNew }); // IMPORTANT: Only set the access token here to avoid the cookie mixing bug // The refresh token already exists and doesn't need to be reset // console.log("successfully refreshed access token"); resp.cookies.set('accessToken', newAT, { httpOnly: true, secure: secureFlag, maxAge: 5 * 60, // 5 minutes path: '/', sameSite: sameSiteFlag }); // DO NOT reset the refresh token here - it's already valid and setting both // cookies in one response causes the Next.js bug where attributes get mixed up return resp; } } catch (err) { console.error(`[${requestId}] Refresh error:`, err); } } // No valid session available return clearResponse(); } async function handleClearReturnUrl(request) { const { secureFlag, sameSiteFlag } = getCookieSettings(); const response = server_1.NextResponse.json({ success: true, message: 'Return URL cookie cleared' }); // Clear the return URL cookie with proper settings response.cookies.delete(auth_1.RETURN_URL_COOKIE); // Also explicitly set it to expire immediately with all possible combinations response.cookies.set(auth_1.RETURN_URL_COOKIE, '', { httpOnly: true, secure: secureFlag, sameSite: sameSiteFlag, maxAge: 0, expires: new Date(0), path: '/' }); return response; }