@spfn/auth 0.2.0-beta.70 → 0.2.0-beta.72

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.js CHANGED
@@ -5658,6 +5658,25 @@ var init_keys_repository = __esm({
5658
5658
  ).returning();
5659
5659
  return result[0] ?? null;
5660
5660
  }
5661
+ /**
5662
+ * 사용자의 모든 활성 공개키 revoke (비활성화)
5663
+ *
5664
+ * 비번 변경 시 전체 세션 로그아웃에 사용. authenticate는 활성 키만 검증하므로,
5665
+ * revoke된 키로 서명한 기존 세션의 요청은 즉시 401이 된다.
5666
+ * Write primary 사용
5667
+ */
5668
+ async revokeAllActiveByUserId(userId, reason) {
5669
+ return await this.db.update(userPublicKeys).set({
5670
+ isActive: false,
5671
+ revokedAt: /* @__PURE__ */ new Date(),
5672
+ revokedReason: reason
5673
+ }).where(
5674
+ and2(
5675
+ eq2(userPublicKeys.userId, userId),
5676
+ eq2(userPublicKeys.isActive, true)
5677
+ )
5678
+ ).returning();
5679
+ }
5661
5680
  /**
5662
5681
  * 공개키 삭제
5663
5682
  * Write primary 사용
@@ -6936,13 +6955,23 @@ function getPublicKeyObject(publicKeyB64) {
6936
6955
  publicKeyCache.set(publicKeyB64, keyObject);
6937
6956
  return keyObject;
6938
6957
  }
6958
+ var INSECURE_JWT_SECRET = "dev-secret-key-change-in-production";
6959
+ function getJwtSecret() {
6960
+ const secret = env2.SPFN_AUTH_JWT_SECRET;
6961
+ if ((!secret || secret === INSECURE_JWT_SECRET) && process.env.NODE_ENV === "production") {
6962
+ throw new Error(
6963
+ "SPFN_AUTH_JWT_SECRET must be set to a strong secret in production (the default is public)."
6964
+ );
6965
+ }
6966
+ return secret;
6967
+ }
6939
6968
  function generateToken(payload) {
6940
- return jwt.sign(payload, env2.SPFN_AUTH_JWT_SECRET, {
6969
+ return jwt.sign(payload, getJwtSecret(), {
6941
6970
  expiresIn: env2.SPFN_AUTH_JWT_EXPIRES_IN
6942
6971
  });
6943
6972
  }
6944
6973
  function verifyToken(token) {
6945
- return jwt.verify(token, env2.SPFN_AUTH_JWT_SECRET, { algorithms: ["HS256"] });
6974
+ return jwt.verify(token, getJwtSecret(), { algorithms: ["HS256"] });
6946
6975
  }
6947
6976
  function verifyClientToken(token, publicKeyB64, algorithm) {
6948
6977
  const publicKeyObject = getPublicKeyObject(publicKeyB64);
@@ -7082,6 +7111,9 @@ async function validateVerificationCode(target, code, purpose) {
7082
7111
  if (!record) {
7083
7112
  return { valid: false, error: "Invalid verification code" };
7084
7113
  }
7114
+ if (record.attempts >= MAX_VERIFICATION_ATTEMPTS) {
7115
+ return { valid: false, error: "Too many attempts, please request a new code" };
7116
+ }
7085
7117
  if (record.code !== code) {
7086
7118
  await verificationCodesRepository.incrementAttempts(record.id);
7087
7119
  return { valid: false, error: "Invalid verification code" };
@@ -7092,9 +7124,6 @@ async function validateVerificationCode(target, code, purpose) {
7092
7124
  if (/* @__PURE__ */ new Date() > new Date(record.expiresAt)) {
7093
7125
  return { valid: false, error: "Verification code expired" };
7094
7126
  }
7095
- if (record.attempts >= MAX_VERIFICATION_ATTEMPTS) {
7096
- return { valid: false, error: "Too many attempts, please request a new code" };
7097
- }
7098
7127
  return { valid: true, codeId: record.id };
7099
7128
  }
7100
7129
  async function markCodeAsUsed(codeId) {
@@ -7549,6 +7578,7 @@ async function changePasswordService(params) {
7549
7578
  }
7550
7579
  const newPasswordHash = await hashPassword(newPassword);
7551
7580
  await usersRepository.updatePassword(userId, newPasswordHash, true);
7581
+ await keysRepository.revokeAllActiveByUserId(userId, "Revoked by password change");
7552
7582
  }
7553
7583
 
7554
7584
  // src/server/services/rbac.service.ts
