@stacksjs/auth 0.73.3 → 0.74.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.
@@ -13,5 +13,18 @@ export declare function verifyTwoFactorCode(token: Token, secret: Secret): Promi
13
13
  * @returns The otpauth:// URI string
14
14
  */
15
15
  export declare function generateTwoFactorUri(user?: string, service?: string, secret?: Secret): string;
16
+ /**
17
+ * Render an otpauth:// URI as a scannable QR code.
18
+ *
19
+ * SVG rather than a data URI: it stays sharp at any size, can be inlined into
20
+ * a server-rendered page or an email, and is a few hundred bytes rather than
21
+ * tens of kilobytes. `ts-qr-codes` is a pure, synchronous string function with
22
+ * no runtime dependencies of its own, so this costs nothing at import time.
23
+ *
24
+ * Every authenticator setup flow needs this. Leaving it to the caller meant
25
+ * each one either shipped an encoder to the browser or, more often, fell back
26
+ * to asking the user to type a 32-character secret by hand.
27
+ */
28
+ export declare function twoFactorQrCode(uri: string, size?: number): string;
16
29
  export type Token = string;
17
30
  export type Secret = string;
@@ -1 +1 @@
1
- import{generateTOTP,generateTOTPSecret,totpKeyUri,verifyTOTP}from"@stacksjs/ts-auth";export function generateTwoFactorSecret(){return generateTOTPSecret()}export async function generateTwoFactorToken(secret){return generateTOTP({secret})}export async function verifyTwoFactorCode(token,secret){return verifyTOTP(token,{secret})}export function generateTwoFactorUri(user,service,secret){const userIdentifier=user||"johndoe@example.com",serviceName=service||"StacksJS 2fa",otpSecret=secret||generateTwoFactorSecret();return totpKeyUri(userIdentifier,serviceName,otpSecret)}
1
+ import{generateTOTP,generateTOTPSecret,totpKeyUri,verifyTOTP}from"@stacksjs/ts-auth";import{toSvg}from"ts-qr-codes";export function generateTwoFactorSecret(){return generateTOTPSecret()}export async function generateTwoFactorToken(secret){return generateTOTP({secret})}export async function verifyTwoFactorCode(token,secret){return verifyTOTP(token,{secret})}export function generateTwoFactorUri(user,service,secret){const userIdentifier=user||"johndoe@example.com",serviceName=service||"StacksJS 2fa",otpSecret=secret||generateTwoFactorSecret();return totpKeyUri(userIdentifier,serviceName,otpSecret)}const DEFAULT_QR_SIZE=240;export function twoFactorQrCode(uri,size=DEFAULT_QR_SIZE){return toSvg(uri,{size,title:"Two-factor authentication setup"})}
@@ -8,9 +8,15 @@
8
8
  export declare function getTwoFactorState(userId: number): Promise<{ secret: string | null, enabled: boolean }>;
9
9
  export declare function isTwoFactorEnabled(user: TwoFactorUser): boolean;
10
10
  /**
11
- * Generate a new (unpersisted) secret + otpauth:// URI for setup.
11
+ * Generate a new (unpersisted) secret + otpauth:// URI for setup, along with
12
+ * the URI rendered as a scannable QR code.
13
+ *
14
+ * The QR code comes back here rather than being left to the caller because
15
+ * every setup screen needs one, and a caller that has to find its own encoder
16
+ * either ships one to the browser or falls back to asking the user to type a
17
+ * 32-character secret by hand. Clients that render their own still have `uri`.
12
18
  */
