Add generateSecureBase58Token to shared utils with login_ prefix

- Create packages/utils/src/generate-base58-token.ts with typed prefix support
- Function returns `${TPrefix}${string}` for type-safe prefixed tokens
- Add isBase58() validator and parseBase58Token() helper
- Add comprehensive tests (13 test cases)

- Update login request tokens to use "login_" prefix
- Fix login-password.ts to not replace token (cookie/DB mismatch bug)
- Migrate all token generation from generateSecureToken (hex) to
  generateSecureBase58Token (base58)
- Remove duplicate token generation from api-server/utils/crypto.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 19:31:30 +08:00
parent 68fc67ba4a
commit ddd7c0c03b
10 changed files with 218 additions and 80 deletions

View File

@@ -11,9 +11,9 @@ import {
setCookie,
} from "../../utils/cookies.js";
import {
generateBase58Token,
generateDeviceFingerprint,
generateExpiry,
generateSecureBase58Token,
} from "../../utils/crypto.js";
import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
import { isDeviceTrusted } from "../../utils/session.js";
@@ -62,7 +62,7 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
if (!user) {
// Generate placeholder token (base58) for anti-enumeration
// This prevents attackers from knowing if an email exists based on response
const placeholderToken = generateBase58Token();
const placeholderToken = generateSecureBase58Token("login_");
// Set placeholder login request token cookie
setCookie(
@@ -107,7 +107,7 @@ export const createLoginRequest = os.auth.createLoginRequest.handler(
// Create login request with secure token
const expiresAt = generateExpiry(COOKIE_DURATIONS.LOGIN_REQUEST);
const token = generateBase58Token();
const token = generateSecureBase58Token("login_");
await context.db
.insertInto("login_requests")

View File

@@ -7,7 +7,10 @@
*/
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
import {
generateExpiry,
generateSecureBase58Token,
} from "../../utils/crypto.js";
import { sendPasswordResetEmail } from "../../utils/email.js";
import { os } from "../base.js";
@@ -33,8 +36,8 @@ export const forgotPassword = os.auth.forgotPassword.handler(
.where("user_id", "=", user.id)
.execute();
// Generate secure token (64 hex chars)
const token = generateSecureToken();
// Generate secure base58 token
const token = generateSecureBase58Token();
// Create password reset record with 1 hour expiry
const expiresAt = generateExpiry(TOKEN_DURATIONS.PASSWORD_RESET);

View File

@@ -5,7 +5,6 @@
import { ORPCError } from "@orpc/server";
import { COOKIE_NAMES, getCookie } from "../../utils/cookies.js";
import { generateSecureToken } from "../../utils/crypto.js";
import { sendLoginConfirmationEmail } from "../../utils/email.js";
import { verifyPassword } from "../../utils/password.js";
import { isDeviceTrusted } from "../../utils/session.js";
@@ -47,6 +46,7 @@ export const loginPassword = os.auth.loginPassword.handler(
"login_requests.id",
"login_requests.user_id",
"login_requests.email",
"login_requests.token",
"login_requests.device_fingerprint",
"login_requests.expires_at",
"login_requests.completed_at",
@@ -106,19 +106,9 @@ export const loginPassword = os.auth.loginPassword.handler(
.where("id", "=", result.id)
.execute();
} else {
// Device is untrusted - generate confirmation token and send email
const confirmationToken = generateSecureToken();
await context.db
.updateTable("login_requests")
.set({
token: confirmationToken,
})
.where("id", "=", result.id)
.execute();
// Send login confirmation email
await sendLoginConfirmationEmail(result.email, confirmationToken);
// Device is untrusted - send confirmation email with existing token
// The same base58 token is used for both cookie lookup and email confirmation
await sendLoginConfirmationEmail(result.email, result.token);
}
// Return void (success)

View File

@@ -5,13 +5,16 @@
* Flow:
* 1. Check if email is already verified (return early if so)
* 2. Delete any existing verification tokens for this user
* 3. Generate new secure token (64 hex chars)
* 3. Generate new secure base58 token
* 4. Create new email_verifications record with 24 hour expiry
* 5. Send verification email (stubbed)
*/
import { TOKEN_DURATIONS } from "../../utils/cookies.js";
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
import {
generateExpiry,
generateSecureBase58Token,
} from "../../utils/crypto.js";
import { sendVerificationEmail } from "../../utils/email.js";
import { authMiddleware, os } from "../base.js";
@@ -30,8 +33,8 @@ export const resendVerificationEmail = os.auth.resendVerificationEmail
.where("user_id", "=", context.user.id)
.execute();
// Generate new secure token
const token = generateSecureToken();
// Generate new secure base58 token
const token = generateSecureBase58Token();
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
// Create new verification record

View File

@@ -17,7 +17,10 @@ import {
setCookie,
TOKEN_DURATIONS,
} from "../../utils/cookies.js";
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
import {
generateExpiry,
generateSecureBase58Token,
} from "../../utils/crypto.js";
import { sendVerificationEmail } from "../../utils/email.js";
import { getGeoInfo, getUserAgent } from "../../utils/geo.js";
import { hashPassword, validatePassword } from "../../utils/password.js";
@@ -262,7 +265,7 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
);
// Generate verification token
const verificationToken = generateSecureToken();
const verificationToken = generateSecureBase58Token();
const expiresAt = generateExpiry(TOKEN_DURATIONS.EMAIL_VERIFICATION);
// Store verification token (store raw token, not hash - it's already high-entropy)

View File

@@ -4,7 +4,10 @@
import { ORPCError } from "@orpc/server";
import { ORG_INVITE_EXPIRY_DAYS } from "../../constants.js";
import { generateExpiry, generateSecureToken } from "../../utils/crypto.js";
import {
generateExpiry,
generateSecureBase58Token,
} from "../../utils/crypto.js";
import { sendOrgInviteEmail } from "../../utils/email.js";
import { authMiddleware, os } from "../base.js";
import { getMembership, lookupOrgBySlug, requireRole } from "./helpers.js";
@@ -88,7 +91,7 @@ export const invitesCreate = os.orgs.invites.create
}
// Generate invite token and expiry
const token = generateSecureToken();
const token = generateSecureBase58Token();
const expiresAt = generateExpiry(ORG_INVITE_EXPIRY_DAYS * 24 * 60 * 60);
try {