Authentication System¶
Mycel includes a complete, enterprise-grade authentication system that can be configured entirely through HCL. No code required.
Overview¶
The auth system provides:
- Core Authentication: JWT tokens, sessions, password hashing
- Security Features: Brute force protection, rate limiting, audit logging
- Multi-Factor Authentication: TOTP, WebAuthn/Passkeys, recovery codes
- SSO & Social Login: Google, GitHub, Apple, OIDC (Okta, Azure AD, Auth0)
- Account Linking: Automatic or manual linking of social accounts
Quick Start¶
auth {
preset = "standard" # strict, standard, relaxed, development
jwt {
secret = env("JWT_SECRET")
}
users {
connector = "postgres"
table = "users"
}
}
Configuration Reference¶
Presets¶
| Preset | Access Token | Refresh Token | MFA | Password Policy |
|---|---|---|---|---|
strict |
15m | 1d | Required | Strong (12+ chars, all types) |
standard |
1h | 7d | Optional | Moderate (8+ chars) |
relaxed |
24h | 30d | Off | Basic (6+ chars) |
development |
24h | 90d | Off | None |
JWT Configuration¶
jwt {
# Secret key (required for HMAC)
secret = env("JWT_SECRET")
# Or use RSA keys
# private_key = file("./keys/private.pem")
# public_key = file("./keys/public.pem")
# Algorithm: HS256, HS384, HS512, RS256, RS384, RS512
algorithm = "HS256"
# Token lifetimes
access_lifetime = "1h"
refresh_lifetime = "7d"
# Token claims
issuer = "my-service"
audience = ["my-app"]
# Enable refresh token rotation
rotation = true
}
Password Policy¶
password {
min_length = 8
max_length = 128
require_upper = true
require_lower = true
require_number = true
require_special = false
# Password history (prevent reuse)
history = 5
# Breach check (haveibeenpwned)
breach_check = true
}
Security Features¶
security {
brute_force {
enabled = true
max_attempts = 5
window = "15m"
lockout_time = "30m"
track_by = "ip+user" # "ip", "user", "ip+user"
}
replay_protection {
enabled = true
window = "5m"
}
impossible_travel {
enabled = true
max_speed_kmh = 1000
alert_only = false # true = alert but allow
}
device_binding {
enabled = true
fields = ["user_agent", "screen_resolution"]
}
}
Session Management¶
sessions {
max_active = 5 # Max concurrent sessions
idle_timeout = "1h" # Logout after inactivity
absolute_timeout = "24h" # Force logout after this time
allow_list = true # Enable session listing
allow_revoke = true # Enable session revocation
on_max_reached = "revoke_oldest" # "deny", "revoke_oldest", "revoke_all"
}
Multi-Factor Authentication¶
mfa {
required = "optional" # "required", "optional", "off"
methods = ["totp", "webauthn"]
# TOTP Configuration
totp {
issuer = "My App"
digits = 6
period = 30 # seconds
}
# WebAuthn Configuration
webauthn {
rp_id = "myapp.com"
rp_name = "My Application"
rp_origins = ["https://myapp.com"]
attestation = "none" # "none", "indirect", "direct"
user_verification = "preferred"
}
# Recovery codes
recovery_codes {
enabled = true
count = 10
length = 8
}
}
SSO Configuration¶
sso {
linking {
enabled = true
match_by = "email" # "email", "none"
require_verification = true # Require verified email
on_match = "link" # "link", "prompt", "reject"
}
}
Social Login Providers¶
social {
google {
client_id = env("GOOGLE_CLIENT_ID")
client_secret = env("GOOGLE_CLIENT_SECRET")
scopes = ["openid", "email", "profile"]
}
github {
client_id = env("GITHUB_CLIENT_ID")
client_secret = env("GITHUB_CLIENT_SECRET")
scopes = ["read:user", "user:email"]
}
apple {
client_id = env("APPLE_CLIENT_ID")
team_id = env("APPLE_TEAM_ID")
key_id = env("APPLE_KEY_ID")
private_key = env("APPLE_PRIVATE_KEY")
}
}
Enterprise OIDC¶
# Okta
oidc "okta" {
issuer = "https://your-org.okta.com"
client_id = env("OKTA_CLIENT_ID")
client_secret = env("OKTA_CLIENT_SECRET")
scopes = ["openid", "email", "profile", "groups"]
# Custom claim mappings
claims {
groups = "groups"
role = "role"
}
}
# Azure AD
oidc "azure" {
issuer = "https://login.microsoftonline.com/${TENANT_ID}/v2.0"
client_id = env("AZURE_CLIENT_ID")
client_secret = env("AZURE_CLIENT_SECRET")
scopes = ["openid", "email", "profile"]
}
# Auth0
oidc "auth0" {
issuer = "https://your-tenant.auth0.com/"
client_id = env("AUTH0_CLIENT_ID")
client_secret = env("AUTH0_CLIENT_SECRET")
scopes = ["openid", "email", "profile"]
}
External Identity Providers¶
A provider block validates an incoming credential (typically an API key or
opaque bearer token) against an external HTTP endpoint at request time, rather
than against local JWTs or a static list. Use it for dynamic API keys, an
upstream introspection service, or any "is this token valid, and who is it?"
backend.
auth {
secret = env("AUTH_SECRET")
provider "api_keys" {
type = "http" # only "http" is supported
validate = env("KEYS_VALIDATE_URL") # URL; supports {token}
# Headers sent to the validate URL. {token} is replaced with the credential.
request = {
Authorization = "Bearer {token}"
}
# Response mapping. Each value is a CEL expression evaluated over:
# status — the HTTP status code (int)
# body — the parsed JSON response (object)
response {
success = "status == 200 && body.active == true" # required; must be truthy
user_id = "body.user_id"
email = "body.email"
roles = "body.roles" # list<string>, or a comma-separated string
token = "body.session_id" # optional; stored on the claims
}
}
}
Behavior
- Order: local JWT validation runs first. Providers are tried only when the
credential is not a valid JWT, in declaration order; the first whose
successexpression is truthy wins. - Auth context: the full response
bodyis exposed to flows asauth.claims.*(so any field is reachable, not just the mapped ones), and the mappeduser_idis available asauth.user_id. - Provider unavailable: a timeout or transport error is treated as a validation failure (the request is rejected), never a 5xx from your service.
- Fail-fast: an unsupported
type, a missingvalidate/success, or an invalid CEL expression fails at startup, not silently at runtime.
Not yet implemented: response caching (every request hits the provider) and
sync_to (parsed, but setting it only logs a warning today).
See the dynamic-api-key example for a complete setup.
User Storage¶
users {
connector = "postgres"
table = "users"
# Field mappings (if different from defaults)
fields {
id = "id"
email = "email"
password_hash = "password_hash"
mfa_enabled = "mfa_enabled"
created_at = "created_at"
updated_at = "updated_at"
}
}
Token Storage¶
# In-memory (default, not for production)
storage {
driver = "memory"
}
# Redis (recommended for production)
storage {
driver = "redis"
url = env("REDIS_URL", "redis://localhost:6379")
password = env("REDIS_PASSWORD", "")
db = 0
}
Audit Logging¶
audit {
enabled = true
connector = "postgres"
table = "auth_audit_log"
events = [
"login",
"logout",
"failed_login",
"register",
"password_change",
"mfa_enabled",
"mfa_disabled",
"sso_login",
"account_linked",
"account_unlinked"
]
}
Custom Endpoints¶
endpoints {
prefix = "/auth"
# Standard auth
login { path = "/login", method = "POST", enabled = true }
logout { path = "/logout", method = "POST", enabled = true }
register { path = "/register", method = "POST", enabled = true }
refresh { path = "/refresh", method = "POST", enabled = true }
me { path = "/me", method = "GET", enabled = true }
# Sessions
sessions_list { path = "/sessions", method = "GET", enabled = true }
sessions_revoke { path = "/sessions/:id", method = "DELETE", enabled = true }
# Password
password_change { path = "/change-password", method = "POST", enabled = true }
password_reset { path = "/reset-password", method = "POST", enabled = false }
# MFA
mfa_setup { path = "/mfa/setup", method = "POST", enabled = true }
mfa_verify { path = "/mfa/verify", method = "POST", enabled = true }
mfa_disable { path = "/mfa/disable", method = "POST", enabled = true }
# SSO
sso_start { path = "/sso/:provider", method = "GET", enabled = true }
sso_callback { path = "/callback/:provider", method = "GET", enabled = true }
# Account linking
link_account { path = "/link/:provider", method = "POST", enabled = true }
unlink_account { path = "/unlink/:provider", method = "DELETE", enabled = true }
linked_list { path = "/linked-accounts", method = "GET", enabled = true }
}
Database Schema¶
PostgreSQL / MySQL¶
-- Users table
CREATE TABLE users (
id VARCHAR(64) PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255),
mfa_enabled BOOLEAN DEFAULT FALSE,
mfa_secret VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
metadata JSONB
);
-- Password history (for reuse prevention)
CREATE TABLE password_history (
id SERIAL PRIMARY KEY,
user_id VARCHAR(64) NOT NULL REFERENCES users(id),
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Linked accounts (SSO/Social)
CREATE TABLE linked_accounts (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL REFERENCES users(id),
provider VARCHAR(50) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
email VARCHAR(255),
name VARCHAR(255),
picture TEXT,
access_token TEXT,
refresh_token TEXT,
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
metadata JSONB,
UNIQUE(provider, provider_id)
);
-- MFA recovery codes
CREATE TABLE mfa_recovery_codes (
id SERIAL PRIMARY KEY,
user_id VARCHAR(64) NOT NULL REFERENCES users(id),
code_hash VARCHAR(255) NOT NULL,
used BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
-- WebAuthn credentials
CREATE TABLE webauthn_credentials (
id VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL REFERENCES users(id),
name VARCHAR(255),
public_key BYTEA NOT NULL,
attestation_type VARCHAR(50),
authenticator_aaguid BYTEA,
sign_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
-- Audit log
CREATE TABLE auth_audit_log (
id SERIAL PRIMARY KEY,
event VARCHAR(50) NOT NULL,
user_id VARCHAR(64),
email VARCHAR(255),
ip VARCHAR(45),
user_agent TEXT,
success BOOLEAN,
error_reason TEXT,
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_password_history_user ON password_history(user_id);
CREATE INDEX idx_linked_accounts_user ON linked_accounts(user_id);
CREATE INDEX idx_linked_accounts_provider ON linked_accounts(provider, provider_id);
CREATE INDEX idx_recovery_codes_user ON mfa_recovery_codes(user_id);
CREATE INDEX idx_webauthn_user ON webauthn_credentials(user_id);
CREATE INDEX idx_audit_user ON auth_audit_log(user_id);
CREATE INDEX idx_audit_event ON auth_audit_log(event);
CREATE INDEX idx_audit_created ON auth_audit_log(created_at);
API Reference¶
Standard Auth¶
| Endpoint | Method | Description |
|---|---|---|
/auth/register |
POST | Register new user |
/auth/login |
POST | Login with email/password |
/auth/logout |
POST | Logout (invalidate session) |
/auth/refresh |
POST | Refresh access token |
/auth/me |
GET | Get current user info |
/auth/change-password |
POST | Change password |
Sessions¶
| Endpoint | Method | Description |
|---|---|---|
/auth/sessions |
GET | List active sessions |
/auth/sessions/:id |
DELETE | Revoke specific session |
MFA¶
| Endpoint | Method | Description |
|---|---|---|
/auth/mfa/setup |
POST | Begin MFA setup (returns QR code) |
/auth/mfa/verify |
POST | Verify TOTP code and enable MFA |
/auth/mfa/disable |
POST | Disable MFA |
SSO / Social Login¶
| Endpoint | Method | Description |
|---|---|---|
/auth/sso/:provider |
GET | Start SSO flow |
/auth/callback/:provider |
GET/POST | OAuth callback |
/auth/link/:provider |
POST | Link social account |
/auth/unlink/:provider |
DELETE | Unlink social account |
/auth/linked-accounts |
GET | List linked accounts |
Security Considerations¶
Production Checklist¶
- [ ] Use strong JWT secret (32+ random bytes)
- [ ] Enable HTTPS
- [ ] Use Redis for token storage (not memory)
- [ ] Enable brute force protection
- [ ] Set appropriate token lifetimes
- [ ] Enable audit logging
- [ ] Configure CORS properly
- [ ] Use
strictorstandardpreset
Best Practices¶
- Never log tokens or passwords - Mycel redacts these automatically
- Rotate secrets periodically - Use key rotation features
- Monitor audit logs - Set up alerts for suspicious activity
- Use MFA - Require or encourage MFA for sensitive operations
- Limit sessions - Prevent unlimited concurrent sessions
Examples¶
See examples/auth for a complete working example.