cosmic-authentication
Version:
Authentication library for cosmic.new. Designed to be used and deployed on cosmic.new
226 lines (225 loc) • 10.1 kB
JavaScript
"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;
}