UNPKG

@auth/core

Version:

Authentication for the Web.

283 lines (282 loc) 13.2 kB
import { CallbackRouteError, OAuthCallbackError, Verification, } from "../../errors.js"; import { handleLogin } from "../callback-handler.js"; import { handleOAuth } from "../oauth/callback.js"; import { handleState } from "../oauth/handle-state.js"; import { createHash } from "../web.js"; import { handleAuthorized } from "./shared.js"; /** Handle callbacks from login services */ export async function callback(params) { const { options, query, body, method, headers, sessionStore } = params; const { provider, adapter, url, callbackUrl, pages, jwt, events, callbacks, session: { strategy: sessionStrategy, maxAge: sessionMaxAge }, logger, } = options; const cookies = []; const useJwtSession = sessionStrategy === "jwt"; try { if (provider.type === "oauth" || provider.type === "oidc") { const { proxyRedirect, randomState } = handleState(query, provider, options.isOnRedirectProxy); if (proxyRedirect) { logger.debug("proxy redirect", { proxyRedirect, randomState }); return { redirect: proxyRedirect }; } const authorizationResult = await handleOAuth(query, params.cookies, options, randomState); if (authorizationResult.cookies.length) { cookies.push(...authorizationResult.cookies); } logger.debug("authorization result", authorizationResult); const { user: userFromProvider, account, profile: OAuthProfile, } = authorizationResult; // If we don't have a profile object then either something went wrong // or the user cancelled signing in. We don't know which, so we just // direct the user to the signin page for now. We could do something // else in future. // TODO: Handle user cancelling signin if (!userFromProvider || !account || !OAuthProfile) { return { redirect: `${url}/signin`, cookies }; } // Check if user is allowed to sign in // Attempt to get Profile from OAuth provider details before invoking // signIn callback - but if no user object is returned, that is fine // (that just means it's a new user signing in for the first time). let userByAccountOrFromProvider; if (adapter) { const { getUserByAccount } = adapter; const userByAccount = await getUserByAccount({ providerAccountId: account.providerAccountId, provider: provider.id, }); if (userByAccount) userByAccountOrFromProvider = userByAccount; } const unauthorizedOrError = await handleAuthorized({ user: userByAccountOrFromProvider, account, profile: OAuthProfile, }, options); if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }; // Sign user in const { user, session, isNewUser } = await handleLogin(sessionStore.value, userFromProvider, account, options); if (useJwtSession) { const defaultToken = { name: user.name, email: user.email, picture: user.image, sub: user.id?.toString(), }; const token = await callbacks.jwt({ token: defaultToken, user, account, profile: OAuthProfile, isNewUser, trigger: isNewUser ? "signUp" : "signIn", }); // Clear cookies if token is null if (token === null) { cookies.push(...sessionStore.clean()); } else { // Encode token const newToken = await jwt.encode({ ...jwt, token }); // Set cookie expiry date const cookieExpires = new Date(); cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000); const sessionCookies = sessionStore.chunk(newToken, { expires: cookieExpires, }); cookies.push(...sessionCookies); } } else { // Save Session Token in cookie cookies.push({ name: options.cookies.sessionToken.name, value: session.sessionToken, options: { ...options.cookies.sessionToken.options, expires: session.expires, }, }); } await events.signIn?.({ user, account, profile: OAuthProfile, isNewUser }); // Handle first logins on new accounts // e.g. option to send users to a new account landing page on initial login // Note that the callback URL is preserved, so the journey can still be resumed if (isNewUser && pages.newUser) { return { redirect: `${pages.newUser}${pages.newUser.includes("?") ? "&" : "?"}${new URLSearchParams({ callbackUrl })}`, cookies, }; } return { redirect: callbackUrl, cookies }; } else if (provider.type === "email") { const token = query?.token; const identifier = query?.email; if (!token || !identifier) { const e = new TypeError("Missing token or email. The sign-in URL was manually opened without token/identifier or the link was not sent correctly in the email.", { cause: { hasToken: !!token, hasEmail: !!identifier } }); e.name = "Configuration"; throw e; } const secret = provider.secret ?? options.secret; // @ts-expect-error -- Verified in `assertConfig`. const invite = await adapter.useVerificationToken({ identifier, token: await createHash(`${token}${secret}`), }); const hasInvite = !!invite; const expired = invite ? invite.expires.valueOf() < Date.now() : undefined; const invalidInvite = !hasInvite || expired; if (invalidInvite) throw new Verification({ hasInvite, expired }); const user = (await adapter.getUserByEmail(identifier)) ?? { id: identifier, email: identifier, emailVerified: null, }; const account = { providerAccountId: user.email, userId: user.id, type: "email", provider: provider.id, }; // Check if user is allowed to sign in const unauthorizedOrError = await handleAuthorized({ user, account }, options); if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }; // Sign user in const { user: loggedInUser, session, isNewUser, } = await handleLogin(sessionStore.value, user, account, options); if (useJwtSession) { const defaultToken = { name: loggedInUser.name, email: loggedInUser.email, picture: loggedInUser.image, sub: loggedInUser.id?.toString(), }; const token = await callbacks.jwt({ token: defaultToken, user: loggedInUser, account, isNewUser, trigger: isNewUser ? "signUp" : "signIn", }); // Clear cookies if token is null if (token === null) { cookies.push(...sessionStore.clean()); } else { // Encode token const newToken = await jwt.encode({ ...jwt, token }); // Set cookie expiry date const cookieExpires = new Date(); cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000); const sessionCookies = sessionStore.chunk(newToken, { expires: cookieExpires, }); cookies.push(...sessionCookies); } } else { // Save Session Token in cookie cookies.push({ name: options.cookies.sessionToken.name, value: session.sessionToken, options: { ...options.cookies.sessionToken.options, expires: session.expires, }, }); } await events.signIn?.({ user: loggedInUser, account, isNewUser }); // Handle first logins on new accounts // e.g. option to send users to a new account landing page on initial login // Note that the callback URL is preserved, so the journey can still be resumed if (isNewUser && pages.newUser) { return { redirect: `${pages.newUser}${pages.newUser.includes("?") ? "&" : "?"}${new URLSearchParams({ callbackUrl })}`, cookies, }; } // Callback URL is already verified at this point, so safe to use if specified return { redirect: callbackUrl, cookies }; } else if (provider.type === "credentials" && method === "POST") { const credentials = body ?? {}; // TODO: Forward the original request as is, instead of reconstructing it Object.entries(query ?? {}).forEach(([k, v]) => url.searchParams.set(k, v)); const user = await provider.authorize(credentials, // prettier-ignore new Request(url, { headers, method, body: JSON.stringify(body) })); if (!user) { return { status: 401, redirect: `${url}/error?${new URLSearchParams({ error: "CredentialsSignin", provider: provider.id, })}`, cookies, }; } /** @type {import("src").Account} */ const account = { providerAccountId: user.id, type: "credentials", provider: provider.id, }; const unauthorizedOrError = await handleAuthorized({ user, account, credentials }, options); if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }; const defaultToken = { name: user.name, email: user.email, picture: user.image, sub: user.id?.toString(), }; const token = await callbacks.jwt({ token: defaultToken, user, // @ts-expect-error account, isNewUser: false, trigger: "signIn", }); // Clear cookies if token is null if (token === null) { cookies.push(...sessionStore.clean()); } else { // Encode token const newToken = await jwt.encode({ ...jwt, token }); // Set cookie expiry date const cookieExpires = new Date(); cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000); const sessionCookies = sessionStore.chunk(newToken, { expires: cookieExpires, }); cookies.push(...sessionCookies); } // @ts-expect-error await events.signIn?.({ user, account }); return { redirect: callbackUrl, cookies }; } return { status: 500, body: `Error: Callback for provider type ${provider.type} not supported`, cookies, }; } catch (e) { if (e instanceof OAuthCallbackError) { logger.error(e); // REVIEW: Should we expose original error= and error_description= // Should we use a different name for error= then, since we already use it for all kind of errors? url.searchParams.set("error", OAuthCallbackError.name); url.pathname += "/signin"; return { redirect: url.toString(), cookies }; } const error = new CallbackRouteError(e, { provider: provider.id }); logger.debug("callback route error details", { method, query, body }); logger.error(error); url.searchParams.set("error", CallbackRouteError.name); url.pathname += "/error"; return { redirect: url.toString(), cookies }; } }