13
- export declare function generateTwoFactorSetup(email: string, serviceName?: string): { secret: string, uri: string };
19
+ export declare function generateTwoFactorSetup(email: string, serviceName?: string): { secret: string, uri: string, qr: string };
14
20
  /**
15
21
  * Stash a freshly generated secret server-side while the user goes
16
22
  * scan/enter it into their authenticator app. Single pending secret
@@ -1 +1 @@
1
- import{randomBytes}from"node:crypto";import{db,sqlDateTime,parseSqlDateTime}from"@stacksjs/database";import{generateTwoFactorSecret,generateTwoFactorUri,verifyTwoFactorCode}from"./authenticator";import{RateLimiter}from"./rate-limiter";const DEFAULT_CHALLENGE_TTL_SECONDS=300,TWO_FACTOR_RATE_LIMIT_PREFIX="2fa:",TWO_FACTOR_STEP_SECONDS=30;function currentTotpStep(){return Math.floor(Date.now()/1000/TWO_FACTOR_STEP_SECONDS)}async function getLastUsedTwoFactorStep(userId){try{const value=(await db.selectFrom("users").where("id","=",userId).selectAll().executeTakeFirst())?.two_factor_last_used_step;return value==null?null:Number(value)}catch{return null}}async function setLastUsedTwoFactorStep(userId,step){try{await db.updateTable("users").set({two_factor_last_used_step:step}).where("id","=",userId).executeTakeFirst()}catch{}}export async function getTwoFactorState(userId){const row=await db.selectFrom("users").where("id","=",userId).select(["two_factor_secret","two_factor_enabled"]).executeTakeFirst();return{secret:row?.two_factor_secret??null,enabled:Boolean(row?.two_factor_enabled)}}export function isTwoFactorEnabled(user){return Boolean(user.two_factor_enabled)}export function generateTwoFactorSetup(email,serviceName){const secret=generateTwoFactorSecret(),uri=generateTwoFactorUri(email,serviceName,secret);return{secret,uri}}const PENDING_SECRET_TTL_SECONDS=600;export async function stashPendingTwoFactorSecret(userId,secret,ttlSeconds=PENDING_SECRET_TTL_SECONDS){const expiresAt=sqlDateTime(new Date(Date.now()+ttlSeconds*1000));await db.deleteFrom("two_factor_pending_secrets").where("user_id","=",userId).execute();await db.insertInto("two_factor_pending_secrets").values({user_id:userId,secret,expires_at:expiresAt}).execute()}export async function consumePendingTwoFactorSecret(userId){const row=await db.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).selectAll().executeTakeFirst();if(!row)return null;await db.deleteFrom("two_factor_pending_secrets").where("user_id","=",userId).execute();const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>expiresAt)return null;return String(row.secret)}export async function enableTwoFactor(userId,secret,code){if(!await verifyTwoFactorCode(code,secret))return!1;await db.updateTable("users").set({two_factor_secret:secret,two_factor_enabled:!0}).where("id","=",userId).executeTakeFirst();return!0}export async function disableTwoFactor(userId){await db.updateTable("users").set({two_factor_secret:null,two_factor_enabled:!1}).where("id","=",userId).executeTakeFirst()}export async function verifyTwoFactorLoginCode(userId,code){const rateKey=`${TWO_FACTOR_RATE_LIMIT_PREFIX}${userId}`;await RateLimiter.validateAttempt(rateKey);const{secret,enabled}=await getTwoFactorState(userId);if(!enabled||!secret)return!1;if(!await verifyTwoFactorCode(code,secret)){await RateLimiter.recordFailedAttempt(rateKey);return!1}const step=currentTotpStep(),lastStep=await getLastUsedTwoFactorStep(userId);if(lastStep!==null&&step<=lastStep)return!1;await setLastUsedTwoFactorStep(userId,step);await RateLimiter.resetAttempts(rateKey);return!0}export async function createTwoFactorChallenge(userId,ttlSeconds=DEFAULT_CHALLENGE_TTL_SECONDS){const id=randomBytes(32).toString("hex"),expiresAt=sqlDateTime(new Date(Date.now()+ttlSeconds*1000));await db.deleteFrom("two_factor_challenges").where("user_id","=",userId).execute();await db.insertInto("two_factor_challenges").values({id,user_id:userId,expires_at:expiresAt}).execute();return id}export async function consumeTwoFactorChallenge(challengeToken){const row=await db.selectFrom("two_factor_challenges").where("id","=",challengeToken).selectAll().executeTakeFirst();if(!row)return null;await db.deleteFrom("two_factor_challenges").where("id","=",challengeToken).execute();const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>expiresAt)return null;return Number(row.user_id)}export const TwoFactor={isEnabled:isTwoFactorEnabled,getState:getTwoFactorState,generateSetup:generateTwoFactorSetup,stashPendingSecret:stashPendingTwoFactorSecret,consumePendingSecret:consumePendingTwoFactorSecret,enable:enableTwoFactor,disable:disableTwoFactor,verifyLoginCode:verifyTwoFactorLoginCode,createChallenge:createTwoFactorChallenge,consumeChallenge:consumeTwoFactorChallenge};
1
+ import{randomBytes}from"node:crypto";import{db,sqlDateTime,parseSqlDateTime}from"@stacksjs/database";import{generateTwoFactorSecret,generateTwoFactorUri,twoFactorQrCode,verifyTwoFactorCode}from"./authenticator";import{RateLimiter}from"./rate-limiter";const DEFAULT_CHALLENGE_TTL_SECONDS=300,TWO_FACTOR_RATE_LIMIT_PREFIX="2fa:",TWO_FACTOR_STEP_SECONDS=30;function currentTotpStep(){return Math.floor(Date.now()/1000/TWO_FACTOR_STEP_SECONDS)}async function getLastUsedTwoFactorStep(userId){try{const value=(await db.selectFrom("users").where("id","=",userId).selectAll().executeTakeFirst())?.two_factor_last_used_step;return value==null?null:Number(value)}catch{return null}}async function setLastUsedTwoFactorStep(userId,step){try{await db.updateTable("users").set({two_factor_last_used_step:step}).where("id","=",userId).executeTakeFirst()}catch{}}export async function getTwoFactorState(userId){const row=await db.selectFrom("users").where("id","=",userId).select(["two_factor_secret","two_factor_enabled"]).executeTakeFirst();return{secret:row?.two_factor_secret??null,enabled:Boolean(row?.two_factor_enabled)}}export function isTwoFactorEnabled(user){return Boolean(user.two_factor_enabled)}export function generateTwoFactorSetup(email,serviceName){const secret=generateTwoFactorSecret(),uri=generateTwoFactorUri(email,serviceName,secret);return{secret,uri,qr:twoFactorQrCode(uri)}}const PENDING_SECRET_TTL_SECONDS=600;export async function stashPendingTwoFactorSecret(userId,secret,ttlSeconds=PENDING_SECRET_TTL_SECONDS){const expiresAt=sqlDateTime(new Date(Date.now()+ttlSeconds*1000));await db.deleteFrom("two_factor_pending_secrets").where("user_id","=",userId).execute();await db.insertInto("two_factor_pending_secrets").values({user_id:userId,secret,expires_at:expiresAt}).execute()}export async function consumePendingTwoFactorSecret(userId){const row=await db.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).selectAll().executeTakeFirst();if(!row)return null;await db.deleteFrom("two_factor_pending_secrets").where("user_id","=",userId).execute();const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>expiresAt)return null;return String(row.secret)}export async function enableTwoFactor(userId,secret,code){if(!await verifyTwoFactorCode(code,secret))return!1;await db.updateTable("users").set({two_factor_secret:secret,two_factor_enabled:!0}).where("id","=",userId).executeTakeFirst();return!0}export async function disableTwoFactor(userId){await db.updateTable("users").set({two_factor_secret:null,two_factor_enabled:!1}).where("id","=",userId).executeTakeFirst()}export async function verifyTwoFactorLoginCode(userId,code){const rateKey=`${TWO_FACTOR_RATE_LIMIT_PREFIX}${userId}`;await RateLimiter.validateAttempt(rateKey);const{secret,enabled}=await getTwoFactorState(userId);if(!enabled||!secret)return!1;if(!await verifyTwoFactorCode(code,secret)){await RateLimiter.recordFailedAttempt(rateKey);return!1}const step=currentTotpStep(),lastStep=await getLastUsedTwoFactorStep(userId);if(lastStep!==null&&step<=lastStep)return!1;await setLastUsedTwoFactorStep(userId,step);await RateLimiter.resetAttempts(rateKey);return!0}export async function createTwoFactorChallenge(userId,ttlSeconds=DEFAULT_CHALLENGE_TTL_SECONDS){const id=randomBytes(32).toString("hex"),expiresAt=sqlDateTime(new Date(Date.now()+ttlSeconds*1000));await db.deleteFrom("two_factor_challenges").where("user_id","=",userId).execute();await db.insertInto("two_factor_challenges").values({id,user_id:userId,expires_at:expiresAt}).execute();return id}export async function consumeTwoFactorChallenge(challengeToken){const row=await db.selectFrom("two_factor_challenges").where("id","=",challengeToken).selectAll().executeTakeFirst();if(!row)return null;await db.deleteFrom("two_factor_challenges").where("id","=",challengeToken).execute();const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>expiresAt)return null;return Number(row.user_id)}export const TwoFactor={isEnabled:isTwoFactorEnabled,getState:getTwoFactorState,generateSetup:generateTwoFactorSetup,stashPendingSecret:stashPendingTwoFactorSecret,consumePendingSecret:consumePendingTwoFactorSecret,enable:enableTwoFactor,disable:disableTwoFactor,verifyLoginCode:verifyTwoFactorLoginCode,createChallenge:createTwoFactorChallenge,consumeChallenge:consumeTwoFactorChallenge};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/auth",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.73.3",
5
+ "version": "0.74.0",
6
6
  "description": "A more simplistic way to authenticate.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,11 +58,22 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@stacksjs/bun-router": "^0.1.8",
61
- "@stacksjs/ts-auth": "^0.4.4"
61
+ "@stacksjs/cache": "0.74.0",
62
+ "@stacksjs/config": "0.74.0",
63
+ "@stacksjs/database": "0.74.0",
64
+ "@stacksjs/email": "0.74.0",
65
+ "@stacksjs/env": "0.74.0",
66
+ "@stacksjs/error-handling": "0.74.0",
67
+ "@stacksjs/logging": "0.74.0",
68
+ "@stacksjs/orm": "0.74.0",
69
+ "@stacksjs/path": "0.74.0",
70
+ "@stacksjs/router": "0.74.0",
71
+ "@stacksjs/security": "0.74.0",
72
+ "@stacksjs/storage": "0.74.0",
73
+ "@stacksjs/ts-auth": "^0.4.4",
74
+ "ts-qr-codes": "^0.1.8"
62
75
  },
63
76
  "devDependencies": {
64
- "better-dx": "^0.2.24",
65
- "@stacksjs/error-handling": "0.73.3",
66
- "@stacksjs/router": "0.73.3"
77
+ "better-dx": "^0.2.24"
67
78
  }
68
79
  }