@absolutejs/auth 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,6 +41,13 @@ export type CredentialsConfig<UserType> = {
41
41
  onEmailVerified?: (context: {
42
42
  email: string;
43
43
  }) => void | Promise<void>;
44
+ /** Called (in the enumeration-safe default) when someone tries to register an
45
+ * email that already exists — send the real owner a "you already have an
46
+ * account, sign in" email out-of-band. Not called when
47
+ * `revealRegistrationConflicts` is true. */
48
+ onExistingAccount?: (context: {
49
+ email: string;
50
+ }) => void | Promise<void>;
44
51
  onPasswordReset?: (context: {
45
52
  email: string;
46
53
  }) => void | Promise<void>;
@@ -55,9 +62,17 @@ export type CredentialsConfig<UserType> = {
55
62
  * rejected until the email is verified. Default false = auto-login on register and
56
63
  * verification acts as a soft, later gate. */
57
64
  requireEmailVerification?: boolean;
65
+ /** When true, `POST /register` returns 409 "Email is already registered" for a
66
+ * known email. Default false = enumeration-safe: return the same generic
67
+ * response a new pending registration does and call `onExistingAccount`
68
+ * instead of confirming the account exists. */
69
+ revealRegistrationConflicts?: boolean;
58
70
  resetPasswordRoute?: RouteString;
59
71
  resetTokenDurationMs?: number;
60
72
  sessionDurationMs?: number;
73
+ /** When set, login/register reject requests whose `Origin` header is not in
74
+ * this list (defense against login/registration CSRF). Omit to disable. */
75
+ trustedOrigins?: readonly string[];
61
76
  verificationTokenDurationMs?: number;
62
77
  verifyEmailRoute?: RouteString;
63
78
  };
@@ -1,6 +1,6 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type CredentialRouteProps } from './config';
3
- export declare const credentialsLogin: <UserType>({ authSessionStore, checkBreachesOnLogin, cookieSecure, credentialStore, getUserByEmail, isMfaRequired, lockoutGuard, loginRoute, onCredentialsLoginError, onCredentialsLoginSuccess, passwordVerifier, rehashOnLogin, requireEmailVerification, sessionDurationMs }: CredentialRouteProps<UserType>) => Elysia<"", {
3
+ export declare const credentialsLogin: <UserType>({ authSessionStore, checkBreachesOnLogin, cookieSecure, credentialStore, getUserByEmail, isMfaRequired, lockoutGuard, loginRoute, onCredentialsLoginError, onCredentialsLoginSuccess, passwordVerifier, rehashOnLogin, requireEmailVerification, sessionDurationMs, trustedOrigins }: CredentialRouteProps<UserType>) => Elysia<"", {
4
4
  decorator: {};
5
5
  store: {
6
6
  session: import("..").SessionRecord<UserType>;
@@ -30,7 +30,7 @@ export declare const credentialsLogin: <UserType>({ authSessionStore, checkBreac
30
30
  headers: unknown;
31
31
  response: {
32
32
  401: "Invalid email or password";
33
- 403: {
33
+ 403: "Request origin is not allowed" | {
34
34
  readonly status: "email_not_verified";
35
35
  };
36
36
  200: {
@@ -1,6 +1,6 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type CredentialRouteProps } from './config';
3
- export declare const credentialsRegister: <UserType>({ authSessionStore, cookieSecure, credentialStore, onCreateCredentialUser, onCredentialsLoginSuccess, onRegistrationSuccess, onSendEmail, passwordPolicy, registerRoute, requireEmailVerification, sessionDurationMs, verificationTokenDurationMs }: CredentialRouteProps<UserType>) => Elysia<"", {
3
+ export declare const credentialsRegister: <UserType>({ authSessionStore, cookieSecure, credentialStore, onCreateCredentialUser, onCredentialsLoginSuccess, onExistingAccount, onRegistrationSuccess, onSendEmail, passwordPolicy, registerRoute, requireEmailVerification, revealRegistrationConflicts, sessionDurationMs, trustedOrigins, verificationTokenDurationMs }: CredentialRouteProps<UserType>) => Elysia<"", {
4
4
  decorator: {};
5
5
  store: {
6
6
  session: import("..").SessionRecord<UserType>;
@@ -98,7 +98,7 @@ export declare const credentialRoutes: <UserType>(config: CredentialRouteProps<U
98
98
  headers: unknown;
99
99
  response: {
100
100
  401: "Invalid email or password";
101
- 403: {
101
+ 403: "Request origin is not allowed" | {
102
102
  readonly status: "email_not_verified";
103
103
  };
104
104
  200: {
package/dist/csrf.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const isTrustedOrigin: (request: Request, trustedOrigins?: readonly string[]) => boolean;
package/dist/index.d.ts CHANGED
@@ -815,7 +815,7 @@ export declare const createAuthApplications: <UserType>(configuration: AuthConfi
815
815
  headers: unknown;
816
816
  response: {
817
817
  401: "Invalid email or password";
818
- 403: {
818
+ 403: "Request origin is not allowed" | {
819
819
  readonly status: "email_not_verified";
820
820
  };
821
821
  200: {
@@ -3468,6 +3468,7 @@ export { resolveAuthHtmxRenderers } from './htmx/renderers';
3468
3468
  export type { AuthHtmxConfig, AuthHtmxConnectorTarget, AuthHtmxProviderData, AuthHtmxProviderInfo, AuthHtmxRenderOverrides, AuthHtmxRenderersConfig, AuthHtmxUser, AuthIdentityPayload, LinkedProviderPayload } from './htmx/types';
3469
3469
  export * from './utils';
3470
3470
  export * from './redirect';
3471
+ export * from './csrf';
3471
3472
  export { buildClientProviders, resolveClientProviderEntry, resolveProviderClientConfiguration } from './providers/clients';
3472
3473
  export type { OAuth2TokenResponse, OAuth2Client, OAuth2ClientForConfig, CustomProviderCredentials, ProviderConfig, ProviderOption, PKCEProvider, OIDCProvider, RefreshableProvider, RevocableProvider, ScopeRequiredProvider, ProvidersMap, ProviderConfiguration, CredentialsFor } from 'citra';
3473
3474
  export { providers, providerOptions, createCustomOAuth2Client, defineProvider, refreshableProviderOptions, revocableProviderOptions, oidcProviderOptions, pkceProviderOptions, scopeRequiredProviderOptions, decodeJWT, extractPropFromIdentity, isValidProviderOption, isRefreshableOAuth2Client, isRefreshableProviderOption, isOIDCProviderOption, isPKCEProviderOption, isRevocableProviderOption, isRevocableOAuth2Client } from 'citra';
package/dist/index.js CHANGED
@@ -5145,6 +5145,15 @@ init_constants();
5145
5145
  init_crypto();
5146
5146
  import { Elysia as Elysia11, t as t8 } from "elysia";
5147
5147
 
5148
+ // src/csrf.ts
5149
+ var isTrustedOrigin = (request, trustedOrigins) => {
5150
+ if (trustedOrigins === undefined || trustedOrigins.length === 0) {
5151
+ return true;
5152
+ }
5153
+ const origin = request.headers.get("origin");
5154
+ return origin !== null && trustedOrigins.includes(origin);
5155
+ };
5156
+
5148
5157
  // src/credentials/import.ts
5149
5158
  init_crypto();
5150
5159
  var normalizeEmail = (email) => email.trim().toLowerCase();
@@ -5333,6 +5342,8 @@ var evaluatePassword = async (password, policy = {}) => {
5333
5342
  var isPasswordCompromised = (password) => isPasswordBreached(password);
5334
5343
 
5335
5344
  // src/credentials/login.ts
5345
+ var dummyPasswordHashPromise;
5346
+ var dummyPasswordHash = () => dummyPasswordHashPromise ??= hashPassword("absolutejs-auth-timing-equalizer");
5336
5347
  var credentialsLogin = ({
5337
5348
  authSessionStore,
5338
5349
  checkBreachesOnLogin,
@@ -5347,7 +5358,8 @@ var credentialsLogin = ({
5347
5358
  passwordVerifier,
5348
5359
  rehashOnLogin = false,
5349
5360
  requireEmailVerification = false,
5350
- sessionDurationMs = DEFAULT_CREDENTIAL_SESSION_TTL_MS
5361
+ sessionDurationMs = DEFAULT_CREDENTIAL_SESSION_TTL_MS,
5362
+ trustedOrigins
5351
5363
  }) => new Elysia11().use(sessionStore()).post(loginRoute, async ({
5352
5364
  body: { email, password },
5353
5365
  cookie: { user_session_id },
@@ -5355,6 +5367,9 @@ var credentialsLogin = ({
5355
5367
  status,
5356
5368
  store: { session, unregisteredSession }
5357
5369
  }) => withSpan("auth.credentials.login", undefined, async (span) => {
5370
+ if (!isTrustedOrigin(request, trustedOrigins)) {
5371
+ return status("Forbidden", "Request origin is not allowed");
5372
+ }
5358
5373
  const headerBag = {};
5359
5374
  request.headers.forEach((value, key) => {
5360
5375
  headerBag[key] = value;
@@ -5374,7 +5389,13 @@ var credentialsLogin = ({
5374
5389
  const credential = await credentialStore.getCredentialByEmail(normalizedEmail);
5375
5390
  const user = await getUserByEmail(normalizedEmail);
5376
5391
  const verifyHash = passwordVerifier ?? verifyPassword;
5377
- const passwordValid = credential === undefined ? false : await verifyHash(password, credential.passwordHash);
5392
+ let passwordValid;
5393
+ if (credential === undefined) {
5394
+ await verifyPassword(password, await dummyPasswordHash());
5395
+ passwordValid = false;
5396
+ } else {
5397
+ passwordValid = await verifyHash(password, credential.passwordHash);
5398
+ }
5378
5399
  if (!credential || !user || credential.status !== "active" || !passwordValid) {
5379
5400
  await lockoutGuard?.recordFailure(normalizedEmail);
5380
5401
  await onCredentialsLoginError?.({
@@ -5506,19 +5527,26 @@ var credentialsRegister = ({
5506
5527
  credentialStore,
5507
5528
  onCreateCredentialUser,
5508
5529
  onCredentialsLoginSuccess,
5530
+ onExistingAccount,
5509
5531
  onRegistrationSuccess,
5510
5532
  onSendEmail,
5511
5533
  passwordPolicy,
5512
5534
  registerRoute = "/auth/register",
5513
5535
  requireEmailVerification = false,
5536
+ revealRegistrationConflicts = false,
5514
5537
  sessionDurationMs = DEFAULT_CREDENTIAL_SESSION_TTL_MS,
5538
+ trustedOrigins,
5515
5539
  verificationTokenDurationMs = DEFAULT_VERIFICATION_TOKEN_TTL_MS
5516
5540
  }) => new Elysia13().use(sessionStore()).post(registerRoute, async ({
5517
5541
  body: { email, password, ...extraFields },
5518
5542
  cookie: { user_session_id },
5543
+ request,
5519
5544
  status,
5520
5545
  store: { session }
5521
5546
  }) => withSpan("auth.credentials.register", undefined, async () => {
5547
+ if (!isTrustedOrigin(request, trustedOrigins)) {
5548
+ return status("Forbidden", "Request origin is not allowed");
5549
+ }
5522
5550
  const normalizedEmail = email.trim().toLowerCase();
5523
5551
  if (!normalizedEmail.includes("@")) {
5524
5552
  return status("Bad Request", "A valid email is required");
@@ -5532,7 +5560,13 @@ var credentialsRegister = ({
5532
5560
  }
5533
5561
  const existing = await credentialStore.getCredentialByEmail(normalizedEmail);
5534
5562
  if (existing) {
5535
- return status("Conflict", "Email is already registered");
5563
+ if (revealRegistrationConflicts) {
5564
+ return status("Conflict", "Email is already registered");
5565
+ }
5566
+ await onExistingAccount?.({ email: normalizedEmail });
5567
+ return status("Created", {
5568
+ status: "verification_required"
5569
+ });
5536
5570
  }
5537
5571
  const created = await onCreateCredentialUser({
5538
5572
  ...extraFields,
@@ -37323,6 +37357,9 @@ var buildAuthApplications = async (configuration) => {
37323
37357
  await webhookDispatch?.(event);
37324
37358
  }
37325
37359
  }) : undefined;
37360
+ if (auditEmit === undefined) {
37361
+ console.warn("[@absolutejs/auth] No audit sink configured (pass `audit` or `webhooks`). " + "Privileged flows (impersonation, role/permission changes, sign-out) will " + "run WITHOUT an audit trail.");
37362
+ }
37326
37363
  const lockoutGuard = lockout ? createLockoutGuard(lockout) : undefined;
37327
37364
  const resolvedAgentAuth = agentAuth === undefined ? undefined : {
37328
37365
  ...agentAuth,
@@ -37705,6 +37742,7 @@ export {
37705
37742
  isValidUser,
37706
37743
  isValidProviderOption,
37707
37744
  isUserSessionId,
37745
+ isTrustedOrigin,
37708
37746
  isSafeLocalPath,
37709
37747
  isRevocableProviderOption,
37710
37748
  isRevocableOAuth2Client,
@@ -38046,5 +38084,5 @@ export {
38046
38084
  AGENT_CLAIM_GRANT_TYPE
38047
38085
  };
38048
38086
 
38049
- //# debugId=6BF9ABB7B635052264756E2164756E21
38087
+ //# debugId=A62F57FCBD818EE864756E2164756E21
38050
38088
  //# sourceMappingURL=index.js.map