@@ -7574,6 +7604,10 @@ var COOKIE_NAMES = {
7574
7604
  /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
7575
7605
  get OAUTH_PENDING() {
7576
7606
  return `spfn_oauth_pending${getCookieSuffix()}`;
7607
+ },
7608
+ /** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
7609
+ get OAUTH_CSRF() {
7610
+ return `spfn_oauth_csrf${getCookieSuffix()}`;
7577
7611
  }
7578
7612
  };
7579
7613
  function parseDuration(duration) {
@@ -8287,11 +8321,14 @@ function generateNonce() {
8287
8321
  crypto.getRandomValues(array);
8288
8322
  return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
8289
8323
  }
8324
+ function generateOAuthNonce() {
8325
+ return generateNonce();
8326
+ }
8290
8327
  async function createOAuthState(params) {
8291
8328
  const key = await getStateKey();
8292
8329
  const state = {
8293
8330
  returnUrl: params.returnUrl,
8294
- nonce: generateNonce(),
8331
+ nonce: params.nonce ?? generateNonce(),
8295
8332
  provider: params.provider,
8296
8333
  publicKey: params.publicKey,
8297
8334
  keyId: params.keyId,
@@ -8509,7 +8546,7 @@ function tokenExpiryDate(expiresIn) {
8509
8546
  return new Date(Date.now() + expiresIn * 1e3);
8510
8547
  }
8511
8548
  async function oauthStartService(params) {
8512
- const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata } = params;
8549
+ const { provider, returnUrl, publicKey, keyId, fingerprint, algorithm, metadata, nonce } = params;
8513
8550
  const oauthProvider = requireEnabledProvider(provider);
8514
8551
  const state = await createOAuthState({
8515
8552
  provider,
@@ -8518,13 +8555,19 @@ async function oauthStartService(params) {
8518
8555
  keyId,
8519
8556
  fingerprint,
8520
8557
  algorithm,
8521
- metadata
8558
+ metadata,
8559
+ nonce
8522
8560
  });
8523
8561
  return { authUrl: oauthProvider.getAuthUrl(state) };
8524
8562
  }
8525
8563
  async function oauthCallbackService(params) {
8526
- const { provider, code, state } = params;
8564
+ const { provider, code, state, expectedNonce } = params;
8527
8565
  const stateData = await verifyOAuthState(state);
8566
+ if (!expectedNonce || stateData.nonce !== expectedNonce) {
8567
+ throw new ValidationError5({
8568
+ message: "OAuth state validation failed"
8569
+ });
8570
+ }
8528
8571
  if (stateData.provider !== provider) {
8529
8572
  throw new ValidationError5({
8530
8573
  message: "OAuth state provider mismatch"
@@ -9251,7 +9294,8 @@ var acceptInvitation2 = route2.post("/_auth/invitations/accept").input({
9251
9294
  }),
9252
9295
  password: Type.String({
9253
9296
  minLength: 8,
9254
- description: "User password (minimum 8 characters)"
9297
+ maxLength: 72,
9298
+ description: "User password (8\u201372 characters). bcrypt silently ignores bytes past 72, so longer inputs are rejected rather than truncated."
9255
9299
  })
9256
9300
  })
9257
9301
  }).interceptor({
@@ -9477,6 +9521,193 @@ var userRouter = defineRouter3({
9477
9521
 
9478
9522
  // src/server/routes/oauth/index.ts
9479
9523
  init_esm();
9524
+
9525
+ // ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/url.js
9526
+ var tryDecode = (str, decoder) => {
9527
+ try {
9528
+ return decoder(str);
9529
+ } catch {
9530
+ return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
9531
+ try {
9532
+ return decoder(match);
9533
+ } catch {
9534
+ return match;
9535
+ }
9536
+ });
9537
+ }
9538
+ };
9539
+ var decodeURIComponent_ = decodeURIComponent;
9540
+
9541
+ // ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/utils/cookie.js
9542
+ var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
9543
+ var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
9544
+ var trimCookieWhitespace = (value) => {
9545
+ let start = 0;
9546
+ let end = value.length;
9547
+ while (start < end) {
9548
+ const charCode = value.charCodeAt(start);
9549
+ if (charCode !== 32 && charCode !== 9) {
9550
+ break;
9551
+ }
9552
+ start++;
9553
+ }
9554
+ while (end > start) {
9555
+ const charCode = value.charCodeAt(end - 1);
9556
+ if (charCode !== 32 && charCode !== 9) {
9557
+ break;
9558
+ }
9559
+ end--;
9560
+ }
9561
+ return start === 0 && end === value.length ? value : value.slice(start, end);
9562
+ };
9563
+ var parse = (cookie, name) => {
9564
+ if (name && cookie.indexOf(name) === -1) {
9565
+ return {};
9566
+ }
9567
+ const pairs = cookie.split(";");
9568
+ const parsedCookie = /* @__PURE__ */ Object.create(null);
9569
+ for (const pairStr of pairs) {
9570
+ const valueStartPos = pairStr.indexOf("=");
9571
+ if (valueStartPos === -1) {
9572
+ continue;
9573
+ }
9574
+ const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos));
9575
+ if (name && name !== cookieName || !validCookieNameRegEx.test(cookieName) || cookieName in parsedCookie) {
9576
+ continue;
9577
+ }
9578
+ let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1));
9579
+ if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
9580
+ cookieValue = cookieValue.slice(1, -1);
9581
+ }
9582
+ if (validCookieValueRegEx.test(cookieValue)) {
9583
+ parsedCookie[cookieName] = cookieValue.indexOf("%") !== -1 ? tryDecode(cookieValue, decodeURIComponent_) : cookieValue;
9584
+ if (name) {
9585
+ break;
9586
+ }
9587
+ }
9588
+ }
9589
+ return parsedCookie;
9590
+ };
9591
+ var _serialize = (name, value, opt = {}) => {
9592
+ if (!validCookieNameRegEx.test(name)) {
9593
+ throw new Error("Invalid cookie name");
9594
+ }
9595
+ let cookie = `${name}=${value}`;
9596
+ if (name.startsWith("__Secure-") && !opt.secure) {
9597
+ throw new Error("__Secure- Cookie must have Secure attributes");
9598
+ }
9599
+ if (name.startsWith("__Host-")) {
9600
+ if (!opt.secure) {
9601
+ throw new Error("__Host- Cookie must have Secure attributes");
9602
+ }
9603
+ if (opt.path !== "/") {
9604
+ throw new Error('__Host- Cookie must have Path attributes with "/"');
9605
+ }
9606
+ if (opt.domain) {
9607
+ throw new Error("__Host- Cookie must not have Domain attributes");
9608
+ }
9609
+ }
9610
+ for (const key of ["domain", "path", "sameSite", "priority"]) {
9611
+ if (opt[key] && /[;\r\n]/.test(opt[key])) {
9612
+ throw new Error(`${key} must not contain ";", "\\r", or "\\n"`);
9613
+ }
9614
+ }
9615
+ if (opt && typeof opt.maxAge === "number" && opt.maxAge >= 0) {
9616
+ if (opt.maxAge > 3456e4) {
9617
+ throw new Error(
9618
+ "Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration."
9619
+ );
9620
+ }
9621
+ cookie += `; Max-Age=${opt.maxAge | 0}`;
9622
+ }
9623
+ if (opt.domain && opt.prefix !== "host") {
9624
+ cookie += `; Domain=${opt.domain}`;
9625
+ }
9626
+ if (opt.path) {
9627
+ cookie += `; Path=${opt.path}`;
9628
+ }
9629
+ if (opt.expires) {
9630
+ if (opt.expires.getTime() - Date.now() > 3456e7) {
9631
+ throw new Error(
9632
+ "Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future."
9633
+ );
9634
+ }
9635
+ cookie += `; Expires=${opt.expires.toUTCString()}`;
9636
+ }
9637
+ if (opt.httpOnly) {
9638
+ cookie += "; HttpOnly";
9639
+ }
9640
+ if (opt.secure) {
9641
+ cookie += "; Secure";
9642
+ }
9643
+ if (opt.sameSite) {
9644
+ cookie += `; SameSite=${opt.sameSite.charAt(0).toUpperCase() + opt.sameSite.slice(1)}`;
9645
+ }
9646
+ if (opt.priority) {
9647
+ cookie += `; Priority=${opt.priority.charAt(0).toUpperCase() + opt.priority.slice(1)}`;
9648
+ }
9649
+ if (opt.partitioned) {
9650
+ if (!opt.secure) {
9651
+ throw new Error("Partitioned Cookie must have Secure attributes");
9652
+ }
9653
+ cookie += "; Partitioned";
9654
+ }
9655
+ return cookie;
9656
+ };
9657
+ var serialize = (name, value, opt) => {
9658
+ value = encodeURIComponent(value);
9659
+ return _serialize(name, value, opt);
9660
+ };
9661
+
9662
+ // ../../node_modules/.pnpm/hono@4.12.27/node_modules/hono/dist/helper/cookie/index.js
9663
+ var getCookie = (c, key, prefix) => {
9664
+ const cookie = c.req.raw.headers.get("Cookie");
9665
+ if (typeof key === "string") {
9666
+ if (!cookie) {
9667
+ return void 0;
9668
+ }
9669
+ let finalKey = key;
9670
+ if (prefix === "secure") {
9671
+ finalKey = "__Secure-" + key;
9672
+ } else if (prefix === "host") {
9673
+ finalKey = "__Host-" + key;
9674
+ }
9675
+ const obj2 = parse(cookie, finalKey);
9676
+ return obj2[finalKey];
9677
+ }
9678
+ if (!cookie) {
9679
+ return {};
9680
+ }
9681
+ const obj = parse(cookie);
9682
+ return obj;
9683
+ };
9684
+ var generateCookie = (name, value, opt) => {
9685
+ let cookie;
9686
+ if (opt?.prefix === "secure") {
9687
+ cookie = serialize("__Secure-" + name, value, { path: "/", ...opt, secure: true });
9688
+ } else if (opt?.prefix === "host") {
9689
+ cookie = serialize("__Host-" + name, value, {
9690
+ ...opt,
9691
+ path: "/",
9692
+ secure: true,
9693
+ domain: void 0
9694
+ });
9695
+ } else {
9696
+ cookie = serialize(name, value, { path: "/", ...opt });
9697
+ }
9698
+ return cookie;
9699
+ };
9700
+ var setCookie = (c, name, value, opt) => {
9701
+ const cookie = generateCookie(name, value, opt);
9702
+ c.header("Set-Cookie", cookie, { append: true });
9703
+ };
9704
+ var deleteCookie = (c, name, opt) => {
9705
+ const deletedCookie = getCookie(c, name, opt?.prefix);
9706
+ setCookie(c, name, "", { ...opt, maxAge: 0 });
9707
+ return deletedCookie;
9708
+ };
9709
+
9710
+ // src/server/routes/oauth/index.ts
9480
9711
  init_types();
