Authentication ​
AYB provides built-in email/password authentication with JWT sessions, magic link passwordless auth, OAuth support, email verification, password reset, SMS OTP auth, SMS MFA, anonymous auth with account linking, TOTP MFA with backup codes, email MFA, and authentication assurance levels (AAL).
Enable auth ​
# ayb.toml
[auth]
enabled = true
jwt_secret = "your-secret-key-at-least-32-characters-long"Or via environment variables:
AYB_AUTH_ENABLED=true
AYB_AUTH_JWT_SECRET="your-secret-key-at-least-32-characters-long"Endpoints ​
Register ​
curl -X POST http://localhost:8090/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "securepassword"}'Response (201 Created):
{
"token": "eyJhbG...",
"refreshToken": "eyJhbG...",
"user": {
"id": "uuid",
"email": "[email protected]",
"emailVerified": false,
"createdAt": "2026-02-07T..."
}
}Login ​
curl -X POST http://localhost:8090/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "securepassword"}'Returns the same response format as register.
Get current user ​
curl http://localhost:8090/api/auth/me \
-H "Authorization: Bearer eyJhbG..."Refresh token ​
curl -X POST http://localhost:8090/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "eyJhbG..."}'Logout ​
curl -X POST http://localhost:8090/api/auth/logout \
-H "Content-Type: application/json" \
-d '{"refreshToken": "eyJhbG..."}'POST /api/auth/logout revokes the provided refresh token; it does not require an Authorization header.
Session management ​
User sessions can be inspected and revoked:
GET /api/auth/sessions— list active sessions for the current userDELETE /api/auth/sessions/{id}— revoke one sessionDELETE /api/auth/sessions?all_except_current=true— revoke all other sessions for the current user
SMS OTP auth ​
SMS ​
Canonical anchor for auth error links that reference #sms.
Enable SMS auth in config:
[auth]
sms_enabled = true
sms_provider = "log" # log, twilio, plivo, telnyx, msg91, sns, vonage, webhook
sms_code_length = 6
sms_code_expiry = 300
sms_max_attempts = 3Request an OTP:
curl -X POST http://localhost:8090/api/auth/sms \
-H "Content-Type: application/json" \
-d '{"phone": "+14155552671"}'Confirm OTP:
curl -X POST http://localhost:8090/api/auth/sms/confirm \
-H "Content-Type: application/json" \
-d '{"phone": "+14155552671", "code": "123456"}'/api/auth/sms always returns 200 to avoid phone-number enumeration.
SMS MFA ​
When SMS auth is enabled, MFA routes are available:
POST /api/auth/mfa/sms/enrollPOST /api/auth/mfa/sms/enroll/confirmPOST /api/auth/mfa/sms/challengePOST /api/auth/mfa/sms/verify
Enroll:
curl -X POST http://localhost:8090/api/auth/mfa/sms/enroll \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"phone": "+14155552671"}'Magic link ​
Magic link auth lets users sign in without a password by receiving a one-time login link by email.
Requires a configured email backend (SMTP or provider).
Enable it in auth config:
[auth]
magic_link_enabled = true
magic_link_duration = 600Or via environment variables:
AYB_AUTH_MAGIC_LINK_ENABLED=true
AYB_AUTH_MAGIC_LINK_DURATION=600POST /api/auth/magic-linkstarts the flow by sending a link to the submitted email.POST /api/auth/magic-link/confirmconfirms the token from that link and returns auth tokens.
Request a magic link:
curl -X POST http://localhost:8090/api/auth/magic-link \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'Response (200 OK):
{
"message": "if valid, a login link has been sent"
}POST /api/auth/magic-link always returns 200 to reduce account-enumeration risk.
Confirm a magic link:
curl -X POST http://localhost:8090/api/auth/magic-link/confirm \
-H "Content-Type: application/json" \
-d '{"token": "magic-link-token-from-email"}'Response (200 OK, authenticated):
{
"token": "eyJhbG...",
"refreshToken": "eyJhbG...",
"user": {
"id": "uuid",
"email": "[email protected]",
"emailVerified": true,
"createdAt": "2026-02-07T..."
}
}Response (200 OK, MFA enrolled):
{
"mfa_pending": true,
"mfa_token": "eyJhbG..."
}If you confirm the link while sending an anonymous-session bearer token, AYB upgrades that existing anonymous user instead of creating a second account.
For SDK callers, use requestMagicLink and confirmMagicLink; the JavaScript SDK guide documents those wrappers in detail.
Anonymous auth ​
Anonymous ​
Canonical anchor for auth error links that reference #anonymous.
Anonymous auth lets users start using your app without signing up. They get a real user ID and session, and can later link their account to an email/password or OAuth identity.
The common progression is: start with an anonymous session, then upgrade to a credentialed account when the user is ready. You can upgrade with a magic link (POST /api/auth/magic-link) or by linking email/password credentials. After that first upgrade, you can add OAuth identity linking for provider-based sign-in.
The AYB demo apps (kanban, polls, movies — see Demos) all use this pattern: bootstrap anonymous on first visit, then surface an in-page upgrade widget so users can claim their work later.
Enable ​
[auth]
anonymous_auth_enabled = trueOr: AYB_AUTH_ANONYMOUS_AUTH_ENABLED=true
Create anonymous session ​
curl -X POST http://localhost:8090/api/auth/anonymousResponse (201 Created):
{
"token": "eyJhbG...",
"refreshToken": "eyJhbG...",
"user": {
"id": "uuid",
"email": "",
"is_anonymous": true,
"createdAt": "2026-02-24T..."
}
}No request body is needed. Rate limited to 30 requests per hour per IP.
Link email + password ​
Convert an anonymous user to a credentialed account:
curl -X POST http://localhost:8090/api/auth/link/email \
-H "Authorization: Bearer <anonymous-token>" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "securepassword"}'Response (200 OK): Returns new tokens with is_anonymous: false. The user ID is preserved — all data created during the anonymous session stays with the account.
If the email is already taken, returns 409 Conflict.
Link OAuth identity ​
curl -X POST http://localhost:8090/api/auth/link/oauth \
-H "Authorization: Bearer <anonymous-token>" \
-H "Content-Type: application/json" \
-d '{"provider": "google", "access_token": "<provider-access-token>"}'Same behavior as email linking: preserves user ID, returns new tokens, returns 409 on conflict.
Restrictions ​
- Anonymous users cannot enroll in MFA. Link the account first.
- Unlinked anonymous accounts become eligible for cleanup after 30 days by default. AYB ships the cleanup helper (
CleanupAnonymousUsers) but does not schedule that cleanup automatically.
TOTP MFA (Authenticator App) ​
TOTP ​
Canonical anchor for auth error links that reference #totp.
TOTP (Time-based One-Time Password) provides NIST-compliant multi-factor authentication using authenticator apps like Google Authenticator, Authy, or 1Password.
Enable ​
[auth]
totp_enabled = trueOr: AYB_AUTH_TOTP_ENABLED=true
Enrollment ​
Step 1 — Start enrollment:
curl -X POST http://localhost:8090/api/auth/mfa/totp/enroll \
-H "Authorization: Bearer <token>"Response (200 OK):
{
"factor_id": "uuid",
"uri": "otpauth://totp/AllYourBase:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=AllYourBase",
"secret": "JBSWY3DPEHPK3PXP"
}Display the uri as a QR code for the user to scan. The secret is shown once for manual entry.
Step 2 — Confirm enrollment:
curl -X POST http://localhost:8090/api/auth/mfa/totp/enroll/confirm \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'The user must enter a valid code from their authenticator app to confirm the enrollment is working.
MFA login flow ​
When a user with TOTP enrolled logs in, the login response changes:
{
"mfa_pending": true,
"mfa_token": "eyJhbG..."
}Use the mfa_token (not a regular token) for the challenge/verify steps:
Step 1 — Create challenge:
curl -X POST http://localhost:8090/api/auth/mfa/totp/challenge \
-H "Authorization: Bearer <mfa_token>"Response: {"challenge_id": "uuid"}
Step 2 — Verify code:
curl -X POST http://localhost:8090/api/auth/mfa/totp/verify \
-H "Authorization: Bearer <mfa_token>" \
-H "Content-Type: application/json" \
-d '{"challenge_id": "uuid", "code": "123456"}'Response (200 OK): Returns full access + refresh tokens with aal: "aal2".
TOTP parameters ​
| Parameter | Value |
|---|---|
| Algorithm | SHA-1 |
| Digits | 6 |
| Period | 30 seconds |
| Skew | ±1 window (accepts codes from adjacent 30s windows) |
These settings are compatible with all major authenticator apps.
Email MFA ​
Email MFA sends a one-time code to the user's email for step-up verification.
Security note: Email MFA does not meet NIST SP 800-63B requirements for out-of-band authentication because email does not prove device possession. Use TOTP for true multi-factor security. Email MFA is best suited as step-up verification for lower-risk operations.
Enable ​
[auth]
email_mfa_enabled = trueOr: AYB_AUTH_EMAIL_MFA_ENABLED=true
Requires a configured email backend (SMTP or provider).
Enrollment ​
# Start enrollment (sends verification code to user's email)
curl -X POST http://localhost:8090/api/auth/mfa/email/enroll \
-H "Authorization: Bearer <token>"
# Confirm enrollment with the code from email
curl -X POST http://localhost:8090/api/auth/mfa/email/enroll/confirm \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'MFA login flow ​
Same pattern as TOTP — login returns mfa_pending, then challenge + verify:
# Create challenge (sends code to email)
curl -X POST http://localhost:8090/api/auth/mfa/email/challenge \
-H "Authorization: Bearer <mfa_token>"
# Verify code
curl -X POST http://localhost:8090/api/auth/mfa/email/verify \
-H "Authorization: Bearer <mfa_token>" \
-H "Content-Type: application/json" \
-d '{"challenge_id": "uuid", "code": "123456"}'Backup codes ​
Backup codes provide recovery access when the primary MFA device is unavailable. They are generated as 10 single-use codes in xxxxx-xxxxx format.
Generate ​
Requires an AAL2 session (must have completed MFA verification):
curl -X POST http://localhost:8090/api/auth/mfa/backup/generate \
-H "Authorization: Bearer <aal2-token>"Response (200 OK):
{
"codes": [
"a1b2c-d3e4f",
"g5h6i-j7k8l",
...
]
}Store these codes securely. They are shown once and cannot be retrieved again.
Use a backup code ​
During MFA verification, use a backup code instead of TOTP/email:
curl -X POST http://localhost:8090/api/auth/mfa/backup/verify \
-H "Authorization: Bearer <mfa_token>" \
-H "Content-Type: application/json" \
-d '{"code": "a1b2c-d3e4f"}'Returns full AAL2 tokens. Each code can only be used once.
Check remaining codes ​
curl -X GET http://localhost:8090/api/auth/mfa/backup/count \
-H "Authorization: Bearer <token>"Response: {"remaining": 8}
Regenerate ​
Invalidates all existing codes and generates a new set of 10:
curl -X POST http://localhost:8090/api/auth/mfa/backup/regenerate \
-H "Authorization: Bearer <aal2-token>"Factor selection ​
When a user has multiple MFA methods enrolled, list available factors:
curl -X GET http://localhost:8090/api/auth/mfa/factors \
-H "Authorization: Bearer <mfa_token>"Response:
{
"factors": [
{"id": "uuid", "method": "totp"},
{"id": "uuid", "method": "email"},
{"id": "uuid", "method": "sms", "phone": "+1***5671"}
]
}The client can then challenge the user's preferred factor.
Authentication assurance levels (AAL) ​
AAL indicates the strength of the current session's authentication:
| Level | Meaning | When issued |
|---|---|---|
aal1 | Single-factor authentication | After password, OAuth, SMS OTP, or anonymous login |
aal2 | Multi-factor authentication | After successful MFA verification (TOTP, email, SMS, or backup code) |
Enforce AAL2 on sensitive routes ​
Use the RequireAAL2 middleware or check the aal claim in RLS policies:
-- Only allow AAL2 sessions to access sensitive data
CREATE POLICY sensitive_data_policy ON financial_records
FOR ALL
USING (current_setting('ayb.aal', true) = 'aal2');Token claims ​
Access tokens include these MFA-related claims:
| Claim | Type | Description |
|---|---|---|
aal | string | "aal1" or "aal2" |
amr | string[] | Authentication methods used, e.g. ["password", "totp"] |
mfa_pending | boolean | true when first factor passed but MFA verification is still needed |
is_anonymous | boolean | true for anonymous user sessions |
The amr (Authentication Method Reference) array records which methods were used:
| Value | Method |
|---|---|
password | Email/password login |
otp | SMS OTP login |
oauth | OAuth provider login |
anonymous | Anonymous sign-in |
totp | TOTP authenticator app |
email | Email MFA code |
sms | SMS MFA code |
backup_code | Backup code |
Refresh behavior ​
Refreshing an AAL2 session produces a new AAL2 token. Refresh never elevates AAL — only MFA verification can do that.
Security properties ​
| Method | NIST compliance | Notes |
|---|---|---|
| TOTP | Meets AAL2 (NIST SP 800-63B) | True multi-factor: proves device possession via shared secret |
| SMS MFA | "Restricted authenticator" per NIST | Vulnerable to SIM-swap and SS7 attacks; acceptable but not recommended |
| Email MFA | Does not meet NIST out-of-band requirements | Email does not prove device possession; use as step-up verification, not as sole MFA method |
| Backup codes | Recovery mechanism | Not a standing MFA method; single-use emergency access |
Internally, all MFA methods issue aal2 tokens to keep the authorization model simple. The distinction matters for compliance reporting and risk assessment, not for API behavior.
Operational limits ​
| Parameter | Default | Description |
|---|---|---|
| TOTP code window | ±30s | Accepts codes from the current and adjacent 30-second windows |
| TOTP replay protection | Per-factor | Each code's time step is recorded; reuse of the same or earlier time step is rejected |
| Email MFA code TTL | 10 minutes | Codes expire after 10 minutes |
| Email MFA attempts per code | 5 | Code is invalidated after 5 failed attempts |
| Email MFA challenges per user | 3 per 10 minutes | Prevents inbox flooding |
| Cumulative MFA lockout | 15 failures/hour | All MFA methods lock for 30 minutes after 15 failures within 1 hour |
| MFA challenge expiry (TOTP/SMS default) | 5 minutes | TOTP challenge rows default to 5 minutes; SMS code expiry is configured via auth.sms_code_expiry |
| Backup code count | 10 | Each generation/regeneration produces 10 codes |
| Anonymous sign-in rate limit | 30/hour per IP | Prevents anonymous account abuse |
| Anonymous account TTL | 30 days | Default retention used by the anonymous cleanup helper; run cleanup on your own schedule |
| Unverified TOTP enrollment TTL | 10 minutes | Default TTL used by CleanupUnverifiedTOTPEnrollments; run cleanup on your own schedule |
JWT structure ​
Access tokens are short-lived (default: 15 minutes). Refresh tokens are long-lived (default: 7 days).
Send the access token in the Authorization header:
Authorization: Bearer <token>Configure token durations:
[auth]
token_duration = 900 # 15 minutes (seconds)
refresh_token_duration = 604800 # 7 days (seconds)Password reset ​
Request reset ​
curl -X POST http://localhost:8090/api/auth/password-reset \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'Sends a reset link via the configured email backend.
Confirm reset ​
curl -X POST http://localhost:8090/api/auth/password-reset/confirm \
-H "Content-Type: application/json" \
-d '{"token": "reset-token-from-email", "password": "newpassword"}'Email verification ​
Verify email ​
curl -X POST http://localhost:8090/api/auth/verify \
-H "Content-Type: application/json" \
-d '{"token": "verification-token-from-email"}'Resend verification ​
curl -X POST http://localhost:8090/api/auth/verify/resend \
-H "Authorization: Bearer eyJhbG..."OAuth ​
AYB supports built-in OAuth providers (google, github, microsoft, apple, discord, twitter, facebook, linkedin, spotify, twitch, gitlab, bitbucket, slack, zoom, notion, figma) and custom OIDC providers.
Configure ​
[auth]
enabled = true
jwt_secret = "..."
oauth_redirect_url = "http://localhost:5173/oauth-callback"
[auth.oauth.google]
enabled = true
client_id = "your-google-client-id"
client_secret = "your-google-client-secret"
[auth.oauth.github]
enabled = true
client_id = "your-github-client-id"
client_secret = "your-github-client-secret"GitHub setup ​
- In GitHub, go to Settings → Developer settings → OAuth Apps and create a new OAuth App.
- Set Authorization callback URL to
http://localhost:8090/api/auth/oauth/github/callback. - Copy the app's client ID and client secret into
[auth.oauth.github]:client_id= GitHub OAuth App Client IDclient_secret= GitHub OAuth App Client Secret
Google setup ​
- In Google Cloud Console, configure an OAuth consent screen for your project.
- Create OAuth 2.0 client credentials for a web application.
- Add
http://localhost:8090/api/auth/oauth/google/callbackto Authorized redirect URIs. - Copy the generated client ID and client secret into
[auth.oauth.google]:client_id= Google OAuth Client IDclient_secret= Google OAuth Client Secret
Scopes and user info ​
AYB requests provider scopes from its built-in OAuth provider configuration (for example, Google requests openid email profile, and GitHub requests user:email). After token exchange, AYB maps provider user info into its normalized auth user shape used by the login/linking flow.
Flow ​
- Redirect the user to
GET /api/auth/oauth/google(orgithub) - AYB redirects to the provider's consent screen
- After approval, the provider redirects back to AYB's callback
- AYB redirects to your
oauth_redirect_urlwith tokens as hash fragments:http://localhost:5173/oauth-callback#token=eyJ...&refreshToken=eyJ...
Per-request redirect_to ​
If you need different callback destinations per request, include redirect_to on the OAuth start endpoint and configure an explicit host allowlist:
GET /api/auth/oauth/google?redirect_to=https://app.example.com/post-authAYB_AUTH_OAUTH_RETURN_TO_ALLOWLIST=app.example.com,admin.example.com,localhost,127.0.0.1redirect_to is accepted only when it passes server-side validation in validatedOAuthReturnTo (internal/auth/handler_oauth.go). Keep AYB_AUTH_OAUTH_REDIRECT_URL configured as the fallback destination when a request does not include redirect_to.
SDK helpers ​
The JavaScript SDK and Dart SDK accept redirectTo as a pass-through option on signInWithOAuth. The SDKs do not validate the value — the server is the single security owner, and the value is rejected at OAuth start unless it passes the allowlist check above.
// JavaScript / TypeScript
await ayb.auth.signInWithOAuth("google", {
redirectTo: "https://app.example.com/post-oauth",
});// Dart
await client.auth.signInWithOAuth(
'google',
redirectTo: 'https://app.example.com/post-oauth',
urlCallback: (url) async => launchUrl(Uri.parse(url)),
);The React SDK forwards the option through useAuth().signInWithOAuth(provider, options) unchanged.
Environment variables ​
AYB_AUTH_OAUTH_GOOGLE_ENABLED=true
AYB_AUTH_OAUTH_GOOGLE_CLIENT_ID=...
AYB_AUTH_OAUTH_GOOGLE_CLIENT_SECRET=...
AYB_AUTH_OAUTH_GITHUB_ENABLED=true
AYB_AUTH_OAUTH_GITHUB_CLIENT_ID=...
AYB_AUTH_OAUTH_GITHUB_CLIENT_SECRET=...
AYB_AUTH_OAUTH_REDIRECT_URL=http://localhost:5173/oauth-callbackOAuth 2.0 Provider Mode ​
In addition to consuming external OAuth providers (Google, GitHub, and other built-in providers), AYB can act as an OAuth 2.0 authorization server itself. This lets third-party applications request scoped access to your AYB instance on behalf of users.
Use OAuth provider mode when you want to:
- Let third-party apps access your AYB data with user consent
- Issue scoped, revocable access tokens to external clients
- Support the standard authorization code flow with PKCE
Enable it in config:
[auth.oauth_provider]
enabled = true
access_token_duration = 3600 # 1 hour (seconds)
refresh_token_duration = 2592000 # 30 days (seconds)
auth_code_duration = 600 # 10 minutes (seconds)Supported grant types: authorization_code (with PKCE S256, required for all clients) and client_credentials. OAuth tokens are opaque (not JWTs) and can be revoked individually.
For the full walkthrough, see the OAuth Provider Guide.
Row-Level Security (RLS) ​
When auth is enabled, AYB injects JWT claims into PostgreSQL session variables before each query. This lets you use standard Postgres RLS policies:
-- Enable RLS on a table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users can only see their own posts
CREATE POLICY posts_select ON posts
FOR SELECT
USING (author_id = current_setting('ayb.user_id')::uuid);
-- Users can only insert posts as themselves
CREATE POLICY posts_insert ON posts
FOR INSERT
WITH CHECK (author_id = current_setting('ayb.user_id')::uuid);Available session variables:
| Variable | Value |
|---|---|
ayb.user_id | The authenticated user's ID |
ayb.user_email | The authenticated user's email |
These are set per-request and scoped to the database connection for that query.