better-auth
Version:
The most comprehensive authentication framework for TypeScript.
257 lines (255 loc) • 10.7 kB
JavaScript
import { parseUserOutput } from "../../db/schema.mjs";
import { formCsrfMiddleware } from "../middlewares/origin-check.mjs";
import { setSessionCookie } from "../../cookies/index.mjs";
import { generateState } from "../../oauth2/state.mjs";
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
import { createEmailVerificationToken } from "./email-verification.mjs";
import "../../utils/index.mjs";
import { BASE_ERROR_CODES } from "@better-auth/core/error";
import { APIError } from "better-call";
import * as z from "zod";
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
import { createAuthEndpoint } from "@better-auth/core/api";
//#region src/api/routes/sign-in.ts
const socialSignInBodySchema = z.object({
callbackURL: z.string().meta({ description: "Callback URL to redirect to after the user has signed in" }).optional(),
newUserCallbackURL: z.string().optional(),
errorCallbackURL: z.string().meta({ description: "Callback URL to redirect to if an error happens" }).optional(),
provider: SocialProviderListEnum,
disableRedirect: z.boolean().meta({ description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself" }).optional(),
idToken: z.optional(z.object({
token: z.string().meta({ description: "ID token from the provider" }),
nonce: z.string().meta({ description: "Nonce used to generate the token" }).optional(),
accessToken: z.string().meta({ description: "Access token from the provider" }).optional(),
refreshToken: z.string().meta({ description: "Refresh token from the provider" }).optional(),
expiresAt: z.number().meta({ description: "Expiry date of the token" }).optional()
})),
scopes: z.array(z.string()).meta({ description: "Array of scopes to request from the provider. This will override the default scopes passed." }).optional(),
requestSignUp: z.boolean().meta({ description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider" }).optional(),
loginHint: z.string().meta({ description: "The login hint to use for the authorization code request" }).optional(),
additionalData: z.record(z.string(), z.any()).optional().meta({ description: "Additional data to be passed through the OAuth flow" })
});
const signInSocial = () => createAuthEndpoint("/sign-in/social", {
method: "POST",
operationId: "socialSignIn",
body: socialSignInBodySchema,
metadata: {
$Infer: {
body: {},
returned: {}
},
openapi: {
description: "Sign in with a social provider",
operationId: "socialSignIn",
responses: { "200": {
description: "Success - Returns either session details or redirect URL",
content: { "application/json": { schema: {
type: "object",
description: "Session response when idToken is provided",
properties: {
token: { type: "string" },
user: {
type: "object",
$ref: "#/components/schemas/User"
},
url: { type: "string" },
redirect: {
type: "boolean",
enum: [false]
}
},
required: [
"redirect",
"token",
"user"
]
} } }
} }
}
}
}, async (c) => {
const provider = c.context.socialProviders.find((p) => p.id === c.body.provider);
if (!provider) {
c.context.logger.error("Provider not found. Make sure to add the provider in your auth config", { provider: c.body.provider });
throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.PROVIDER_NOT_FOUND });
}
if (c.body.idToken) {
if (!provider.verifyIdToken) {
c.context.logger.error("Provider does not support id token verification", { provider: c.body.provider });
throw new APIError("NOT_FOUND", { message: BASE_ERROR_CODES.ID_TOKEN_NOT_SUPPORTED });
}
const { token, nonce } = c.body.idToken;
if (!await provider.verifyIdToken(token, nonce)) {
c.context.logger.error("Invalid id token", { provider: c.body.provider });
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_TOKEN });
}
const userInfo = await provider.getUserInfo({
idToken: token,
accessToken: c.body.idToken.accessToken,
refreshToken: c.body.idToken.refreshToken
});
if (!userInfo || !userInfo?.user) {
c.context.logger.error("Failed to get user info", { provider: c.body.provider });
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO });
}
if (!userInfo.user.email) {
c.context.logger.error("User email not found", { provider: c.body.provider });
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.USER_EMAIL_NOT_FOUND });
}
const data = await handleOAuthUserInfo(c, {
userInfo: {
...userInfo.user,
email: userInfo.user.email,
id: String(userInfo.user.id),
name: userInfo.user.name || "",
image: userInfo.user.image,
emailVerified: userInfo.user.emailVerified || false
},
account: {
providerId: provider.id,
accountId: String(userInfo.user.id),
accessToken: c.body.idToken.accessToken
},
callbackURL: c.body.callbackURL,
disableSignUp: provider.disableImplicitSignUp && !c.body.requestSignUp || provider.disableSignUp
});
if (data.error) throw new APIError("UNAUTHORIZED", { message: data.error });
await setSessionCookie(c, data.data);
return c.json({
redirect: false,
token: data.data.session.token,
url: void 0,
user: parseUserOutput(c.context.options, data.data.user)
});
}
const { codeVerifier, state } = await generateState(c, void 0, c.body.additionalData);
const url = await provider.createAuthorizationURL({
state,
codeVerifier,
redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
scopes: c.body.scopes,
loginHint: c.body.loginHint
});
if (!c.body.disableRedirect) c.setHeader("Location", url.toString());
return c.json({
url: url.toString(),
redirect: !c.body.disableRedirect
});
});
const signInEmail = () => createAuthEndpoint("/sign-in/email", {
method: "POST",
operationId: "signInEmail",
use: [formCsrfMiddleware],
body: z.object({
email: z.string().meta({ description: "Email of the user" }),
password: z.string().meta({ description: "Password of the user" }),
callbackURL: z.string().meta({ description: "Callback URL to use as a redirect for email verification" }).optional(),
rememberMe: z.boolean().meta({ description: "If this is false, the session will not be remembered. Default is `true`." }).default(true).optional()
}),
metadata: {
allowedMediaTypes: ["application/x-www-form-urlencoded", "application/json"],
$Infer: {
body: {},
returned: {}
},
openapi: {
operationId: "signInEmail",
description: "Sign in with email and password",
responses: { "200": {
description: "Success - Returns either session details or redirect URL",
content: { "application/json": { schema: {
type: "object",
description: "Session response when idToken is provided",
properties: {
redirect: {
type: "boolean",
enum: [false]
},
token: {
type: "string",
description: "Session token"
},
url: {
type: "string",
nullable: true
},
user: {
type: "object",
$ref: "#/components/schemas/User"
}
},
required: [
"redirect",
"token",
"user"
]
} } }
} }
}
}
}, async (ctx) => {
if (!ctx.context.options?.emailAndPassword?.enabled) {
ctx.context.logger.error("Email and password is not enabled. Make sure to enable it in the options on you `auth.ts` file. Check `https://better-auth.com/docs/authentication/email-password` for more!");
throw new APIError("BAD_REQUEST", { message: "Email and password is not enabled" });
}
const { email, password } = ctx.body;
if (!z.email().safeParse(email).success) throw new APIError("BAD_REQUEST", { message: BASE_ERROR_CODES.INVALID_EMAIL });
const user = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: true });
if (!user) {
await ctx.context.password.hash(password);
ctx.context.logger.error("User not found", { email });
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD });
}
const credentialAccount = user.accounts.find((a) => a.providerId === "credential");
if (!credentialAccount) {
await ctx.context.password.hash(password);
ctx.context.logger.error("Credential account not found", { email });
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD });
}
const currentPassword = credentialAccount?.password;
if (!currentPassword) {
await ctx.context.password.hash(password);
ctx.context.logger.error("Password not found", { email });
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD });
}
if (!await ctx.context.password.verify({
hash: currentPassword,
password
})) {
ctx.context.logger.error("Invalid password");
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.INVALID_EMAIL_OR_PASSWORD });
}
if (ctx.context.options?.emailAndPassword?.requireEmailVerification && !user.user.emailVerified) {
if (!ctx.context.options?.emailVerification?.sendVerificationEmail) throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED });
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
const token = await createEmailVerificationToken(ctx.context.secret, user.user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/");
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
user: user.user,
url,
token
}, ctx.request));
}
throw new APIError("FORBIDDEN", { message: BASE_ERROR_CODES.EMAIL_NOT_VERIFIED });
}
const session = await ctx.context.internalAdapter.createSession(user.user.id, ctx.body.rememberMe === false);
if (!session) {
ctx.context.logger.error("Failed to create session");
throw new APIError("UNAUTHORIZED", { message: BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION });
}
await setSessionCookie(ctx, {
session,
user: user.user
}, ctx.body.rememberMe === false);
if (ctx.body.callbackURL) ctx.setHeader("Location", ctx.body.callbackURL);
return ctx.json({
redirect: !!ctx.body.callbackURL,
token: session.token,
url: ctx.body.callbackURL,
user: parseUserOutput(ctx.context.options, user.user)
});
});
//#endregion
export { signInEmail, signInSocial };
//# sourceMappingURL=sign-in.mjs.map