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 token
Auth.validate(credentials: AuthCredentials): Promise<boolean> — alias for attempt
Auth.login(credentials: AuthCredentials, options?: TokenCreateOptions): Promise<{ user, token } | null> — login and create token
Auth.loginUsingId(userId: number, options?: TokenCreateOptions): Promise<{ user, token } | null> — login by user ID
Auth.logout(): Promise<void> — revoke current token
Personal access tokens (Sanctum-shaped)
oauth_access_tokens is polymorphic: tokenable_type holds the owner's TABLE
name (users, authors) and tokenable_id its id there, so any model
declaring useAuth can hold tokens - not only User.
createToken(id, name, scopes, { tokenableType }) — mint one. Returns the
plaintext ONCE (plainTextToken); the table stores a hash and nothing can
recover it afterwards. tokenableType defaults to users.
tokens(id, tokenableType?) — list an owner's live tokens.
tokenCan(scope) / tokenCanAll / tokenCanAny / tokenAbilities — check
the current request's token.
revokeToken, revokeTokenById, revokeAllTokens(id, type?),
revokeOtherTokens(id, type?) — revocation also revokes the paired refresh
token, which a raw row delete does not.
setTrailActor(id) — attribute writes in a queue job or CLI run that has no
request to read a user from.
The PersonalAccessToken model maps the same table, so owner.with('tokenable')
lists exactly what createToken minted. It deliberately generates no CRUD
routes: minting and revoking both carry semantics a generic route does not.
Auth.once(credentials: AuthCredentials): Promise<boolean> — one-time auth without token
Auth.requestToken(credentials, clientId, clientSecret): Promise<{ token } | null> — OAuth token request
User State
Auth.user(): Promise<UserModel | undefined> — get authenticated user from bearer token
Auth.check(): Promise<boolean> — is user authenticated?
Auth.guest(): Promise<boolean> — is user a guest?
Auth.id(): Promise<number | undefined> — get authenticated user ID
Auth.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 token
Auth.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 currently
Auth.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? }
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(): string
generateTwoFactorToken(secret: Secret): Promise<Token>
verifyTwoFactorCode(token: Token, secret: Secret): Promise<boolean>
generateTwoFactorUri(user?, service?, secret?): string
createPersonalAccessClient(): 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>): void
policy(model: string | { name }, policyClass: new () => Policy): void
before(callback): void — run before any gate check
after(callback): void — run after any gate check
allows(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 deny
inspect(ability, user, ...args): Promise<AuthorizationResponse> — never throws
has(ability): boolean
hasPolicy(model): boolean
abilities(): string[]
getPolicyFor<T>(model: T): Policy<T> | null
flush(): void — clear all gates
Gate Facade — Gate.define(), Gate.can(), etc
AuthorizationResponse Class
static allow(message?): AuthorizationResponse
static deny(message?, code?): AuthorizationResponse
allowed(): boolean, denied(): boolean
authorize(): 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> - replaces assignments for that guard and preserves roles from other guards
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): void
SessionAuth.user(sessionId): Promise<UserModel | undefined>
SessionAuth.check(sessionId): boolean
SessionAuth.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): boolean
EmailVerification.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, generateAuthenticationOptions
verifyRegistrationResponse, verifyAuthenticationResponse
startRegistration, startAuthentication (browser)
browserSupportsWebAuthn, browserSupportsWebAuthnAutofill
platformAuthenticatorIsAvailable
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)
Auth-relevant aliases: auth, guest, verified (EnsureEmailIsVerified),
abilities, can, role, permission, team, signed, throttle. The
environment aliases are env, env:local, env:development / env:dev,
env:staging, env:production / env:prod — with a COLON, not a dot; an
earlier version of this list wrote env.local and those never existed. See
stacks-middleware for the full set and for the !alias and alias:params
forms.
Application Gates (app/Gates.ts)
import { defineGates } from '@stacksjs/auth'
export default defineGates({
gates: {
'access-admin': user => user?.email?.endsWith('@stacksjs.com') ?? false,
'edit-settings': user => !!user,
'view-dashboard': user => !!user,
},
policies: {
Post: 'PostPolicy',
},
})
Registered at boot by initializeAuthorization(), from
injectGlobalAutoImports() — the one place every entry point comes through, so
HTTP, buddy seed, a scheduled job and a console command all get the same
gates.
Both halves of policies are checked: the key names a model the ORM exposes,
the value a policy file under app/Policies/ or the framework defaults. An
explicit mapping WINS over the <Model>Policy naming convention, which is the
reason to write one.
Gate.define(...) still works for a gate registered at runtime; defineGates
is the declarative form and the one the ability-name completions come from.
Default API Routes
POST /login → LoginAction (validates email + password)
POST /register → RegisterAction
POST /auth/refresh → RefreshTokenAction
POST /auth/token → CreateTokenAction
GET /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-auth for 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 calls getBearerToken() → parseToken() → getTokenFromId() → validates hash
- RBAC has an internal cache (
userRoles, userPermissions, rolePermissions) — call Rbac.flushCache() after direct DB changes
syncRoles() and syncPermissions() are guard-scoped replacements: they preserve assignments belonging to other guards
- Gate
before callbacks can short-circuit — return true to allow, null to continue checking
- An ability with no gate and no policy method denies. That is the right default, and it means a gate that was never registered is indistinguishable from one that says no — which is how
initializeAuthorization() went unnoticed while nothing called it
allows() and friends take Ability, which is open (GateName | PolicyAbility | (string & {})). A /can/:ability route passes an ability straight through, so narrowing it would reject correct code; the union is for completions
withRbac() and withAuthorization() return new objects with methods mixed in
- The
RbacStore interface must be implemented and set via Rbac.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
1---2name: stacks-auth-43description: 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/.4license: MIT5---67# Stacks Authentication & Authorization89The `@stacksjs/auth` package provides comprehensive authentication and authorization for Stacks applications, built on `@stacksjs/ts-auth`.1011## Key Paths1213- Core package source: `storage/framework/core/auth/src/`14- Configuration: `config/auth.ts`15- Security config: `config/security.ts`16- Hashing config: `config/hashing.ts`17- Application gates: `app/Gates.ts`18- Application middleware: `app/Middleware/`19- Middleware aliases: `app/Middleware.ts`20- Auth types: `storage/framework/core/types/src/auth.ts`2122## Source Files2324```25auth/src/26├── index.ts # All re-exports27├── authentication.ts # Auth class - core auth logic28├── authenticator.ts # 2FA and personal access client29├── client.ts # Client re-exports30├── middleware.ts # Auth middleware handler31├── rate-limiter.ts # RateLimiter class (5 attempts, 15min lockout)32├── passkey.ts # WebAuthn/Passkey support33├── password/reset.ts # Password reset flow34├── register.ts # User registration35├── user.ts # Auth user helpers36├── tokens.ts # Token CRUD, scopes, refresh tokens, OAuth clients37├── gate.ts # Authorization gates & policies38├── policy.ts # BasePolicy class + discovery39├── authorizable.ts # User authorization mixin40├── rbac.ts # Full RBAC system41├── email-verification.ts # Email verification flow42└── session-auth.ts # Session-based SPA auth43```4445## Auth Class (authentication.ts) — Static Methods4647### Login & Authentication48- `Auth.attempt(credentials: AuthCredentials): Promise<boolean>` — validate credentials without creating token49- `Auth.validate(credentials: AuthCredentials): Promise<boolean>` — alias for attempt50- `Auth.login(credentials: AuthCredentials, options?: TokenCreateOptions): Promise<{ user, token } | null>` — login and create token51- `Auth.loginUsingId(userId: number, options?: TokenCreateOptions): Promise<{ user, token } | null>` — login by user ID52- `Auth.logout(): Promise<void>` — revoke current token5354### Personal access tokens (Sanctum-shaped)5556`oauth_access_tokens` is polymorphic: `tokenable_type` holds the owner's TABLE57name (`users`, `authors`) and `tokenable_id` its id there, so any model58declaring `useAuth` can hold tokens - not only `User`.5960- `createToken(id, name, scopes, { tokenableType })` — mint one. Returns the61 plaintext ONCE (`plainTextToken`); the table stores a hash and nothing can62 recover it afterwards. `tokenableType` defaults to `users`.63- `tokens(id, tokenableType?)` — list an owner's live tokens.64- `tokenCan(scope)` / `tokenCanAll` / `tokenCanAny` / `tokenAbilities` — check65 the current request's token.66- `revokeToken`, `revokeTokenById`, `revokeAllTokens(id, type?)`,67 `revokeOtherTokens(id, type?)` — revocation also revokes the paired refresh68 token, which a raw row delete does not.69- `setTrailActor(id)` — attribute writes in a queue job or CLI run that has no70 request to read a user from.7172The `PersonalAccessToken` model maps the same table, so `owner.with('tokenable')`73lists exactly what `createToken` minted. It deliberately generates no CRUD74routes: minting and revoking both carry semantics a generic route does not.75- `Auth.once(credentials: AuthCredentials): Promise<boolean>` — one-time auth without token76- `Auth.requestToken(credentials, clientId, clientSecret): Promise<{ token } | null>` — OAuth token request7778### User State79- `Auth.user(): Promise<UserModel | undefined>` — get authenticated user from bearer token80- `Auth.check(): Promise<boolean>` — is user authenticated?81- `Auth.guest(): Promise<boolean>` — is user a guest?82- `Auth.id(): Promise<number | undefined>` — get authenticated user ID83- `Auth.setUser(user: UserModel): void` — manually set user8485### Token Creation86- `Auth.createTokenForUser(user, options?: TokenCreateOptions): Promise<NewAccessToken>`87- `Auth.createToken(user, name?, abilities?): Promise<AuthToken>`8889### Token Validation90- `Auth.validateToken(token: string): Promise<boolean>` — validate bearer token91- `Auth.getUserFromToken(token: string): Promise<UserModel | undefined>`92- `Auth.currentAccessToken(): Promise<PersonalAccessToken | undefined>`9394### Token Abilities (Scopes)95- `Auth.tokenCan(ability: string): Promise<boolean>`96- `Auth.tokenCant(ability: string): Promise<boolean>`97- `Auth.tokenAbilities(): Promise<string[]>`98- `Auth.tokenCanAll(abilities: string[]): Promise<boolean>`99- `Auth.tokenCanAny(abilities: string[]): Promise<boolean>`100101### Token Management102- `Auth.tokens(userId?: number): Promise<PersonalAccessToken[]>`103- `Auth.revokeToken(token: string): Promise<void>`104- `Auth.revokeTokenById(tokenId: number): Promise<void>`105- `Auth.revokeAllTokens(userId?: number): Promise<void>`106- `Auth.revokeOtherTokens(userId?: number): Promise<void>`107- `Auth.pruneExpiredTokens(): Promise<number>`108- `Auth.pruneRevokedTokens(): Promise<number>`109- `Auth.rotateToken(oldToken: string): Promise<AuthToken | null>`110- `Auth.findToken(tokenId: number): Promise<PersonalAccessToken | null>`111112### Utility113- `Auth.guard(name?: string): typeof Auth` — select guard (returns self)114- `Auth.viaRemember(): boolean` — always false currently115- `Auth.clearState(): void` — clear cached user/token116117## Token System (tokens.ts)118119### Access Tokens120- `tokens(userId: number): Promise<AccessToken[]>`121- `findToken(plainTextToken: string): Promise<AccessToken | null>`122- `currentAccessToken(): Promise<AccessToken | null>`123- `createToken(userId, name?, scopes?, options?): Promise<PersonalAccessTokenResult>`124 - Options: `{ expiresInMinutes?, withRefreshToken?, refreshExpiresInDays? }`125126### Refresh Tokens127- `refreshToken(refreshTokenPlain, options?): Promise<RefreshTokenResult>`128- `validateRefreshToken(refreshTokenPlain): Promise<boolean>`129- `revokeRefreshToken(refreshTokenPlain): Promise<void>`130- `revokeAllRefreshTokens(userId): Promise<void>`131- `deleteExpiredRefreshTokens(): Promise<number>`132- `deleteRevokedRefreshTokens(daysOld?): Promise<number>`133134### Token Revocation135- `revokeToken(plainTextToken): Promise<void>`136- `revokeTokenById(tokenId): Promise<void>`137- `revokeAllTokens(userId): Promise<void>`138- `revokeOtherTokens(userId): Promise<void>`139- `deleteExpiredTokens(): Promise<number>`140- `deleteRevokedTokens(daysOld?): Promise<number>`141142### Token Scopes143- `tokenCan(scope): Promise<boolean>`144- `tokenCant(scope): Promise<boolean>`145- `tokenCanAll(scopes): Promise<boolean>`146- `tokenCanAny(scopes): Promise<boolean>`147- `tokenAbilities(): Promise<string[]>`148- `parseScopes(scopes: string | string[] | null | undefined): TokenScopes`149150### OAuth Clients151- `clients(userId): Promise<OAuthClient[]>`152- `findClient(clientId): Promise<OAuthClient | null>`153- `createClient(options: CreateClientOptions): Promise<CreateClientResult>`154- `revokeClient(clientId): Promise<void>`155156## Two-Factor Authentication (authenticator.ts)157158- `generateTwoFactorSecret(): string`159- `generateTwoFactorToken(secret: Secret): Promise<Token>`160- `verifyTwoFactorCode(token: Token, secret: Secret): Promise<boolean>`161- `generateTwoFactorUri(user?, service?, secret?): string`162- `createPersonalAccessClient(): Promise<Result<string, never>>`163164### Re-exported from @stacksjs/ts-auth165- `generateTOTP`, `verifyTOTP`, `generateTOTPSecret`, `totpKeyUri`166167## Authorization Gates (gate.ts)168169### Gate Functions170- `define<T>(ability: string, callback: GateCallback<T>): void`171- `policy(model: string | { name }, policyClass: new () => Policy): void`172- `before(callback): void` — run before any gate check173- `after(callback): void` — run after any gate check174- `allows(ability, user, ...args): Promise<boolean>`175- `denies(ability, user, ...args): Promise<boolean>`176- `can(ability, user, ...args): Promise<boolean>`177- `cannot(ability, user, ...args): Promise<boolean>`178- `any(abilities[], user, ...args): Promise<boolean>`179- `all(abilities[], user, ...args): Promise<boolean>`180- `none(abilities[], user, ...args): Promise<boolean>`181- `authorize(ability, user, ...args): Promise<AuthorizationResponse>` — throws on deny182- `inspect(ability, user, ...args): Promise<AuthorizationResponse>` — never throws183- `has(ability): boolean`184- `hasPolicy(model): boolean`185- `abilities(): string[]`186- `getPolicyFor<T>(model: T): Policy<T> | null`187- `flush(): void` — clear all gates188189### Gate Facade — `Gate.define()`, `Gate.can()`, etc190191### AuthorizationResponse Class192- `static allow(message?): AuthorizationResponse`193- `static deny(message?, code?): AuthorizationResponse`194- `allowed(): boolean`, `denied(): boolean`195- `authorize(): void` — throws AuthorizationException if denied196197### Policy Interface198Methods: `before?`, `viewAny?`, `view?`, `create?`, `update?`, `delete?`, `restore?`, `forceDelete?`199200### BasePolicy Abstract Class201Protected helpers: `allow(message?)`, `deny(message?, code?)`, `denyIf(condition)`, `denyUnless(condition)`, `allowIf(condition)`202203## RBAC System (rbac.ts)204205### Role Management206- `Rbac.createRole(name, guardName?, description?): Promise<RoleRecord>`207- `Rbac.findRole(name, guardName?): Promise<RoleRecord | null>`208- `Rbac.deleteRole(name, guardName?): Promise<void>`209- `Rbac.getAllRoles(guardName?): Promise<RoleRecord[]>`210211### Permission Management212- `Rbac.createPermission(name, guardName?, description?): Promise<PermissionRecord>`213- `Rbac.findPermission(name, guardName?): Promise<PermissionRecord | null>`214- `Rbac.deletePermission(name, guardName?): Promise<void>`215- `Rbac.getAllPermissions(guardName?): Promise<PermissionRecord[]>`216217### User-Role Operations218- `Rbac.getUserRoles(user): Promise<RoleRecord[]>`219- `Rbac.assignRole(user, roleName, guardName?): Promise<void>`220- `Rbac.removeRole(user, roleName, guardName?): Promise<void>`221- `Rbac.removeAllRoles(user): Promise<void>`222- `Rbac.syncRoles(user, roleNames[], guardName?): Promise<void>` - replaces assignments for that guard and preserves roles from other guards223- `Rbac.hasRole(user, roleName, guardName?): Promise<boolean>`224- `Rbac.hasAnyRole(user, roleNames[], guardName?): Promise<boolean>`225- `Rbac.hasAllRoles(user, roleNames[], guardName?): Promise<boolean>`226227### User-Permission Operations228- `Rbac.getUserPermissions(user): Promise<PermissionRecord[]>`229- `Rbac.givePermission(user, permissionName, guardName?): Promise<void>`230- `Rbac.revokePermission(user, permissionName, guardName?): Promise<void>`231- `Rbac.revokeAllPermissions(user): Promise<void>`232- `Rbac.syncPermissions(user, permissionNames[], guardName?): Promise<void>`233- `Rbac.hasPermission(user, permissionName, guardName?): Promise<boolean>`234- `Rbac.hasAnyPermission(user, permissionNames[], guardName?): Promise<boolean>`235- `Rbac.hasAllPermissions(user, permissionNames[], guardName?): Promise<boolean>`236237### Role-Permission Operations238- `Rbac.getRolePermissions(roleId): Promise<PermissionRecord[]>`239- `Rbac.givePermissionToRole(roleName, permissionName, guardName?): Promise<void>`240- `Rbac.revokePermissionFromRole(roleName, permissionName, guardName?): Promise<void>`241- `Rbac.syncRolePermissions(roleName, permissionNames[], guardName?): Promise<void>`242243### withRbac Mixin244`withRbac(user)` — adds `hasRole()`, `hasPermission()`, `assignRole()`, `givePermission()`, etc. to any user object245246### RBAC Types247```typescript248interface RoleRecord { id, name, guard_name, description?, created_at?, updated_at? }249interface PermissionRecord { id, name, guard_name, description?, created_at?, updated_at? }250interface RbacStore { findRoleByName, createRole, deleteRole, getAllRoles, findPermissionByName, createPermission, ... }251```252253## Session Auth (session-auth.ts)254255- `SessionAuth.login(email, password): Promise<{ user, sessionId }>`256- `SessionAuth.logout(sessionId): void`257- `SessionAuth.user(sessionId): Promise<UserModel | undefined>`258- `SessionAuth.check(sessionId): boolean`259- `SessionAuth.refresh(sessionId, ttlMs?): boolean`260261Internal: in-memory Map with 10k session limit, 5-minute eviction interval, timing-safe password comparison with dummy bcrypt hash.262263## Email Verification (email-verification.ts)264265- `EmailVerification.isVerified(user): boolean`266- `EmailVerification.send(user): Promise<void>`267- `EmailVerification.verify(userId, token): Promise<EmailVerificationResult>`268- `EmailVerification.resend(user): Promise<EmailVerificationResult>`269270## Password Reset (password/reset.ts)271272```typescript273const actions = passwordResets(email)274await actions.sendEmail()275const valid = await actions.verifyToken(token)276const result = await actions.resetPassword(token, newPassword)277```278279## Registration (register.ts)280281- `register(credentials: NewUser): Promise<{ token: AuthToken }>`282283## User Helpers (user.ts)284285- `authUser(): Promise<UserModel | undefined>`286- `check(): Promise<boolean>`287- `id(): Promise<number | undefined>`288- `email(): Promise<string | undefined>`289- `name(): Promise<string | undefined>`290- `isAuthenticated(): Promise<boolean>`291- `logout(): Promise<void>`292- `refresh(): Promise<void>`293294## Passkey/WebAuthn (passkey.ts)295296- `getUserPasskeys(userId): Promise<PasskeyAttribute[]>`297- `getUserPasskey(userId, passkeyId): Promise<PasskeyAttribute | undefined>`298- `setCurrentRegistrationOptions(user, verified): Promise<void>`299300### Re-exported from @stacksjs/ts-auth301- `generateRegistrationOptions`, `generateAuthenticationOptions`302- `verifyRegistrationResponse`, `verifyAuthenticationResponse`303- `startRegistration`, `startAuthentication` (browser)304- `browserSupportsWebAuthn`, `browserSupportsWebAuthnAutofill`305- `platformAuthenticatorIsAvailable`306307## Auth Middleware (middleware.ts)308309```typescript310export const authMiddlewareHandler = {311 name: 'auth',312 handle: authMiddleware, // validates bearer token, throws 401313}314```315316## Rate Limiter (rate-limiter.ts)317318```typescript319class RateLimiter {320 static MAX_ATTEMPTS = 5321 static LOCKOUT_DURATION = 15 * 60 * 1000 // 15 minutes322 static MAX_STORE_SIZE = 10_000323 static EVICTION_INTERVAL = 5 * 60 * 1000 // 5 minutes324325 static isRateLimited(email): boolean326 static recordFailedAttempt(email): void327 static resetAttempts(email): void328 static validateAttempt(email): void // throws HttpError 429329}330```331332## Authorizable Mixin (authorizable.ts)333334```typescript335const authUser = withAuthorization(user)336await authUser.can('edit-post', post)337await authUser.cannot('delete-post', post)338await authUser.canAny(['edit', 'delete'], post)339await authUser.canAll(['edit', 'publish'], post)340await authUser.authorize('edit-post', post) // throws if denied341```342343## Configuration344345### config/auth.ts346```typescript347{348 default: 'api',349 guards: { api: { driver: 'token', provider: 'users' } },350 providers: { users: { driver: 'database', table: 'users' } },351 username: 'email', // AUTH_USERNAME_FIELD env352 password: 'password', // AUTH_PASSWORD_FIELD env353 tokenExpiry: 30, // days, AUTH_TOKEN_EXPIRY env354 tokenRotation: 7, // days, AUTH_TOKEN_ROTATION env355 defaultAbilities: ['*'],356 defaultTokenName: 'auth-token',357 passwordReset: { expire: 60, throttle: 60 }358}359```360361### config/hashing.ts362```typescript363{364 driver: 'bcrypt', // 'bcrypt' | 'argon2'365 bcrypt: { rounds: 12 },366 argon2: { memory: 65536, time: 3 }367}368```369370### config/security.ts371```typescript372{373 firewall: {374 enabled: true,375 countryCodes: [],376 ipAddresses: { allowlist: [], blocklist: [] },377 rateLimitPerMinute: 500,378 useIpReputationLists: true,379 useKnownBadInputsRuleSet: true380 }381}382```383384## Middleware Aliases (app/Middleware.ts)385386Auth-relevant aliases: `auth`, `guest`, `verified` (EnsureEmailIsVerified),387`abilities`, `can`, `role`, `permission`, `team`, `signed`, `throttle`. The388environment aliases are `env`, `env:local`, `env:development` / `env:dev`,389`env:staging`, `env:production` / `env:prod` — with a COLON, not a dot; an390earlier version of this list wrote `env.local` and those never existed. See391`stacks-middleware` for the full set and for the `!alias` and `alias:params`392forms.393394## Application Gates (app/Gates.ts)395396```typescript397import { defineGates } from '@stacksjs/auth'398399export default defineGates({400 gates: {401 'access-admin': user => user?.email?.endsWith('@stacksjs.com') ?? false,402 'edit-settings': user => !!user,403 'view-dashboard': user => !!user,404 },405 policies: {406 Post: 'PostPolicy',407 },408})409```410411Registered at boot by `initializeAuthorization()`, from412`injectGlobalAutoImports()` — the one place every entry point comes through, so413HTTP, `buddy seed`, a scheduled job and a console command all get the same414gates.415416Both halves of `policies` are checked: the key names a model the ORM exposes,417the value a policy file under `app/Policies/` or the framework defaults. An418explicit mapping WINS over the `<Model>Policy` naming convention, which is the419reason to write one.420421`Gate.define(...)` still works for a gate registered at runtime; `defineGates`422is the declarative form and the one the ability-name completions come from.423424## Default API Routes425426- `POST /login` → LoginAction (validates email + password)427- `POST /register` → RegisterAction428- `POST /auth/refresh` → RefreshTokenAction429- `POST /auth/token` → CreateTokenAction430- `GET /auth/tokens` → ListTokensAction (auth middleware)431- `DELETE /auth/tokens/{id}` → RevokeTokenAction (auth middleware)432- `GET /me` → GetMeAction (auth middleware)433- `POST /logout` → LogoutAction (auth middleware)434435## User Model Traits436437```typescript438// User model uses:439traits: {440 useAuth: { usePasskey: true },441 useUuid: true,442 useTimestamps: true,443 useSocials: ['github'],444}445```446447## Gotchas448449- Auth depends on `@stacksjs/ts-auth` for TOTP and passkey functions450- Password hashing defaults to bcrypt with 12 rounds (config/hashing.ts)451- Rate limiter uses in-memory Map, resets on server restart — not shared across workers452- Session auth also uses in-memory Map with 10k limit — for SPA cookie auth453- Token format is `tokenId|plainText` — the `|` separates the encrypted ID from the plain token454- The `parseToken()` helper splits on `|` to extract both parts455- Bearer tokens come from the `Authorization: Bearer <token>` header456- `Auth.user()` internally calls `getBearerToken()` → `parseToken()` → `getTokenFromId()` → validates hash457- RBAC has an internal cache (`userRoles`, `userPermissions`, `rolePermissions`) — call `Rbac.flushCache()` after direct DB changes458- `syncRoles()` and `syncPermissions()` are guard-scoped replacements: they preserve assignments belonging to other guards459- Gate `before` callbacks can short-circuit — return `true` to allow, `null` to continue checking460- An ability with no gate and no policy method **denies**. That is the right default, and it means a gate that was never registered is indistinguishable from one that says no — which is how `initializeAuthorization()` went unnoticed while nothing called it461- `allows()` and friends take `Ability`, which is open (`GateName | PolicyAbility | (string & {})`). A `/can/:ability` route passes an ability straight through, so narrowing it would reject correct code; the union is for completions462- `withRbac()` and `withAuthorization()` return new objects with methods mixed in463- The `RbacStore` interface must be implemented and set via `Rbac.setStore()` for RBAC to work464- Password reset tokens expire after 60 minutes by default465- Default token abilities are `['*']` — wildcard access466- Token expiry defaults to 30 days467- Session auth uses timing-safe bcrypt comparison even for failed lookups (dummy hash prevents timing attacks)468469## Build470471```bash472cd storage/framework/core/auth && bun build.ts473```