UNPKG

arctic-lights

Version:

Drop-In replacement for NextAuth v5, built with arctic for faster performance

259 lines (258 loc) 11.8 kB
import { generateCodeVerifier, generateState, GitHub, Google, MicrosoftEntraId } from "arctic"; import { redirect } from "next/navigation"; import { cookies, headers } from "next/headers"; import { createJWT, validateJWT } from "oslo/jwt"; import { TimeSpan } from "oslo"; const getArcticProvider = (provider, redirectUrl) => { redirectUrl = redirectUrl + provider.type; switch (provider.type) { case 'google': return new Google(provider.options.clientId, provider.options.clientSecret, redirectUrl); case 'entra-id': return new MicrosoftEntraId(provider.options.tenantId, provider.options.clientId, provider.options.clientSecret, redirectUrl); case 'azure-ad': return new MicrosoftEntraId(provider.options.tenantId, provider.options.clientId, provider.options.clientSecret, redirectUrl); case 'github': return new GitHub(provider.options.clientId, provider.options.clientSecret, { redirectURI: redirectUrl, enterpriseDomain: provider.options.enterpriseDomain }); default: console.error('Unknown provider type:', provider.type); return null; } }; const getRedirectUrl = (url) => { return getHost(url) + '/api/auth/callback/'; }; const getHost = (url) => { let hostUrl; if (process.env.ARCTICLIGHTS_URL) hostUrl = process.env.ARCTICLIGHTS_URL; else if (process.env.NEXTAUTH_URL) hostUrl = process.env.NEXTAUTH_URL; if (hostUrl) { if (hostUrl.match(/https?:\/\/.*/g) === null) console.error('ArcticLights: redirectUrl is invalid. It must be a valid URL'); else return hostUrl.endsWith('/') ? hostUrl.substring(0, hostUrl.length - 1) : hostUrl; } return url.split('://')[0] + '://' + url.split('://')[1].split('/')[0]; }; const getProviders = async (options) => { if (options.providers instanceof Promise) return await options.providers; return options.providers; }; const findProvider = async (options, providerType) => { return (await getProviders(options)).find(provider => (provider === null || provider === void 0 ? void 0 : provider.type) === providerType) || null; }; const getSignInPage = (options, host) => { var _a; const page = ((_a = options.pages) === null || _a === void 0 ? void 0 : _a.signIn) || 'signin'; return host + (page.startsWith('/') ? page : ('/' + page)); }; const signInWithProvider = async (options, provider, url) => { const host = getHost(url); const params = new URLSearchParams(url.split('?')[1]); const state = generateState(); const codeVerifier = generateCodeVerifier(); cookies().set('arctic_state', state, { secure: process.env.NODE_ENV === 'production', path: '/', httpOnly: true, maxAge: 60 * 10, }); if (provider.pkce) cookies().set('arctic_code_verifier', codeVerifier, { secure: process.env.NODE_ENV === 'production', path: '/', httpOnly: true, maxAge: 60 * 10, }); let callbackURL = decodeURIComponent(params.get('callback') || '/'); if (!callbackURL.includes('://')) callbackURL = host + (callbackURL.startsWith('/') ? callbackURL : ('/' + callbackURL)); cookies().set('arctic_callback', encodeURI(callbackURL), { secure: process.env.NODE_ENV === 'production', path: '/', httpOnly: true, maxAge: 60 * 10, }); const arcticProvider = getArcticProvider(provider, getRedirectUrl(url)); if (arcticProvider === null) return Response.redirect(getSignInPage(options, host) + '?error=provider_not_found'); if (provider.pkce) redirect((await arcticProvider.createAuthorizationURL(state, codeVerifier, { scopes: provider.scopes })).toString()); else redirect((await arcticProvider.createAuthorizationURL(state, { scopes: provider.scopes })).toString()); }; const handleCallback = async (options, provider, url) => { var _a, _b, _c, _d, _e, _f; const host = getHost(url); const params = new URLSearchParams(url.split('?')[1]); const code = params.get('code'); const state = params.get('state'); const storedState = (_a = cookies().get('arctic_state')) === null || _a === void 0 ? void 0 : _a.value; const codeVerifier = provider.pkce ? (_b = cookies().get('arctic_code_verifier')) === null || _b === void 0 ? void 0 : _b.value : undefined; const callbackURL = ((_c = cookies().get('arctic_callback')) === null || _c === void 0 ? void 0 : _c.value) || host; cookies().set('arctic_state', '', { maxAge: 0 }); cookies().set('arctic_callback', '', { maxAge: 0 }); if (provider.pkce) cookies().set('arctic_code_verifier', '', { maxAge: 0 }); if (!code || !state || !storedState || (provider.pkce && !codeVerifier) || state !== storedState) return Response.redirect(getSignInPage(options, host) + '?error=signin_failed'); try { const arcticProvider = getArcticProvider(provider, getRedirectUrl(url)); if (arcticProvider === null) return Response.redirect(getSignInPage(options, host) + '?error=provider_not_found'); const tokens = await arcticProvider.validateAuthorizationCode(code, provider.pkce ? codeVerifier : undefined); const user = await provider.getUser(tokens.accessToken); if (user === undefined) { console.error('ArcticLights: Unable to get user data'); return Response.redirect(getSignInPage(options, host) + '?error=signin_failed'); } const sessionToken = await createSessionToken(options, provider.type, user, tokens); if (!sessionToken) return Response.redirect(getSignInPage(options, host) + '?error=session_creation_failed'); cookies().set(((_e = (_d = options.session) === null || _d === void 0 ? void 0 : _d.accepted) === null || _e === void 0 ? void 0 : _e.cookie) || 'arctic_session', sessionToken, { secure: process.env.NODE_ENV === 'production', path: '/', httpOnly: true, maxAge: ((_f = options.session) === null || _f === void 0 ? void 0 : _f.maxAge) || 60 * 60 * 24 * 7, }); return Response.redirect(decodeURI(callbackURL)); } catch (e) { console.error('ArcticLights: Unable to sign in:', e); return Response.redirect(getSignInPage(options, host) + '?error=signin_failed'); } }; const listProviders = (providers) => { return new Response(JSON.stringify(providers.map(provider => { return { type: provider.type, style: provider.style }; })), { status: 200 }); }; const getSecret = (options) => { const secret = options.secret || process.env.ARCTICLIGHTS_SECRET || process.env.NEXTAUTH_SECRET; if (secret === undefined) { console.error('ArcticLights: secret is missing. Set secret in options or ARCTICLIGHTS_SECRET in environment variables'); return; } return new TextEncoder().encode(secret); }; const createSessionToken = async (options, provider, user, tokens) => { var _a, _b; const secret = getSecret(options); if (!secret) return; let token = { provider, user: { email: user.email, name: user.name, image: user.image } }; if ((_a = options.callbacks) === null || _a === void 0 ? void 0 : _a.jwt) token = await options.callbacks.jwt({ token, account: tokens, profile: user }); try { return await createJWT('HS256', secret, token, { expiresIn: new TimeSpan(((_b = options.session) === null || _b === void 0 ? void 0 : _b.maxAge) || 60 * 60 * 24 * 7, 's') }); } catch (e) { console.error('ArcticLights: Unable to create session token:', e); } }; const signOutSession = async (url) => { const params = new URLSearchParams(url.split('?')[1]); let callbackURL = decodeURIComponent(params.get('callback') || '/'); const host = getHost(url); if (!callbackURL.includes('://')) callbackURL = host + (callbackURL.startsWith('/') ? callbackURL : ('/' + callbackURL)); cookies().set('arctic_session', '', { maxAge: 0 }); return Response.redirect(callbackURL); }; const getToken = async (options) => { var _a, _b; const accepted = ((_a = options.session) === null || _a === void 0 ? void 0 : _a.accepted) || { cookie: 'arctic_session' }; if (accepted.cookie) { const cookie = (_b = cookies().get(accepted.cookie)) === null || _b === void 0 ? void 0 : _b.value; if (cookie) return { token: cookie, type: 'cookie' }; } if (accepted.header) { const header = headers().get(accepted.header); if (header) return { token: header.substring(6), type: 'header' }; // Remove Token prefix } }; const ArcticLights = (options) => { return { handlers: { GET: async (req, { params }) => { const path = params.arcticlights || params.nextauth; const host = getHost(req.url); if (path === undefined || path.length == 0) return Response.redirect(getSignInPage(options, host)); if (path.length == 1 && path[0] === 'providers') return listProviders((await getProviders(options)).filter(provider => provider !== null)); if (path.length == 1 && path[0] === 'signout') return signOutSession(req.url); if (path.length == 2) { const provider = await findProvider(options, path[1]); if (provider === null) return Response.redirect(getSignInPage(options, host) + '?error=provider_not_found'); if (path[0] === 'signin') return signInWithProvider(options, provider, req.url); if (path[0] === 'callback') return handleCallback(options, provider, req.url); } return Response.redirect(getSignInPage(options, host)); }, POST: async () => { return new Response(JSON.stringify({ error: 'Bad request' }), { status: 400 }); } }, auth: async () => { var _a; const token = await getToken(options); const secret = getSecret(options); if (!secret || !token) return null; try { const jwt = await validateJWT('HS256', secret, token.token); const payload = jwt.payload; const session = { expires: jwt.expiresAt, provider: payload.provider, user: payload.user }; try { if ((_a = options.callbacks) === null || _a === void 0 ? void 0 : _a.session) return await options.callbacks.session({ session, token: payload, user: payload.user }); } catch (e) { console.error('ArcticLights: Unable to run session callback:', e); } return session; } catch (e) { return null; } }, getProviders: async () => { return (await getProviders(options)).filter(provider => provider !== null).map(provider => { return { type: provider.type, style: provider.style }; }); }, generateToken: async (session) => { return await createSessionToken(options, session.provider, session.user); }, }; }; export default ArcticLights;