@korajs/auth 0.6.0 → 1.0.0-beta.1

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.
package/dist/server.d.cts CHANGED
@@ -3602,6 +3602,13 @@ interface TotpSecret {
3602
3602
  createdAt: number;
3603
3603
  /** When MFA was verified (confirmed with first valid code) */
3604
3604
  verifiedAt: number | null;
3605
+ /**
3606
+ * The most recent TOTP time-step counter that was successfully consumed
3607
+ * during authentication. Used to enforce single-use of codes (RFC 6238 §5.2):
3608
+ * a code at or before this time-step is rejected as a replay. `undefined`
3609
+ * until the first code is consumed.
3610
+ */
3611
+ lastUsedTimeStep?: number;
3605
3612
  }
3606
3613
  /**
3607
3614
  * Setup result returned when enabling TOTP MFA.
@@ -3702,6 +3709,11 @@ declare class TotpManager {
3702
3709
  /**
3703
3710
  * Verify a TOTP code during login.
3704
3711
  * Returns true if the code is valid, false otherwise.
3712
+ *
3713
+ * Enforces single-use: a code is rejected if its time-step has already
3714
+ * been consumed (or predates a previously consumed time-step). This
3715
+ * prevents an attacker who observes one valid code from replaying it
3716
+ * within the acceptance window (RFC 6238 §5.2).
3705
3717
  */
3706
3718
  verify(userId: string, code: string): Promise<boolean>;
3707
3719
  /**
@@ -3728,6 +3740,18 @@ declare class TotpManager {
3728
3740
  */
3729
3741
  remainingRecoveryCodes(userId: string): Promise<number>;
3730
3742
  private validateCode;
3743
+ /**
3744
+ * Find the TOTP time-step counter (within the acceptance window) whose
3745
+ * generated code matches `code`, or null if none matches. Comparison is
3746
+ * timing-safe.
3747
+ */
3748
+ private matchTimeStep;
3749
+ /**
3750
+ * Validate a code AND consume its time-step so it (and any earlier code in
3751
+ * the window) cannot be reused. Returns false for an invalid code or a
3752
+ * replay of an already-consumed time-step. Persists the consumed time-step.
3753
+ */
3754
+ private consumeCode;
3731
3755
  }
3732
3756
  /**
3733
3757
  * Encode bytes to base32 string (RFC 4648, no padding).
package/dist/server.d.ts CHANGED
@@ -3602,6 +3602,13 @@ interface TotpSecret {
3602
3602
  createdAt: number;
3603
3603
  /** When MFA was verified (confirmed with first valid code) */
3604
3604
  verifiedAt: number | null;
3605
+ /**
3606
+ * The most recent TOTP time-step counter that was successfully consumed
3607
+ * during authentication. Used to enforce single-use of codes (RFC 6238 §5.2):
3608
+ * a code at or before this time-step is rejected as a replay. `undefined`
3609
+ * until the first code is consumed.
3610
+ */
3611
+ lastUsedTimeStep?: number;
3605
3612
  }
3606
3613
  /**
3607
3614
  * Setup result returned when enabling TOTP MFA.
@@ -3702,6 +3709,11 @@ declare class TotpManager {
3702
3709
  /**
3703
3710
  * Verify a TOTP code during login.
3704
3711
  * Returns true if the code is valid, false otherwise.
3712
+ *
3713
+ * Enforces single-use: a code is rejected if its time-step has already
3714
+ * been consumed (or predates a previously consumed time-step). This
3715
+ * prevents an attacker who observes one valid code from replaying it
3716
+ * within the acceptance window (RFC 6238 §5.2).
3705
3717
  */
3706
3718
  verify(userId: string, code: string): Promise<boolean>;
3707
3719
  /**
@@ -3728,6 +3740,18 @@ declare class TotpManager {
3728
3740
  */
3729
3741
  remainingRecoveryCodes(userId: string): Promise<number>;
3730
3742
  private validateCode;
3743
+ /**
3744
+ * Find the TOTP time-step counter (within the acceptance window) whose
3745
+ * generated code matches `code`, or null if none matches. Comparison is
3746
+ * timing-safe.
3747
+ */
3748
+ private matchTimeStep;
3749
+ /**
3750
+ * Validate a code AND consume its time-step so it (and any earlier code in
3751
+ * the window) cannot be reused. Returns false for an invalid code or a
3752
+ * replay of an already-consumed time-step. Persists the consumed time-step.
3753
+ */
3754
+ private consumeCode;
3731
3755
  }
