@stacksjs/auth 0.75.473 → 0.75.477
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/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/two-factor-recovery-codes.d.ts +12 -0
- package/dist/two-factor-recovery-codes.js +1 -0
- package/dist/two-factor.d.ts +2 -0
- package/dist/two-factor.js +1 -1
- package/package.json +14 -14
package/dist/index.d.ts
CHANGED
|
@@ -73,6 +73,7 @@ export {
|
|
|
73
73
|
} from '@stacksjs/ts-auth';
|
|
74
74
|
// TOTP setup/enable/disable + login-challenge persistence
|
|
75
75
|
export * from './two-factor';
|
|
76
|
+
export * from './two-factor-recovery-codes';
|
|
76
77
|
// Team resolution from auth credentials (dashboard-form scoping)
|
|
77
78
|
export * from './team';
|
|
78
79
|
// Referral codes, signup attribution, and server-verified conversions.
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./authentication";export*from"./impersonation";export{requestImpersonation}from"./impersonation-auth";export*from"./impersonation-store";export*from"./authenticator";export*from"./client";export*from"./credential-user";export*from"./middleware";export*from"./oauth-authorization";export*from"./oauth-authorization-endpoint";export*from"./oauth-authorization-requests";export*from"./oauth-authorization-response";export*from"./oauth-authorization-codes";export*from"./oauth-browser-session";export*from"./oauth-client-registration";export*from"./oauth-consent";export*from"./oauth-delegated-access";export*from"./oauth-grants";export*from"./oauth-introspection";export*from"./oauth-metadata";export*from"./oauth-provider";export*from"./oauth-pruning";export*from"./oauth-pkce";export*from"./oauth-revocation";export*from"./oauth-token-exchange";export*from"./oauth-token-endpoint";export*from"./oauth-token-request";export*from"./oauth-token-response";export*from"./rate-limiter";export*from"./magic-link";export*from"./passkey";export*from"./passkeys";export*from"./password/reset";export*from"./password/reset-token";export*from"./register";export*from"./user";export*from"./tokens";export*from"./gate";export*from"./policy";export*from"./authorizable";export*from"./permissions";export*from"./rbac";export{createBqbRbacStore}from"./rbac-store-bqb";export{DEFAULT_ROLE_PACKS,seedDefaultRoles}from"./rbac-seed";export{adminEmails,databaseAdminAccounts,nameFromEmail,provisionAdmins}from"./admins";export*from"./email-verification";export*from"./session-auth";export*from"./cookie-auth";export*from"./browser-session";export*from"./browser-session-store";export*from"./request-token";export*from"./page-gate";export*from"./socials";export*from"./session-handoff";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./team";export*from"./referrals";export*from"./oauth-dynamic-registration";export*from"./oauth-protected-resource";export*from"./oauth-userinfo";export*from"./oauth-server";
|
|
1
|
+
export*from"./authentication";export*from"./impersonation";export{requestImpersonation}from"./impersonation-auth";export*from"./impersonation-store";export*from"./authenticator";export*from"./client";export*from"./credential-user";export*from"./middleware";export*from"./oauth-authorization";export*from"./oauth-authorization-endpoint";export*from"./oauth-authorization-requests";export*from"./oauth-authorization-response";export*from"./oauth-authorization-codes";export*from"./oauth-browser-session";export*from"./oauth-client-registration";export*from"./oauth-consent";export*from"./oauth-delegated-access";export*from"./oauth-grants";export*from"./oauth-introspection";export*from"./oauth-metadata";export*from"./oauth-provider";export*from"./oauth-pruning";export*from"./oauth-pkce";export*from"./oauth-revocation";export*from"./oauth-token-exchange";export*from"./oauth-token-endpoint";export*from"./oauth-token-request";export*from"./oauth-token-response";export*from"./rate-limiter";export*from"./magic-link";export*from"./passkey";export*from"./passkeys";export*from"./password/reset";export*from"./password/reset-token";export*from"./register";export*from"./user";export*from"./tokens";export*from"./gate";export*from"./policy";export*from"./authorizable";export*from"./permissions";export*from"./rbac";export{createBqbRbacStore}from"./rbac-store-bqb";export{DEFAULT_ROLE_PACKS,seedDefaultRoles}from"./rbac-seed";export{adminEmails,databaseAdminAccounts,nameFromEmail,provisionAdmins}from"./admins";export*from"./email-verification";export*from"./session-auth";export*from"./cookie-auth";export*from"./browser-session";export*from"./browser-session-store";export*from"./request-token";export*from"./page-gate";export*from"./socials";export*from"./session-handoff";export{generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri}from"@stacksjs/ts-auth";export*from"./two-factor";export*from"./two-factor-recovery-codes";export*from"./team";export*from"./referrals";export*from"./oauth-dynamic-registration";export*from"./oauth-protected-resource";export*from"./oauth-userinfo";export*from"./oauth-server";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Return plaintext once. Only hashes and the current factor version persist. */
|
|
2
|
+
export declare function generateTwoFactorRecoveryCodes(userId: number): Promise<string[]>;
|
|
3
|
+
export declare function remainingTwoFactorRecoveryCodes(userId: number): Promise<number>;
|
|
4
|
+
/** A recovery code is another second factor, with the same account lockout. */
|
|
5
|
+
export declare function verifyTwoFactorRecoveryCode(userId: number, value: string): Promise<boolean>;
|
|
6
|
+
export declare function verifyTwoFactorCredential(userId: number, code: string): Promise<boolean>;
|
|
7
|
+
/** Management operations require fresh proof of the current factor. */
|
|
8
|
+
export declare function disableTwoFactorWithCode(userId: number, code: string): Promise<boolean>;
|
|
9
|
+
export declare function regenerateTwoFactorRecoveryCodes(userId: number, code: string): Promise<string[] | null>;
|
|
10
|
+
/** A mistyped setup code keeps the same QR usable; promotion and backup codes commit together. */
|
|
11
|
+
export declare function confirmTwoFactorSetup(userId: number, code: string): Promise<TwoFactorSetupResult>;
|
|
12
|
+
export type TwoFactorSetupResult = { enabled: true, recoveryCodes: string[] } | { enabled: false, reason: 'missing' | 'invalid' | 'already_enabled' }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash,randomBytes}from"node:crypto";import{db,getDatabaseDialect,mutationCount,parseSqlDateTime}from"@stacksjs/database/runtime";import{RateLimiter}from"./rate-limiter";import{disableTwoFactor,enableTwoFactor,getTwoFactorState,revokeTwoFactorRecoveryCodes,verifyTwoFactorLoginCode}from"./two-factor";const digest=(value)=>createHash("sha256").update(value).digest("hex");async function withOwner(userId,work){return db.transaction(async()=>{let owner=db.primary.selectFrom("users").where("id","=",userId).select("id");if(getDatabaseDialect()!=="sqlite")owner=owner.lockForUpdate();if(!await owner.executeTakeFirst())throw Error("Account no longer exists");return work()})}export async function generateTwoFactorRecoveryCodes(userId){return withOwner(userId,async()=>{const factor=await getTwoFactorState(userId);if(!factor.enabled||!factor.secret)throw Error("Two-factor authentication is not enabled");const codes=Array.from({length:10},()=>randomBytes(16).toString("hex"));await revokeTwoFactorRecoveryCodes(userId);await db.insertInto("two_factor_recovery_codes").values(codes.map((code)=>({user_id:userId,code_hash:digest(code),secret_digest:digest(factor.secret)}))).execute();return codes.map((code)=>code.match(/.{8}/g).join("-"))})}export async function remainingTwoFactorRecoveryCodes(userId){const factor=await getTwoFactorState(userId);if(!factor.enabled||!factor.secret)return 0;return(await db.primary.selectFrom("two_factor_recovery_codes").select("code_hash").where("user_id","=",userId).where("secret_digest","=",digest(factor.secret)).execute()).length}export async function verifyTwoFactorRecoveryCode(userId,value){const rateKey=`2fa:${userId}`;await RateLimiter.validateAttempt(rateKey);const code=value.replace(/[\s-]/g,"").toLowerCase(),verified=/^[a-f0-9]{32}$/.test(code)&&await withOwner(userId,async()=>{const factor=await getTwoFactorState(userId);if(!factor.enabled||!factor.secret)return!1;const claimed=await db.deleteFrom("two_factor_recovery_codes").where("user_id","=",userId).where("code_hash","=",digest(code)).where("secret_digest","=",digest(factor.secret)).execute();return mutationCount(claimed)===1});if(verified)await RateLimiter.resetAttempts(rateKey);else await RateLimiter.recordFailedAttempt(rateKey);return verified}export async function verifyTwoFactorCredential(userId,code){return/^\d{6}$/.test(code)?verifyTwoFactorLoginCode(userId,code):verifyTwoFactorRecoveryCode(userId,code)}export async function disableTwoFactorWithCode(userId,code){return withOwner(userId,async()=>{if(!await verifyTwoFactorCredential(userId,code))return!1;await disableTwoFactor(userId);return!0})}export async function regenerateTwoFactorRecoveryCodes(userId,code){return withOwner(userId,async()=>{if(!await verifyTwoFactorCredential(userId,code))return null;return generateTwoFactorRecoveryCodes(userId)})}export async function confirmTwoFactorSetup(userId,code){return withOwner(userId,async()=>{if((await getTwoFactorState(userId)).enabled)return{enabled:!1,reason:"already_enabled"};const pending=await db.primary.selectFrom("two_factor_pending_secrets").select(["secret","expires_at"]).where("user_id","=",userId).executeTakeFirst();if(!pending||(parseSqlDateTime(pending.expires_at)?.getTime()??0)<=Date.now())return{enabled:!1,reason:"missing"};if(!await enableTwoFactor(userId,String(pending.secret),code))return{enabled:!1,reason:"invalid"};const recoveryCodes=await generateTwoFactorRecoveryCodes(userId);await db.deleteFrom("two_factor_pending_secrets").where("user_id","=",userId).execute();await db.deleteFrom("two_factor_challenges").where("user_id","=",userId).execute();return{enabled:!0,recoveryCodes}})}
|
package/dist/two-factor.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export declare function consumePendingTwoFactorSecret(userId: number): Promise<s
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function enableTwoFactor(userId: number, secret: string, code: string): Promise<boolean>;
|
|
37
37
|
export declare function disableTwoFactor(userId: number): Promise<void>;
|
|
38
|
+
/** Invalidate backup credentials, including on installations predating their schema. */
|
|
39
|
+
export declare function revokeTwoFactorRecoveryCodes(userId: number): Promise<void>;
|
|
38
40
|
/**
|
|
39
41
|
* Verify a live login/dashboard-reauth code against the user's
|
|
40
42
|
* already-persisted secret.
|
package/dist/two-factor.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomBytes}from"node:crypto";import{db,getDatabaseDialect,mutationCount,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{verifyTOTPWithCounter}from"@stacksjs/ts-auth";import{generateTwoFactorSecret,generateTwoFactorUri,twoFactorQrCode}from"./authenticator";import{RateLimiter}from"./rate-limiter";const DEFAULT_CHALLENGE_TTL_SECONDS=300,BASELINE_CHALLENGE_PREFIX="baseline.",REMEMBERED_CHALLENGE_PREFIX="remembered.";function challengeExpiry(ttlSeconds){const deadline=new Date(Date.now()+ttlSeconds*1000);if(getDatabaseDialect()==="mysql")deadline.setUTCMilliseconds(0);return sqlDateTime(deadline)}const TWO_FACTOR_RATE_LIMIT_PREFIX="2fa:";async function getLastUsedTwoFactorStep(userId){const value=(await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_last_used_step"]).executeTakeFirst())?.two_factor_last_used_step;return value==null?null:Number(value)}async function claimTwoFactorStep(userId,secret,step,lastStep){let claim=db.updateTable("users").set({two_factor_last_used_step:step}).where("id","=",userId).where("two_factor_secret","=",secret).where("two_factor_enabled","=",!0);claim=lastStep===null?claim.whereNull("two_factor_last_used_step"):claim.where("two_factor_last_used_step","=",lastStep);return mutationCount(await claim.execute())===1}export async function getTwoFactorState(userId){const row=await db.primary.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=challengeExpiry(ttlSeconds);await db.transaction(async()=>{await db.upsert("two_factor_pending_secrets",[{user_id:userId,secret,expires_at:expiresAt}],["user_id"],["secret","expires_at"]);const stored=await db.primary.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).select(["secret","expires_at"]).executeTakeFirst();if(stored?.secret!==secret||parseSqlDateTime(stored.expires_at)?.getTime()!==parseSqlDateTime(expiresAt)?.getTime())throw Error("[auth] Pending two-factor setup could not be stored.")})}export async function consumePendingTwoFactorSecret(userId){const row=await db.primary.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).select(["secret","expires_at"]).executeTakeFirst();if(!row)return null;const claimed=await db.deleteFrom("two_factor_pending_secrets").where("user_id","=",userId).where("secret","=",row.secret).where("expires_at","=",row.expires_at).execute();if(mutationCount(claimed)!==1)return null;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){const{valid,counter:step}=await verifyTOTPWithCounter(code,{secret});if(!valid||step===null)return!1;return persistTwoFactorState(userId,secret,!0,step)}export async function disableTwoFactor(userId){await persistTwoFactorState(userId,null,!1,null)}async function persistTwoFactorState(userId,secret,enabled,lastUsedStep){return db.transaction(async()=>{await db.updateTable("users").set({two_factor_secret:secret,two_factor_enabled:enabled,two_factor_last_used_step:lastUsedStep}).where("id","=",userId).execute();const stored=await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_secret","two_factor_enabled"]).executeTakeFirst();if(!stored)return!1;if(stored.two_factor_secret!==secret||Boolean(stored.two_factor_enabled)!==enabled)throw Error("[auth] Two-factor state could not be stored.");return!0})}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;const{valid,counter:step}=await verifyTOTPWithCounter(code,{secret});if(!valid||step===null){await RateLimiter.recordFailedAttempt(rateKey);return!1}const lastStep=await getLastUsedTwoFactorStep(userId);if(lastStep!==null&&step<=lastStep)return!1;if(!await claimTwoFactorStep(userId,secret,step,lastStep))return!1;await RateLimiter.resetAttempts(rateKey);return!0}export async function createTwoFactorChallenge(userId,options={}){const ttlSeconds=typeof options==="number"?options:options.ttlSeconds??DEFAULT_CHALLENGE_TTL_SECONDS,id=`${typeof options==="object"&&options.remembered===!0?REMEMBERED_CHALLENGE_PREFIX:BASELINE_CHALLENGE_PREFIX}${randomBytes(32).toString("hex")}`,expiresAt=challengeExpiry(ttlSeconds);await db.transaction(async()=>{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();const stored=await db.primary.selectFrom("two_factor_challenges").where("user_id","=",userId).select("id").execute();if(stored.length!==1||stored[0]?.id!==id)throw Error("[auth] Two-factor login challenge could not be stored.")});return id}export async function consumeTwoFactorChallenge(challengeToken){const row=await db.primary.selectFrom("two_factor_challenges").where("id","=",challengeToken).select(["user_id","expires_at"]).executeTakeFirst();if(!row)return null;const claimed=await db.deleteFrom("two_factor_challenges").where("id","=",challengeToken).execute();if(mutationCount(claimed)!==1)return null;const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>=expiresAt)return null;return Number(row.user_id)}export async function withTwoFactorChallenge(challengeToken,complete){const pending=await db.primary.selectFrom("two_factor_challenges").where("id","=",challengeToken).select("user_id").executeTakeFirst();if(!pending)return null;const userId=Number(pending.user_id),outcome=await db.transaction(async()=>{let owner=db.primary.selectFrom("users").where("id","=",userId).select("id");if(getDatabaseDialect()!=="sqlite")owner=owner.lockForUpdate();if(!await owner.executeTakeFirst())return{value:null};if(await consumeTwoFactorChallenge(challengeToken)!==userId)return{value:null};try{return{value:await db.transaction(()=>complete(userId,{remembered:challengeToken.startsWith(REMEMBERED_CHALLENGE_PREFIX)}))}}catch(error){return{error}}});if("error"in outcome)throw outcome.error;return outcome.value}export async function revokeTwoFactorChallenges(userId){try{await db.transaction(async()=>{await db.deleteFrom("two_factor_challenges").where("user_id","=",userId).execute();if(await db.primary.selectFrom("two_factor_challenges").where("user_id","=",userId).select("id").executeTakeFirst())throw Error("[auth] Two-factor login challenges could not be revoked.")})}catch(error){const message=error&&typeof error==="object"&&"message"in error&&typeof error.message==="string"?error.message:String(error);if(/^(?:no such table: (?:main\.)?two_factor_challenges|relation "two_factor_challenges" does not exist|Table '(?:[^'.]+\.)?two_factor_challenges' doesn't exist)$/i.test(message))return;throw error}}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,getDatabaseDialect,mutationCount,parseSqlDateTime,sqlDateTime}from"@stacksjs/database/runtime";import{verifyTOTPWithCounter}from"@stacksjs/ts-auth";import{generateTwoFactorSecret,generateTwoFactorUri,twoFactorQrCode}from"./authenticator";import{RateLimiter}from"./rate-limiter";const DEFAULT_CHALLENGE_TTL_SECONDS=300,BASELINE_CHALLENGE_PREFIX="baseline.",REMEMBERED_CHALLENGE_PREFIX="remembered.";function challengeExpiry(ttlSeconds){const deadline=new Date(Date.now()+ttlSeconds*1000);if(getDatabaseDialect()==="mysql")deadline.setUTCMilliseconds(0);return sqlDateTime(deadline)}const TWO_FACTOR_RATE_LIMIT_PREFIX="2fa:";async function getLastUsedTwoFactorStep(userId){const value=(await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_last_used_step"]).executeTakeFirst())?.two_factor_last_used_step;return value==null?null:Number(value)}async function claimTwoFactorStep(userId,secret,step,lastStep){let claim=db.updateTable("users").set({two_factor_last_used_step:step}).where("id","=",userId).where("two_factor_secret","=",secret).where("two_factor_enabled","=",!0);claim=lastStep===null?claim.whereNull("two_factor_last_used_step"):claim.where("two_factor_last_used_step","=",lastStep);return mutationCount(await claim.execute())===1}export async function getTwoFactorState(userId){const row=await db.primary.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=challengeExpiry(ttlSeconds);await db.transaction(async()=>{await db.upsert("two_factor_pending_secrets",[{user_id:userId,secret,expires_at:expiresAt}],["user_id"],["secret","expires_at"]);const stored=await db.primary.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).select(["secret","expires_at"]).executeTakeFirst();if(stored?.secret!==secret||parseSqlDateTime(stored.expires_at)?.getTime()!==parseSqlDateTime(expiresAt)?.getTime())throw Error("[auth] Pending two-factor setup could not be stored.")})}export async function consumePendingTwoFactorSecret(userId){const row=await db.primary.selectFrom("two_factor_pending_secrets").where("user_id","=",userId).select(["secret","expires_at"]).executeTakeFirst();if(!row)return null;const claimed=await db.deleteFrom("two_factor_pending_secrets").where("user_id","=",userId).where("secret","=",row.secret).where("expires_at","=",row.expires_at).execute();if(mutationCount(claimed)!==1)return null;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){const{valid,counter:step}=await verifyTOTPWithCounter(code,{secret});if(!valid||step===null)return!1;return persistTwoFactorState(userId,secret,!0,step)}export async function disableTwoFactor(userId){await db.transaction(async()=>{await persistTwoFactorState(userId,null,!1,null);await revokeTwoFactorRecoveryCodes(userId);await revokeTwoFactorChallenges(userId)})}export async function revokeTwoFactorRecoveryCodes(userId){try{await db.transaction(async()=>{await db.deleteFrom("two_factor_recovery_codes").where("user_id","=",userId).execute();if(await db.primary.selectFrom("two_factor_recovery_codes").select("user_id").where("user_id","=",userId).executeTakeFirst())throw Error("[auth] Two-factor recovery codes could not be revoked.")})}catch(error){const message=error&&typeof error==="object"&&"message"in error&&typeof error.message==="string"?error.message:String(error);if(/^(?:no such table: (?:main\.)?two_factor_recovery_codes|relation "two_factor_recovery_codes" does not exist|Table '(?:[^'.]+\.)?two_factor_recovery_codes' doesn't exist)$/i.test(message))return;throw error}}async function persistTwoFactorState(userId,secret,enabled,lastUsedStep){return db.transaction(async()=>{await db.updateTable("users").set({two_factor_secret:secret,two_factor_enabled:enabled,two_factor_last_used_step:lastUsedStep}).where("id","=",userId).execute();const stored=await db.primary.selectFrom("users").where("id","=",userId).select(["two_factor_secret","two_factor_enabled"]).executeTakeFirst();if(!stored)return!1;if(stored.two_factor_secret!==secret||Boolean(stored.two_factor_enabled)!==enabled)throw Error("[auth] Two-factor state could not be stored.");return!0})}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;const{valid,counter:step}=await verifyTOTPWithCounter(code,{secret});if(!valid||step===null){await RateLimiter.recordFailedAttempt(rateKey);return!1}const lastStep=await getLastUsedTwoFactorStep(userId);if(lastStep!==null&&step<=lastStep)return!1;if(!await claimTwoFactorStep(userId,secret,step,lastStep))return!1;await RateLimiter.resetAttempts(rateKey);return!0}export async function createTwoFactorChallenge(userId,options={}){const ttlSeconds=typeof options==="number"?options:options.ttlSeconds??DEFAULT_CHALLENGE_TTL_SECONDS,id=`${typeof options==="object"&&options.remembered===!0?REMEMBERED_CHALLENGE_PREFIX:BASELINE_CHALLENGE_PREFIX}${randomBytes(32).toString("hex")}`,expiresAt=challengeExpiry(ttlSeconds);await db.transaction(async()=>{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();const stored=await db.primary.selectFrom("two_factor_challenges").where("user_id","=",userId).select("id").execute();if(stored.length!==1||stored[0]?.id!==id)throw Error("[auth] Two-factor login challenge could not be stored.")});return id}export async function consumeTwoFactorChallenge(challengeToken){const row=await db.primary.selectFrom("two_factor_challenges").where("id","=",challengeToken).select(["user_id","expires_at"]).executeTakeFirst();if(!row)return null;const claimed=await db.deleteFrom("two_factor_challenges").where("id","=",challengeToken).execute();if(mutationCount(claimed)!==1)return null;const expiresAt=parseSqlDateTime(row.expires_at)?.getTime()??0;if(Date.now()>=expiresAt)return null;return Number(row.user_id)}export async function withTwoFactorChallenge(challengeToken,complete){const pending=await db.primary.selectFrom("two_factor_challenges").where("id","=",challengeToken).select("user_id").executeTakeFirst();if(!pending)return null;const userId=Number(pending.user_id),outcome=await db.transaction(async()=>{let owner=db.primary.selectFrom("users").where("id","=",userId).select("id");if(getDatabaseDialect()!=="sqlite")owner=owner.lockForUpdate();if(!await owner.executeTakeFirst())return{value:null};if(await consumeTwoFactorChallenge(challengeToken)!==userId)return{value:null};try{return{value:await db.transaction(()=>complete(userId,{remembered:challengeToken.startsWith(REMEMBERED_CHALLENGE_PREFIX)}))}}catch(error){return{error}}});if("error"in outcome)throw outcome.error;return outcome.value}export async function revokeTwoFactorChallenges(userId){try{await db.transaction(async()=>{await db.deleteFrom("two_factor_challenges").where("user_id","=",userId).execute();if(await db.primary.selectFrom("two_factor_challenges").where("user_id","=",userId).select("id").executeTakeFirst())throw Error("[auth] Two-factor login challenges could not be revoked.")})}catch(error){const message=error&&typeof error==="object"&&"message"in error&&typeof error.message==="string"?error.message:String(error);if(/^(?:no such table: (?:main\.)?two_factor_challenges|relation "two_factor_challenges" does not exist|Table '(?:[^'.]+\.)?two_factor_challenges' doesn't exist)$/i.test(message))return;throw error}}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.75.
|
|
5
|
+
"version": "0.75.477",
|
|
6
6
|
"description": "A more simplistic way to authenticate.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -59,19 +59,19 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@stacksjs/bun-router": "^0.1.24",
|
|
62
|
-
"@stacksjs/cache": "0.75.
|
|
63
|
-
"@stacksjs/config": "0.75.
|
|
64
|
-
"@stacksjs/database": "0.75.
|
|
65
|
-
"@stacksjs/email": "0.75.
|
|
66
|
-
"@stacksjs/env": "0.75.
|
|
67
|
-
"@stacksjs/error-handling": "0.75.
|
|
68
|
-
"@stacksjs/logging": "0.75.
|
|
69
|
-
"@stacksjs/orm": "0.75.
|
|
70
|
-
"@stacksjs/path": "0.75.
|
|
71
|
-
"@stacksjs/router": "0.75.
|
|
72
|
-
"@stacksjs/security": "0.75.
|
|
73
|
-
"@stacksjs/socials": "0.75.
|
|
74
|
-
"@stacksjs/storage": "0.75.
|
|
62
|
+
"@stacksjs/cache": "0.75.477",
|
|
63
|
+
"@stacksjs/config": "0.75.477",
|
|
64
|
+
"@stacksjs/database": "0.75.477",
|
|
65
|
+
"@stacksjs/email": "0.75.477",
|
|
66
|
+
"@stacksjs/env": "0.75.477",
|
|
67
|
+
"@stacksjs/error-handling": "0.75.477",
|
|
68
|
+
"@stacksjs/logging": "0.75.477",
|
|
69
|
+
"@stacksjs/orm": "0.75.477",
|
|
70
|
+
"@stacksjs/path": "0.75.477",
|
|
71
|
+
"@stacksjs/router": "0.75.477",
|
|
72
|
+
"@stacksjs/security": "0.75.477",
|
|
73
|
+
"@stacksjs/socials": "0.75.477",
|
|
74
|
+
"@stacksjs/storage": "0.75.477",
|
|
75
75
|
"@stacksjs/ts-auth": "^0.4.7",
|
|
76
76
|
"ts-qr-codes": "^0.1.8"
|
|
77
77
|
},
|