name: stacks-auth description: Use when implementing authentication, authorization, passkeys, TOTP/2FA, RBAC, gates, policies, session auth, token management, email verification, password resets, or rate limiting in a Stacks application. Covers the @stacksjs/auth package, config/auth.ts, app/Gates.ts, and app/Middleware/. license: MIT compatibility: Bun >= 1.3.0, TypeScript allowed-tools: Read Edit Write Bash Grep Glob
Stacks Authentication & Authorization
The @stacksjs/auth package provides comprehensive authentication and authorization for Stacks applications, built on @stacksjs/ts-auth.
Key Paths
- Core package source:
storage/framework/core/auth/src/ - Configuration:
config/auth.ts - Security config:
config/security.ts - Hashing config:
config/hashing.ts - Application gates:
app/Gates.ts - Application middleware:
app/Middleware/ - Middleware aliases:
app/Middleware.ts - Auth types:
storage/framework/core/types/src/auth.ts
Source Files
auth/src/
├── index.ts # All re-exports
├── authentication.ts # Auth class - core auth logic
├── authenticator.ts # 2FA and personal access client
├── client.ts # Client re-exports
├── middleware.ts # Auth middleware handler
├── rate-limiter.ts # RateLimiter class (5 attempts, 15min lockout)
├── passkey.ts # WebAuthn/Passkey support
├── password/reset.ts # Password reset flow
├── register.ts # User registration
├── user.ts # Auth user helpers
├── tokens.ts # Token CRUD, scopes, refresh tokens, OAuth clients
├── gate.ts # Authorization gates & policies
├── policy.ts # BasePolicy class + discovery
├── authorizable.ts # User authorization mixin
├── rbac.ts # Full RBAC system
├── email-verification.ts # Email verification flow
└── session-auth.ts # Session-based SPA auth
Auth Class (authentication.ts) — Static Methods
Login & Authentication
Auth.attempt(credentials: AuthCredentials): Promise<boolean>— validate credentials without creating tokenAuth.validate(credentials: AuthCredentials): Promise<boolean>— alias for attemptAuth.login(credentials: AuthCredentials, options?: TokenCreateOptions): Promise<{ user, token } | null>— login and create tokenAuth.loginUsingId(userId: number, options?: TokenCreateOptions): Promise<{ user, token } | null>— login by user IDAuth.logout(): Promise<void>— revoke current tokenAuth.once(credentials: AuthCredentials): Promise<boolean>— one-time auth without tokenAuth.requestToken(credentials, clientId, clientSecret): Promise<{ token } | null>— OAuth token request
User State
Auth.user(): Promise<UserModel | undefined>— get authenticated user from bearer tokenAuth.check(): Promise<boolean>— is user authenticated?Auth.guest(): Promise<boolean>— is user a guest?Auth.id(): Promise<number | undefined>— get authenticated user IDAuth.setUser(user: UserModel): void— manually set user
Token Creation
Auth.createTokenForUser(user, options?: TokenCreateOptions): Promise<NewAccessToken>Auth.createToken(user, name?, abilities?): Promise<AuthToken>
Token Validation
Auth.validateToken(token: string): Promise<boolean>— validate bearer tokenAuth.getUserFromToken(token: string): Promise<UserModel | undefined>Auth.currentAccessToken(): Promise<PersonalAccessToken | undefined>
Token Abilities (Scopes)
Auth.tokenCan(ability: string): Promise<boolean>Auth.tokenCant(ability: string): Promise<boolean>Auth.tokenAbilities(): Promise<string[]>Auth.tokenCanAll(abilities: string[]): Promise<boolean>Auth.tokenCanAny(abilities: string[]): Promise<boolean>
Token Management
Auth.tokens(userId?: number): Promise<PersonalAccessToken[]>Auth.revokeToken(token: string): Promise<void>Auth.revokeTokenById(tokenId: number): Promise<void>Auth.revokeAllTokens(userId?: number): Promise<void>Auth.revokeOtherTokens(userId?: number): Promise<void>Auth.pruneExpiredTokens(): Promise<number>Auth.pruneRevokedTokens(): Promise<number>Auth.rotateToken(oldToken: string): Promise<AuthToken | null>Auth.findToken(tokenId: number): Promise<PersonalAccessToken | null>
Utility
Auth.guard(name?: string): typeof Auth— select guard (returns self)Auth.viaRemember(): boolean— always false currentlyAuth.clearState(): void— clear cached user/token
Token System (tokens.ts)
Access Tokens
tokens(userId: number): Promise<AccessToken[]>findToken(plainTextToken: string): Promise<AccessToken | null>currentAccessToken(): Promise<AccessToken | null>createToken(userId, name?, scopes?, options?): Promise<PersonalAccessTokenResult>- Options:
{ expiresInMinutes?, withRefreshToken?, refreshExpiresInDays? }
- Options:
Refresh Tokens
refreshToken(refreshTokenPlain, options?): Promise<RefreshTokenResult>validateRefreshToken(refreshTokenPlain): Promise<boolean>revokeRefreshToken(refreshTokenPlain): Promise<void>revokeAllRefreshTokens(userId): Promise<void>deleteExpiredRefreshTokens(): Promise<number>deleteRevokedRefreshTokens(daysOld?): Promise<number>
Token Revocation
revokeToken(plainTextToken): Promise<void>revokeTokenById(tokenId): Promise<void>revokeAllTokens(userId): Promise<void>revokeOtherTokens(userId): Promise<void>deleteExpiredTokens(): Promise<number>deleteRevokedTokens(daysOld?): Promise<number>
Token Scopes
tokenCan(scope): Promise<boolean>tokenCant(scope): Promise<boolean>tokenCanAll(scopes): Promise<boolean>tokenCanAny(scopes): Promise<boolean>tokenAbilities(): Promise<string[]>parseScopes(scopes: string | string[] | null | undefined): TokenScopes
OAuth Clients
clients(userId): Promise<OAuthClient[]>findClient(clientId): Promise<OAuthClient | null>createClient(options: CreateClientOptions): Promise<CreateClientResult>revokeClient(clientId): Promise<void>
Two-Factor Authentication (authenticator.ts)
generateTwoFactorSecret(): stringgenerateTwoFactorToken(secret: Secret): Promise<Token>verifyTwoFactorCode(token: Token, secret: Secret): Promise<boolean>generateTwoFactorUri(user?, service?, secret?): stringcreatePersonalAccessClient(): Promise<Result<string, never>>
Re-exported from @stacksjs/ts-auth:
generateTOTP,verifyTOTP,generateTOTPSecret,totpKeyUri
Authorization Gates (gate.ts)
Gate Functions
define<T>(ability: string, callback: GateCallback<T>): voidpolicy(model: string | { name }, policyClass: new () => Policy): voidbefore(callback): void— run before any gate checkafter(callback): void— run after any gate checkallows(ability, user, ...args): Promise<boolean>denies(ability, user, ...args): Promise<boolean>can(ability, user, ...args): Promise<boolean>cannot(ability, user, ...args): Promise<boolean>any(abilities[], user, ...args): Promise<boolean>all(abilities[], user, ...args): Promise<boolean>none(abilities[], user, ...args): Promise<boolean>authorize(ability, user, ...args): Promise<AuthorizationResponse>— throws on denyinspect(ability, user, ...args): Promise<AuthorizationResponse>— never throwshas(ability): booleanhasPolicy(model): booleanabilities(): string[]getPolicyFor<T>(model: T): Policy<T> | nullflush(): void— clear all gates
Gate Facade — Gate.define(), Gate.can(), etc.
AuthorizationResponse Class
static allow(message?): AuthorizationResponsestatic deny(message?, code?): AuthorizationResponseallowed(): boolean,denied(): booleanauthorize(): void— throws AuthorizationException if denied
Policy Interface
Methods: before?, viewAny?, view?, create?, update?, delete?, restore?, forceDelete?
BasePolicy Abstract Class
Protected helpers: allow(message?), deny(message?, code?), denyIf(condition), denyUnless(condition), allowIf(condition)
RBAC System (rbac.ts)
Role Management
Rbac.createRole(name, guardName?, description?): Promise<RoleRecord>Rbac.findRole(name, guardName?): Promise<RoleRecord | null>Rbac.deleteRole(name, guardName?): Promise<void>Rbac.getAllRoles(guardName?): Promise<RoleRecord[]>
Permission Management
Rbac.createPermission(name, guardName?, description?): Promise<PermissionRecord>Rbac.findPermission(name, guardName?): Promise<PermissionRecord | null>Rbac.deletePermission(name, guardName?): Promise<void>Rbac.getAllPermissions(guardName?): Promise<PermissionRecord[]>
User-Role Operations
Rbac.getUserRoles(user): Promise<RoleRecord[]>Rbac.assignRole(user, roleName, guardName?): Promise<void>Rbac.removeRole(user, roleName, guardName?): Promise<void>Rbac.removeAllRoles(user): Promise<void>Rbac.syncRoles(user, roleNames[], guardName?): Promise<void>Rbac.hasRole(user, roleName, guardName?): Promise<boolean>Rbac.hasAnyRole(user, roleNames[], guardName?): Promise<boolean>Rbac.hasAllRoles(user, roleNames[], guardName?): Promise<boolean>
User-Permission Operations
Rbac.getUserPermissions(user): Promise<PermissionRecord[]>Rbac.givePermission(user, permissionName, guardName?): Promise<void>Rbac.revokePermission(user, permissionName, guardName?): Promise<void>Rbac.revokeAllPermissions(user): Promise<void>Rbac.syncPermissions(user, permissionNames[], guardName?): Promise<void>Rbac.hasPermission(user, permissionName, guardName?): Promise<boolean>Rbac.hasAnyPermission(user, permissionNames[], guardName?): Promise<boolean>Rbac.hasAllPermissions(user, permissionNames[], guardName?): Promise<boolean>
Role-Permission Operations
Rbac.getRolePermissions(roleId): Promise<PermissionRecord[]>Rbac.givePermissionToRole(roleName, permissionName, guardName?): Promise<void>Rbac.revokePermissionFromRole(roleName, permissionName, guardName?): Promise<void>Rbac.syncRolePermissions(roleName, permissionNames[], guardName?): Promise<void>
withRbac Mixin
withRbac(user) — adds hasRole(), hasPermission(), assignRole(), givePermission(), etc. to any user object
RBAC Types
interface RoleRecord { id, name, guard_name, description?, created_at?, updated_at? }
interface PermissionRecord { id, name, guard_name, description?, created_at?, updated_at? }
interface RbacStore { findRoleByName, createRole, deleteRole, getAllRoles, findPermissionByName, createPermission, ... }
Session Auth (session-auth.ts)
SessionAuth.login(email, password): Promise<{ user, sessionId }>SessionAuth.logout(sessionId): voidSessionAuth.user(sessionId): Promise<UserModel | undefined>SessionAuth.check(sessionId): booleanSessionAuth.refresh(sessionId, ttlMs?): boolean
Internal: in-memory Map with 10k session limit, 5-minute eviction interval, timing-safe password comparison with dummy bcrypt hash.
Email Verification (email-verification.ts)
EmailVerification.isVerified(user): booleanEmailVerification.send(user): Promise<void>EmailVerification.verify(userId, token): Promise<EmailVerificationResult>EmailVerification.resend(user): Promise<EmailVerificationResult>
Password Reset (password/reset.ts)
const actions = passwordResets(email)
await actions.sendEmail()
const valid = await actions.verifyToken(token)
const result = await actions.resetPassword(token, newPassword)
Registration (register.ts)
register(credentials: NewUser): Promise<{ token: AuthToken }>
User Helpers (user.ts)
authUser(): Promise<UserModel | undefined>check(): Promise<boolean>id(): Promise<number | undefined>email(): Promise<string | undefined>name(): Promise<string | undefined>isAuthenticated(): Promise<boolean>logout(): Promise<void>refresh(): Promise<void>
Passkey/WebAuthn (passkey.ts)
getUserPasskeys(userId): Promise<PasskeyAttribute[]>getUserPasskey(userId, passkeyId): Promise<PasskeyAttribute | undefined>setCurrentRegistrationOptions(user, verified): Promise<void>
Re-exported from @stacksjs/ts-auth:
generateRegistrationOptions,generateAuthenticationOptionsverifyRegistrationResponse,verifyAuthenticationResponsestartRegistration,startAuthentication(browser)browserSupportsWebAuthn,browserSupportsWebAuthnAutofillplatformAuthenticatorIsAvailable
Auth Middleware (middleware.ts)
export const authMiddlewareHandler = {
name: 'auth',
handle: authMiddleware, // validates bearer token, throws 401
}
Rate Limiter (rate-limiter.ts)
class RateLimiter {
static MAX_ATTEMPTS = 5
static LOCKOUT_DURATION = 15 * 60 * 1000 // 15 minutes
static MAX_STORE_SIZE = 10_000
static EVICTION_INTERVAL = 5 * 60 * 1000 // 5 minutes
static isRateLimited(email): boolean
static recordFailedAttempt(email): void
static resetAttempts(email): void
static validateAttempt(email): void // throws HttpError 429
}
Authorizable Mixin (authorizable.ts)
const authUser = withAuthorization(user)
await authUser.can('edit-post', post)
await authUser.cannot('delete-post', post)
await authUser.canAny(['edit', 'delete'], post)
await authUser.canAll(['edit', 'publish'], post)
await authUser.authorize('edit-post', post) // throws if denied
Configuration
config/auth.ts
{
default: 'api',
guards: { api: { driver: 'token', provider: 'users' } },
providers: { users: { driver: 'database', table: 'users' } },
username: 'email', // AUTH_USERNAME_FIELD env
password: 'password', // AUTH_PASSWORD_FIELD env
tokenExpiry: 30, // days, AUTH_TOKEN_EXPIRY env
tokenRotation: 7, // days, AUTH_TOKEN_ROTATION env
defaultAbilities: ['*'],
defaultTokenName: 'auth_token',
passwordReset: { expire: 60, throttle: 60 }
}
config/hashing.ts
{
driver: 'bcrypt', // 'bcrypt' | 'argon2'
bcrypt: { rounds: 12 },
argon2: { memory: 65536, time: 3 }
}
config/security.ts
{
firewall: {
enabled: true,
countryCodes: [],
ipAddresses: { allowlist: [], blocklist: [] },
rateLimitPerMinute: 500,
useIpReputationLists: true,
useKnownBadInputsRuleSet: true
}
}
Middleware Aliases (app/Middleware.ts)
Available middleware names: maintenance, auth, guest, api, team, logger, abilities, can, throttle, local, development, staging, production, env.local, env.development, env.staging, env.production, role, permission, verified (EnsureEmailIsVerified)
Application Gates Example (app/Gates.ts)
Gate.define('access-admin', (user) => user?.email?.endsWith('@stacksjs.org') ?? false)
Gate.define('edit-settings', (user) => !!user)
Gate.define('view-dashboard', (user) => !!user)
Default API Routes
POST /login→ LoginAction (validates email + password)POST /register→ RegisterActionPOST /auth/refresh→ RefreshTokenActionPOST /auth/token→ CreateTokenActionGET /auth/tokens→ ListTokensAction (auth middleware)DELETE /auth/tokens/{id}→ RevokeTokenAction (auth middleware)GET /me→ GetMeAction (auth middleware)POST /logout→ LogoutAction (auth middleware)
User Model Traits
// User model uses:
traits: {
useAuth: { usePasskey: true },
useUuid: true,
useTimestamps: true,
useSocials: ['github'],
}
Gotchas
- Auth depends on
@stacksjs/ts-authfor TOTP and passkey functions - Password hashing defaults to bcrypt with 12 rounds (config/hashing.ts)
- Rate limiter uses in-memory Map, resets on server restart — not shared across workers
- Session auth also uses in-memory Map with 10k limit — for SPA cookie auth
- Token format is
tokenId|plainText— the|separates the encrypted ID from the plain token - The
parseToken()helper splits on|to extract both parts - Bearer tokens come from the
Authorization: Bearer <token>header Auth.user()internally callsgetBearerToken()→parseToken()→getTokenFromId()→ validates hash- RBAC has an internal cache (
userRoles,userPermissions,rolePermissions) — callRbac.flushCache()after direct DB changes - Gate
beforecallbacks can short-circuit — returntrueto allow,nullto continue checking withRbac()andwithAuthorization()return new objects with methods mixed in- The
RbacStoreinterface must be implemented and set viaRbac.setStore()for RBAC to work - Password reset tokens expire after 60 minutes by default
- Default token abilities are
['*']— wildcard access - Token expiry defaults to 30 days
- Session auth uses timing-safe bcrypt comparison even for failed lookups (dummy hash prevents timing attacks)
Build
cd storage/framework/core/auth && bun build.ts