3732
3756
  /**
3733
3757
  * Encode bytes to base32 string (RFC 4648, no padding).
package/dist/server.js CHANGED
@@ -77,7 +77,7 @@ var MAX_PASSWORD_LENGTH = 128;
77
77
  var MAX_NAME_LENGTH = 200;
78
78
  var CHALLENGE_TTL_MS = 6e4;
79
79
  function isValidEmail(email) {
80
- if (email.length === 0 || email.length > 254) {
80
+ if (typeof email !== "string" || email.length === 0 || email.length > 254) {
81
81
  return false;
82
82
  }
83
83
  const atIndex = email.indexOf("@");
@@ -97,6 +97,9 @@ function isValidEmail(email) {
97
97
  return true;
98
98
  }
99
99
  function sanitizeName(name) {
100
+ if (typeof name !== "string") {
101
+ return "";
102
+ }
100
103
  const cleaned = name.replace(/[\x00-\x1f\x7f]/g, "");
101
104
  const trimmed = cleaned.trim();
102
105
  if (trimmed.length > MAX_NAME_LENGTH) {
@@ -147,7 +150,7 @@ var BuiltInAuthRoutes = class {
147
150
  }
148
151
  };
149
152
  }
150
- if (body.password.length < MIN_PASSWORD_LENGTH) {
153
+ if (typeof body.password !== "string" || body.password.length < MIN_PASSWORD_LENGTH) {
151
154
  return {
152
155
  status: 400,
153
156
  body: {
@@ -211,6 +214,12 @@ var BuiltInAuthRoutes = class {
211
214
  * @returns Auth response with the user and tokens, or an error
212
215
  */
