better-auth
Version:
The most comprehensive authentication framework for TypeScript.
139 lines (138 loc) • 5.48 kB
JavaScript
import { generateRandomString } from "./crypto/random.mjs";
import { symmetricDecrypt, symmetricEncrypt } from "./crypto/index.mjs";
import { expireCookie } from "./cookies/index.mjs";
import { BetterAuthError } from "@better-auth/core/error";
import * as z from "zod";
//#region src/state.ts
const stateDataSchema = z.looseObject({
callbackURL: z.string(),
codeVerifier: z.string(),
errorURL: z.string().optional(),
newUserURL: z.string().optional(),
expiresAt: z.number(),
/**
* CSRF nonce returned to the OAuth provider. When using cookie state storage,
* this must match the callback `state` query parameter.
*/
oauthState: z.string().optional(),
link: z.object({
email: z.string(),
userId: z.coerce.string()
}).optional(),
requestSignUp: z.boolean().optional()
});
const INTERNAL_STATE_KEYS = new Set(Object.keys(stateDataSchema.shape));
var StateError = class extends BetterAuthError {
code;
details;
/**
* The per-flow `errorCallbackURL` recovered from the parsed state, when the
* failure happened after the state was successfully parsed (for example a
* nonce or state-cookie mismatch). It was origin-validated at sign-in, so
* the callback can safely redirect there instead of the default error page.
* Absent when the state could not be parsed at all.
*/
errorURL;
constructor(message, options) {
super(message, options);
this.code = options.code;
this.details = options.details;
this.errorURL = options.errorURL;
}
};
async function generateGenericState(c, stateData, settings) {
const state = generateRandomString(32);
if (c.context.oauthConfig.storeStateStrategy === "cookie") {
const payload = {
...stateData,
oauthState: state
};
const encryptedData = await symmetricEncrypt({
key: c.context.secretConfig,
data: JSON.stringify(payload)
});
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "oauth_state", { maxAge: 600 });
c.setCookie(stateCookie.name, encryptedData, stateCookie.attributes);
return {
state,
codeVerifier: stateData.codeVerifier
};
}
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "state", { maxAge: 300 });
await c.setSignedCookie(stateCookie.name, state, c.context.secret, stateCookie.attributes);
const expiresAt = /* @__PURE__ */ new Date();
expiresAt.setMinutes(expiresAt.getMinutes() + 10);
if (!await c.context.internalAdapter.createVerificationValue({
value: JSON.stringify({
...stateData,
oauthState: state
}),
identifier: state,
expiresAt
})) throw new StateError("Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database", { code: "state_generation_error" });
return {
state,
codeVerifier: stateData.codeVerifier
};
}
async function parseGenericState(c, state, settings) {
if (!state) throw new StateError("State not found in OAuth callback", { code: "state_not_found" });
const storeStateStrategy = c.context.oauthConfig.storeStateStrategy;
let parsedData;
if (storeStateStrategy === "cookie") {
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "oauth_state");
const encryptedData = c.getCookie(stateCookie.name);
if (!encryptedData) throw new StateError("State mismatch: auth state cookie not found", {
code: "state_mismatch",
details: { state }
});
try {
const decryptedData = await symmetricDecrypt({
key: c.context.secretConfig,
data: encryptedData
});
parsedData = stateDataSchema.parse(JSON.parse(decryptedData));
} catch (error) {
throw new StateError("State invalid: Failed to decrypt or parse auth state", {
code: "state_invalid",
details: { state },
cause: error
});
}
if (!parsedData.oauthState || parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", {
code: "state_security_mismatch",
details: { state },
errorURL: parsedData.errorURL
});
expireCookie(c, stateCookie);
} else {
const data = await c.context.internalAdapter.findVerificationValue(state);
if (!data) throw new StateError("State mismatch: verification not found", {
code: "state_mismatch",
details: { state }
});
parsedData = stateDataSchema.parse(JSON.parse(data.value));
if (parsedData.oauthState !== void 0 && parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", {
code: "state_security_mismatch",
details: { state },
errorURL: parsedData.errorURL
});
const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "state");
const stateCookieValue = await c.getSignedCookie(stateCookie.name, c.context.secret);
if (!(settings?.skipStateCookieCheck ?? c.context.oauthConfig.skipStateCookieCheck) && (!stateCookieValue || stateCookieValue !== state)) throw new StateError("State mismatch: State not persisted correctly", {
code: "state_security_mismatch",
details: { state },
errorURL: parsedData.errorURL
});
expireCookie(c, stateCookie);
await c.context.internalAdapter.deleteVerificationByIdentifier(state);
}
if (parsedData.expiresAt < Date.now()) throw new StateError("Invalid state: request expired", {
code: "state_mismatch",
details: { expiresAt: parsedData.expiresAt },
errorURL: parsedData.errorURL
});
return parsedData;
}
//#endregion
export { INTERNAL_STATE_KEYS, StateError, generateGenericState, parseGenericState };