10 KiB
10 KiB
Advanced Features
Better Auth plugins extend functionality beyond basic authentication.
Two-Factor Authentication
Server Setup
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
twoFactor({
issuer: "YourAppName", // TOTP issuer name
otpOptions: {
period: 30, // OTP validity period (seconds)
digits: 6, // OTP length
}
})
]
});
Client Setup
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
twoFactorClient({
twoFactorPage: "/two-factor", // Redirect to 2FA verification page
redirect: true // Auto-redirect if 2FA required
})
]
});
Enable 2FA for User
// Enable TOTP
const { data } = await authClient.twoFactor.enable({
password: "userPassword" // Verify user identity
});
// data contains QR code URI for authenticator app
const qrCodeUri = data.totpURI;
const backupCodes = data.backupCodes; // Save these securely
Verify TOTP Code
await authClient.twoFactor.verifyTOTP({
code: "123456",
trustDevice: true // Skip 2FA on this device for 30 days
});
Disable 2FA
await authClient.twoFactor.disable({
password: "userPassword"
});
Backup Codes
// Generate new backup codes
const { data } = await authClient.twoFactor.generateBackupCodes({
password: "userPassword"
});
// Use backup code instead of TOTP
await authClient.twoFactor.verifyBackupCode({
code: "backup-code-123"
});
Passkeys (WebAuthn)
Server Setup
import { betterAuth } from "better-auth";
import { passkey } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
passkey({
rpName: "YourApp", // Relying Party name
rpID: "yourdomain.com" // Your domain
})
]
});
Client Setup
import { createAuthClient } from "better-auth/client";
import { passkeyClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [passkeyClient()]
});
Register Passkey
// User must be authenticated first
await authClient.passkey.register({
name: "My Laptop" // Optional: name for this passkey
});
Sign In with Passkey
await authClient.passkey.signIn();
List User Passkeys
const { data } = await authClient.passkey.list();
// data contains array of registered passkeys
Delete Passkey
await authClient.passkey.delete({
id: "passkey-id"
});
Magic Link
Server Setup
import { betterAuth } from "better-auth";
import { magicLink } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
magicLink({
sendMagicLink: async ({ email, url, token }) => {
await sendEmail({
to: email,
subject: "Sign in to YourApp",
html: `Click <a href="${url}">here</a> to sign in.`
});
},
expiresIn: 300, // Link expires in 5 minutes (seconds)
})
]
});
Client Setup
import { createAuthClient } from "better-auth/client";
import { magicLinkClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [magicLinkClient()]
});
Send Magic Link
await authClient.magicLink.sendMagicLink({
email: "user@example.com",
callbackURL: "/dashboard"
});
Verify Magic Link
// Called automatically when user clicks link
// Token in URL query params handled by Better Auth
await authClient.magicLink.verify({
token: "token-from-url"
});
Organizations (Multi-Tenancy)
Server Setup
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
organization({
allowUserToCreateOrganization: true,
organizationLimit: 5, // Max orgs per user
creatorRole: "owner" // Role for org creator
})
]
});
Client Setup
import { createAuthClient } from "better-auth/client";
import { organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [organizationClient()]
});
Create Organization
await authClient.organization.create({
name: "Acme Corp",
slug: "acme", // Unique slug
metadata: {
industry: "Technology"
}
});
Invite Members
await authClient.organization.inviteMember({
organizationId: "org-id",
email: "user@example.com",
role: "member", // owner, admin, member
message: "Join our team!" // Optional
});
Accept Invitation
await authClient.organization.acceptInvitation({
invitationId: "invitation-id"
});
List Organizations
const { data } = await authClient.organization.list();
// Returns user's organizations
Update Member Role
await authClient.organization.updateMemberRole({
organizationId: "org-id",
userId: "user-id",
role: "admin"
});
Remove Member
await authClient.organization.removeMember({
organizationId: "org-id",
userId: "user-id"
});
Delete Organization
await authClient.organization.delete({
organizationId: "org-id"
});
Session Management
Configure Session Expiration
export const auth = betterAuth({
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (seconds)
updateAge: 60 * 60 * 24, // Update session every 24 hours
cookieCache: {
enabled: true,
maxAge: 5 * 60 // Cache for 5 minutes
}
}
});
Server-Side Session
// Next.js
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
const session = await auth.api.getSession({
headers: await headers()
});
if (!session) {
// Not authenticated
}
Client-Side Session
// React
import { authClient } from "@/lib/auth-client";
function UserProfile() {
const { data: session, isPending, error } = authClient.useSession();
if (isPending) return <div>Loading...</div>;
if (error) return <div>Error</div>;
if (!session) return <div>Not logged in</div>;
return <div>Hello, {session.user.name}!</div>;
}
List Active Sessions
const { data: sessions } = await authClient.listSessions();
// Returns all active sessions for current user
Revoke Session
await authClient.revokeSession({
sessionId: "session-id"
});
Revoke All Sessions
await authClient.revokeAllSessions();
Rate Limiting
Server Configuration
export const auth = betterAuth({
rateLimit: {
enabled: true,
window: 60, // Time window in seconds
max: 10, // Max requests per window
storage: "memory", // "memory" or "database"
customRules: {
"/api/auth/sign-in": {
window: 60,
max: 5 // Stricter limit for sign-in
},
"/api/auth/sign-up": {
window: 3600,
max: 3 // 3 signups per hour
}
}
}
});
Custom Rate Limiter
import { betterAuth } from "better-auth";
export const auth = betterAuth({
rateLimit: {
enabled: true,
customLimiter: async ({ request, limit }) => {
// Custom rate limiting logic
const ip = request.headers.get("x-forwarded-for");
const key = `ratelimit:${ip}`;
// Use Redis, etc.
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, limit.window);
}
if (count > limit.max) {
throw new Error("Rate limit exceeded");
}
}
}
});
Anonymous Sessions
Track users before they sign up.
Server Setup
import { betterAuth } from "better-auth";
import { anonymous } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [anonymous()]
});
Client Usage
// Create anonymous session
const { data } = await authClient.signIn.anonymous();
// Convert to full account
await authClient.signUp.email({
email: "user@example.com",
password: "password123",
linkAnonymousSession: true // Link anonymous data
});
Email OTP
One-time password via email (passwordless).
Server Setup
import { betterAuth } from "better-auth";
import { emailOTP } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
emailOTP({
sendVerificationOTP: async ({ email, otp }) => {
await sendEmail({
to: email,
subject: "Your verification code",
text: `Your code is: ${otp}`
});
},
expiresIn: 300, // 5 minutes
length: 6 // OTP length
})
]
});
Client Usage
// Send OTP to email
await authClient.emailOTP.sendOTP({
email: "user@example.com"
});
// Verify OTP
await authClient.emailOTP.verifyOTP({
email: "user@example.com",
otp: "123456"
});
Phone Number Authentication
Requires phone number plugin.
Server Setup
import { betterAuth } from "better-auth";
import { phoneNumber } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
phoneNumber({
sendOTP: async ({ phoneNumber, otp }) => {
// Use Twilio, AWS SNS, etc.
await sendSMS(phoneNumber, `Your code: ${otp}`);
}
})
]
});
Client Usage
// Sign up with phone
await authClient.signUp.phoneNumber({
phoneNumber: "+1234567890",
password: "password123"
});
// Send OTP
await authClient.phoneNumber.sendOTP({
phoneNumber: "+1234567890"
});
// Verify OTP
await authClient.phoneNumber.verifyOTP({
phoneNumber: "+1234567890",
otp: "123456"
});
Best Practices
- 2FA: Offer 2FA as optional, make mandatory for admin users
- Passkeys: Implement as progressive enhancement (fallback to password)
- Magic Links: Set short expiration (5-15 minutes)
- Organizations: Implement RBAC for org permissions
- Sessions: Use short expiration for sensitive apps
- Rate Limiting: Enable in production, adjust limits based on usage
- Anonymous Sessions: Clean up old anonymous sessions periodically
- Backup Codes: Force users to save backup codes before enabling 2FA
- Multi-Device: Allow users to manage trusted devices
- Audit Logs: Track sensitive operations (role changes, 2FA changes)
Regenerate Schema After Plugins
After adding any plugin:
npx @better-auth/cli generate
npx @better-auth/cli migrate # if using Kysely
Or manually apply migrations for your ORM (Drizzle, Prisma).