@auth/core
Version:
Authentication for the Web.
283 lines (282 loc) • 13.2 kB
JavaScript
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 };
}
}