Auth Package
A comprehensive authentication system providing Laravel Passport-style token management, OAuth2 support, authorization gates/policies, WebAuthn/Passkey support, and two-factor authentication.
Installation
bun add @stacksjs/auth
Basic Usage
import { Auth } from '@stacksjs/auth'
const result = await Auth.login({
email: 'user@example.com',
password: 'password123'
})
if (result) {
console.log('Token:', result.token)
console.log('User:', result.user)
}
if (await Auth.check()) {
const user = await Auth.user()
console.log('Authenticated as:', user.name)
}
await Auth.logout()
Configuration
Configure authentication in config/auth.ts:
export default {
username: 'email',
password: 'password',
defaultTokenName: 'auth-token',
defaultAbilities: ['*'],
tokenExpiry: 30 * 24 * 60 * 60 * 1000,
tokenRotation: 24,
password: {
minLength: 8,
requireUppercase: true,
requireNumbers: true,
requireSpecialChars: false,
},
rateLimit: {
maxAttempts: 5,
decayMinutes: 15,
},
}
Authentication Methods
Login
import { Auth } from '@stacksjs/auth'
const result = await Auth.login({
email: 'user@example.com',
password: 'password123'
})
const result = await Auth.login(
{ email: 'user@example.com', password: 'secret' },
{
name: 'mobile-app',
abilities: ['read', 'write'],
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
}
)
Login Using ID
import { Auth } from '@stacksjs/auth'
const result = await Auth.loginUsingId(userId, {
name: 'social-login',
abilities: ['*']
})
Validate Without Login
import { Auth } from '@stacksjs/auth'
const isValid = await Auth.validate({
email: 'user@example.com',
password: 'password123'
})
Attempt Authentication
import { Auth } from '@stacksjs/auth'
const success = await Auth.attempt({
email: 'user@example.com',
password: 'password123'
})
if (success) {
const user = await Auth.user()
}
One-Time Authentication
import { Auth } from '@stacksjs/auth'
const success = await Auth.once({
email: 'user@example.com',
password: 'password123'
})
User & Auth State
Getting Current User
import { Auth } from '@stacksjs/auth'
const user = await Auth.user()
const userId = await Auth.id()
const isAuthenticated = await Auth.check()
const isGuest = await Auth.guest()
Setting User (Testing)
import { Auth } from '@stacksjs/auth'
Auth.setUser(mockUser)
Auth.clearState()
Token Management
Creating Tokens
import { Auth } from '@stacksjs/auth'
const { accessToken, plainTextToken } = await Auth.createTokenForUser(user, {
name: 'api-token',
abilities: ['posts:read', 'posts:write'],
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
})
const token = await Auth.createToken(user, 'my-token', ['*'])
Validating Tokens
import { Auth } from '@stacksjs/auth'
const isValid = await Auth.validateToken(bearerToken)
const user = await Auth.getUserFromToken(bearerToken)
Token Abilities
import { Auth } from '@stacksjs/auth'
if (await Auth.tokenCan('posts:write')) {
}
if (await Auth.tokenCant('admin:access')) {
}
const canAll = await Auth.tokenCanAll(['read', 'write'])
const canAny = await Auth.tokenCanAny(['admin', 'moderator'])
const abilities = await Auth.tokenAbilities()
Current Access Token
import { Auth } from '@stacksjs/auth'
const token = await Auth.currentAccessToken()
console.log(token.name)
console.log(token.abilities)
console.log(token.expiresAt)
Managing User Tokens
import { Auth } from '@stacksjs/auth'
const tokens = await Auth.tokens(userId)
const token = await Auth.findToken(tokenId)
await Auth.revokeToken(tokenString)
await Auth.revokeTokenById(tokenId)
await Auth.revokeAllTokens(userId)
await Auth.revokeOtherTokens(userId)
const newToken = await Auth.rotateToken(oldToken)
Token Cleanup
import { Auth } from '@stacksjs/auth'
const expiredCount = await Auth.pruneExpiredTokens()
const revokedCount = await Auth.pruneRevokedTokens()
Middleware
Auth Middleware
import { route } from '@stacksjs/router'
route.get('/profile', 'Actions/Profile').middleware('auth')
route.group({ middleware: 'auth' }, () => {
route.get('/dashboard', 'Actions/Dashboard')
route.get('/settings', 'Actions/Settings')
})
Abilities Middleware
route.get('/posts', 'Actions/ListPosts').middleware('abilities:posts:read')
route.post('/posts', 'Actions/CreatePost').middleware('abilities:posts:write')
route.delete('/posts/:id', 'Actions/DeletePost').middleware('abilities:posts:delete')
Custom Middleware
import { Auth } from '@stacksjs/auth'
export default {
async handle(req: EnhancedRequest) {
const user = await Auth.user()
if (!user || user.role !== 'admin') {
throw { statusCode: 403, message: 'Forbidden' }
}
}
}
Authorization (Gates & Policies)
Defining Gates
import { Gate } from '@stacksjs/auth'
Gate.define('edit-post', async (user, post) => {
return user.id === post.userId
})
Gate.define('delete-post', async (user, post) => {
return user.id = post.userId || user.role = 'admin'
})
Gate.define('admin-access', async (user) => {
return user.role === 'admin'
})
Using Gates
import { Gate } from '@stacksjs/auth'
if (await Gate.allows('edit-post', post)) {
}
if (await Gate.denies('delete-post', post)) {
}
await Gate.authorize('admin-access')
Policies
import { Policy } from '@stacksjs/auth'
export default class PostPolicy extends Policy {
async view(user: User, post: Post) {
return post.published || user.id === post.userId
}
async create(user: User) {
return user.emailVerified
}
async update(user: User, post: Post) {
return user.id === post.userId
}
async delete(user: User, post: Post) {
return user.id = post.userId || user.role = 'admin'
}
}
Using Policies
import { Gate } from '@stacksjs/auth'
Gate.policy(Post, PostPolicy)
if (await Gate.allows('update', post)) {
}
route.put('/posts/:id', async (req) => {
const post = await Post.find(req.params.id)
await Gate.authorize('update', post)
})
Two-Factor Authentication (TOTP)
Setting Up 2FA
import {
generateTOTPSecret,
generateQRCodeDataURL,
verifyTOTP,
totpKeyUri
} from '@stacksjs/auth'
const secret = generateTOTPSecret()
const uri = totpKeyUri({
secret,
issuer: 'MyApp',
accountName: user.email
})
const qrCode = await generateQRCodeDataURL(uri)
await user.update({ totpSecret: secret })
Verifying 2FA Code
import { verifyTOTP } from '@stacksjs/auth'
const isValid = verifyTOTP({
token: userInputCode,
secret: user.totpSecret
})
if (isValid) {
}
2FA in Login Flow
import { Auth, verifyTOTP } from '@stacksjs/auth'
route.post('/login', async (req) => {
const { email, password, twoFactorCode } = req.all()
const valid = await Auth.attempt({ email, password })
if (!valid) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 })
}
const user = await Auth.user()
if (user.totpSecret) {
if (!twoFactorCode) {
return Response.json({ requiresTwoFactor: true }, { status: 200 })
}
const validCode = verifyTOTP({
token: twoFactorCode,
secret: user.totpSecret
})
if (!validCode) {
return Response.json({ error: 'Invalid 2FA code' }, { status: 401 })
}
}
const { plainTextToken } = await Auth.createTokenForUser(user)
return Response.json({ token: plainTextToken })
})
WebAuthn / Passkeys
Registration
import {
generateRegistrationOptions,
verifyRegistrationResponse
} from '@stacksjs/auth'
route.get('/webauthn/register/options', async (req) => {
const user = await req.user()
const options = await generateRegistrationOptions({
rpName: 'My App',
rpID: 'myapp.com',
userID: user.id.toString(),
userName: user.email,
userDisplayName: user.name,
attestationType: 'none',
})
await cache.set(`webauthn:challenge:${user.id}`, options.challenge, 300)
return Response.json(options)
})
route.post('/webauthn/register', async (req) => {
const user = await req.user()
const response = req.all()
const challenge = await cache.get(`webauthn:challenge:${user.id}`)
const verification = await verifyRegistrationResponse({
response,
expectedChallenge: challenge,
expectedOrigin: 'https://myapp.com',
expectedRPID: 'myapp.com',
})
if (verification.verified) {
await user.passkeys().create({
credentialId: verification.registrationInfo.credentialID,
publicKey: verification.registrationInfo.credentialPublicKey,
counter: verification.registrationInfo.counter,
})
}
})
Authentication
import {
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@stacksjs/auth'
route.post('/webauthn/login/options', async (req) => {
const { email } = req.all()
const user = await User.where('email', '=', email).first()
const passkeys = await user.passkeys().get()
const options = await generateAuthenticationOptions({
rpID: 'myapp.com',
allowCredentials: passkeys.map(p => ({
id: p.credentialId,
type: 'public-key',
})),
})
await cache.set(`webauthn:auth:${user.id}`, options.challenge, 300)
return Response.json(options)
})
route.post('/webauthn/login', async (req) => {
const { email, response } = req.all()
const user = await User.where('email', '=', email).first()
const challenge = await cache.get(`webauthn:auth:${user.id}`)
const passkey = await user.passkeys()
.where('credentialId', '=', response.id)
.first()
const verification = await verifyAuthenticationResponse({
response,
expectedChallenge: challenge,
expectedOrigin: 'https://myapp.com',
expectedRPID: 'myapp.com',
authenticator: {
credentialID: passkey.credentialId,
credentialPublicKey: passkey.publicKey,
counter: passkey.counter,
},
})
if (verification.verified) {
await passkey.update({ counter: verification.authenticationInfo.newCounter })
const { plainTextToken } = await Auth.createTokenForUser(user)
return Response.json({ token: plainTextToken })
}
})
Password Reset
import { sendPasswordReset, resetPassword } from '@stacksjs/auth'
route.post('/forgot-password', async (req) => {
const { email } = req.all()
await sendPasswordReset(email, {
resetUrl: 'https://myapp.com/reset-password',
expiresIn: 60 * 60 * 1000,
})
return Response.json({ message: 'Reset email sent' })
})
route.post('/reset-password', async (req) => {
const { token, password } = req.all()
const result = await resetPassword(token, password)
if (result.success) {
return Response.json({ message: 'Password reset successfully' })
}
return Response.json({ error: result.error }, { status: 400 })
})
Rate Limiting
import { RateLimiter } from '@stacksjs/auth'
route.post('/login', async (req) => {
const { email } = req.all()
if (!RateLimiter.canAttempt(email)) {
const waitTime = RateLimiter.getWaitTime(email)
return Response.json({
error: 'Too many attempts',
retryAfter: waitTime
}, { status: 429 })
}
const success = await Auth.attempt(req.all())
if (!success) {
RateLimiter.recordFailedAttempt(email)
return Response.json({ error: 'Invalid credentials' }, { status: 401 })
}
RateLimiter.resetAttempts(email)
})
API Reference
Auth Class Methods
| Method | Description |
attempt(credentials) | Authenticate and store user |
validate(credentials) | Validate without login |
login(credentials, options?) | Login and get token |
loginUsingId(id, options?) | Login by user ID |
logout() | Logout current user |
once(credentials) | One-time authentication |
user() | Get authenticated user |
id() | Get user ID |
check() | Check if authenticated |
guest() | Check if guest |
setUser(user) | Set user (testing) |
createTokenForUser(user, options?) | Create token |
validateToken(token) | Validate token |
tokenCan(ability) | Check token ability |
tokenCant(ability) | Check token lacks ability |
tokens(userId?) | Get user's tokens |
revokeToken(token) | Revoke token |
revokeAllTokens(userId?) | Revoke all tokens |
Gate Methods
| Method | Description |
define(name, callback) | Define gate |
allows(ability, ...args) | Check if allowed |
denies(ability, ...args) | Check if denied |
authorize(ability, ...args) | Authorize or throw |
policy(model, policy) | Register policy |
TOTP Functions
| Function | Description |
generateTOTPSecret() | Generate secret |
verifyTOTP(options) | Verify code |
totpKeyUri(options) | Generate URI |
generateQRCodeDataURL(uri) | Generate QR code |
generateQRCodeSVG(uri) | Generate SVG QR |