9481
9712
  import { Transactional as Transactional3 } from "@spfn/core/db";
9482
9713
  import { ValidationError as ValidationError7 } from "@spfn/core/errors";
@@ -9525,11 +9756,14 @@ var oauthGoogleCallback = route4.get("/_auth/oauth/google/callback").input({
9525
9756
  if (!query.code || !query.state) {
9526
9757
  return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
9527
9758
  }
9759
+ const expectedNonce = getCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF);
9760
+ deleteCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, { path: "/" });
9528
9761
  try {
9529
9762
  const result = await oauthCallbackService({
9530
9763
  provider: "google",
9531
9764
  code: query.code,
9532
- state: query.state
9765
+ state: query.state,
9766
+ expectedNonce
9533
9767
  });
9534
9768
  return c.redirect(result.redirectUrl);
9535
9769
  } catch (err) {
@@ -9563,7 +9797,15 @@ var oauthStart = route4.post("/_auth/oauth/start").input({
9563
9797
  })
9564
9798
  }).use([rateLimit2({ limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
9565
9799
  const { body } = await c.data();
9566
- const result = await oauthStartService(body);
9800
+ const nonce = generateOAuthNonce();
9801
+ setCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, nonce, {
9802
+ httpOnly: true,
9803
+ secure: process.env.NODE_ENV === "production",
9804
+ sameSite: "Lax",
9805
+ maxAge: 600,
9806
+ path: "/"
9807
+ });
9808
+ const result = await oauthStartService({ ...body, nonce });
9567
9809
  return result;
9568
9810
  });
9569
9811
  var oauthProviders = route4.get("/_auth/oauth/providers").skip(["auth"]).handler(async () => {
@@ -9647,11 +9889,14 @@ var oauthProviderCallback = route4.get("/_auth/oauth/:provider/callback").input(
9647
9889
  if (!query.code || !query.state) {
9648
9890
  return c.redirect(buildOAuthErrorUrl("Missing authorization code or state"));
9649
9891
  }
9892
+ const expectedNonce = getCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF);
9893
+ deleteCookie(c.raw, COOKIE_NAMES.OAUTH_CSRF, { path: "/" });
9650
9894
  try {
9651
9895
  const result = await oauthCallbackService({
9652
9896
  provider: params.provider,
9653
9897
  code: query.code,
9654
- state: query.state
9898
+ state: query.state,
9899
+ expectedNonce
9655
9900
  });
9656
9901
  return c.redirect(result.redirectUrl);
9657
9902
  } catch (err) {
@@ -10233,6 +10478,7 @@ export {
10233
10478
  generateKeyPair,
10234
10479
  generateKeyPairES256,
10235
10480
  generateKeyPairRS256,
10481
+ generateOAuthNonce,
10236
10482
  generateToken,
10237
10483
  getAllRoles,
10238
10484
  getAuth,