213
216
  async handleSignIn(body, clientIp) {
217
+ if (typeof body.email !== "string" || typeof body.password !== "string") {
218
+ return {
219
+ status: 400,
220
+ body: { error: "Email and password are required." }
221
+ };
222
+ }
214
223
  const rateLimitKey = clientIp ? `signin:${body.email.toLowerCase()}:${clientIp}` : `signin:${body.email.toLowerCase()}`;
215
224
  if (!await this.rateLimiter.isAllowed(rateLimitKey)) {
216
225
  return {
@@ -714,6 +723,9 @@ function decodeJwt(token) {
714
723
  }
715
724
  }
716
725
  function verifyJwt(token, secret) {
726
+ if (typeof token !== "string") {
727
+ return null;
728
+ }
717
729
  const parts = token.split(".");
718
730
  if (parts.length !== 3) {
719
731
  return null;
@@ -1716,6 +1728,11 @@ function createDefaultTokenManager(options) {
1716
1728
  "createKoraAuthServer requires jwtSecret in production. Set KORA_AUTH_SECRET or pass jwtSecret."
1717
1729
  );
1718
1730
  }
1731
+ if (!secret) {
1732
+ console.warn(
1733
+ "[kora] No JWT secret configured; using an ephemeral random secret. Every token is invalidated when the process restarts. Set KORA_AUTH_SECRET or pass jwtSecret to createKoraAuthServer for stable sessions."
1734
+ );
1735
+ }
1719
1736
  return new TokenManager({
1720
1737
  secret: secret ?? TokenManager.generateSecret(),
1721
1738
  revocationStore: new InMemoryTokenRevocationStore(),
@@ -2018,7 +2035,11 @@ function notFound() {
2018
2035
  return { status: 404, body: { error: "Not found" } };
2019
2036
  }
2020
2037
  function readEnvSecret() {
2021
- return typeof process !== "undefined" ? process.env.KORA_AUTH_SECRET : void 0;
2038
+ if (typeof process === "undefined") {
2039
+ return void 0;
2040
+ }
2041
+ const value = process.env.KORA_AUTH_SECRET;
2042
+ return value && value.trim().length > 0 ? value : void 0;
2022
2043
  }
2023
2044
  function isProduction() {
2024
2045
  return typeof process !== "undefined" && process.env.NODE_ENV === "production";
@@ -3515,7 +3536,7 @@ var MAX_ORG_NAME_LENGTH = 200;
3515
3536
  var MAX_SLUG_LENGTH = 100;
3516
3537
  var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,98}[a-z0-9]$/;
3517
3538
  function isValidEmail2(email) {
3518
- if (email.length === 0 || email.length > 254) return false;
3539
+ if (typeof email !== "string" || email.length === 0 || email.length > 254) return false;
3519
3540
  const atIndex = email.indexOf("@");
3520
3541
  if (atIndex < 1) return false;
3521
3542
  const domain = email.slice(atIndex + 1);
@@ -3783,6 +3804,13 @@ var OrgRoutes = class {
3783
3804
  body: { error: "Cannot assign owner role directly. Use ownership transfer." }
3784
3805
  };
3785
3806
  }
3807
+ const org = await this.store.getOrg(orgId);
3808
+ if (org && org.ownerId === params.targetUserId) {
3809
+ return {
3810
+ status: 403,
3811
+ body: { error: "The organization owner's role cannot be changed. Use ownership transfer." }
3812
+ };
3813
+ }
3786
3814
  const callerMembership = await this.store.getMembership(orgId, userId);
3787
3815
  if (callerMembership && callerMembership.role !== "owner" && params.role === "admin") {
3788
3816
  return { status: 403, body: { error: "Only the owner can assign admin role." } };
@@ -5358,9 +5386,9 @@ var TotpManager = class {
5358
5386
  throw new TotpNotEnabledError(userId);
5359
5387
  }
5360
5388
  if (stored.verified) {
5361
- return this.validateCode(stored.secret, code);
5389
+ return this.consumeCode(stored, code);
5362
5390
  }
5363
- const valid = this.validateCode(stored.secret, code);
5391
+ const valid = await this.consumeCode(stored, code);
5364
5392
  if (!valid) {
5365
5393
  throw new TotpInvalidCodeError();
5366
5394
  }
@@ -5372,6 +5400,11 @@ var TotpManager = class {
5372
5400
  /**
5373
5401
  * Verify a TOTP code during login.
5374
5402
  * Returns true if the code is valid, false otherwise.
5403
+ *
5404
+ * Enforces single-use: a code is rejected if its time-step has already
5405
+ * been consumed (or predates a previously consumed time-step). This
5406
+ * prevents an attacker who observes one valid code from replaying it
5407
+ * within the acceptance window (RFC 6238 §5.2).
5375
5408
  */
5376
5409
  async verify(userId, code) {
5377
5410
  const stored = await this.store.getByUserId(userId);
@@ -5381,7 +5414,7 @@ var TotpManager = class {
5381
5414
  if (!stored.verified) {
5382
5415
  throw new TotpNotVerifiedError(userId);
5383
5416
  }
5384
- return this.validateCode(stored.secret, code);
5417
+ return this.consumeCode(stored, code);
5385
5418
  }
5386
5419
  /**
5387
5420
  * Verify a recovery code as an alternative to TOTP.
@@ -5465,16 +5498,41 @@ var TotpManager = class {
5465
5498
  }
5466
5499
  // --- Private ---
5467
5500
  validateCode(base32Secret, code) {
5501
+ return this.matchTimeStep(base32Secret, code) !== null;
5502
+ }
5503
+ /**
5504
+ * Find the TOTP time-step counter (within the acceptance window) whose
5505
+ * generated code matches `code`, or null if none matches. Comparison is
5506
+ * timing-safe.
5507
+ */
5508
+ matchTimeStep(base32Secret, code) {
5468
5509
  const secretBytes = base32Decode(base32Secret);
5469
5510
  const now = Math.floor(Date.now() / 1e3);
5470
5511
  for (let offset = -this.window; offset <= this.window; offset++) {
5471
5512
  const timeCounter = Math.floor((now + offset * this.period) / this.period);
5472
5513
  const expected = generateTotpCode(secretBytes, timeCounter, this.digits, this.algorithm);
5473
5514
  if (timingSafeEqual(code, expected)) {
5474
- return true;
5515
+ return timeCounter;
5475
5516
  }
5476
5517
  }
5477
- return false;
5518
+ return null;
5519
+ }
5520
+ /**
5521
+ * Validate a code AND consume its time-step so it (and any earlier code in
5522
+ * the window) cannot be reused. Returns false for an invalid code or a
5523
+ * replay of an already-consumed time-step. Persists the consumed time-step.
5524
+ */
5525
+ async consumeCode(stored, code) {
5526
+ const matched = this.matchTimeStep(stored.secret, code);
5527
+ if (matched === null) {
5528
+ return false;
5529
+ }
5530
+ if (stored.lastUsedTimeStep !== void 0 && matched <= stored.lastUsedTimeStep) {
5531
+ return false;
5532
+ }
5533
+ stored.lastUsedTimeStep = matched;
5534
+ await this.store.save(stored);
5535
+ return true;
5478
5536
  }
5479
5537
  };
5480
5538
  function generateTotpCode(secret, counter, digits, algorithm) {