- Overview
- Goals & Motivation
- Key Concepts
- Architecture Diagrams
- Quickstart
- Installation & Build
- Configuration
- HTTP API Reference & Usage
- OpenAPI Specification & Client Example
- Email Queue Worker
- Testing
- Docker & Orchestration
- Shell Helpers & Scripts
- Example File Layout After Run
- Troubleshooting & Common Issues
- Security Considerations
- Extension Points / Developer Notes
- Contributing
- Glossary
- License
Passwordless Auth is a self-hosted identity and authentication server designed for small teams and internal tools. It provides modern passwordless login mechanismsβmagic links delivered via email, WebAuthn (passkeys), and TOTP fallbackβwhile issuing JWTs for session management. No third-party identity provider is required; all state is under your control, with revocable short-lived sessions and refresh tokens.
- Eliminate shared passwords: Reduce risk by avoiding traditional passwords in favor of magic links, passkeys, and one-time TOTP codes.
- Self-hosted control: No reliance on external SaaS for auth; all data, tokens, and credentials live under your infrastructure.
- Modern UX: Support for WebAuthn (biometric or hardware keys), magic link convenience, and TOTP as backup.
- Secure session management: JWT access + refresh tokens with revocation and lifetime policies.
- Reliability: Email queueing with retries to ensure magic links arrive even under transient SMTP issues.
- Rust: Implementation language emphasizing safety and performance.
- Axum: HTTP server framework powering the REST API.
- SQLite: Embedded persistence for users, tokens, pending challenges, and queue; light and portable.
- OpenAPI: Machine-readable specification of the API for clients and tooling.
- Docker / Docker Compose: Containerized deployment for consistency.
- Magic Link: Email-based one-time login link with expiration and single-use semantics.
- TOTP: Time-based one-time passwords for fallback or second factor.
- WebAuthn: Strong cryptographic passwordless authentication using platform or roaming authenticators.
- JWTs: Access and refresh tokens encapsulate session state; refresh tokens are stored and revocable.
- HMAC & signed tokens: JWTs are signed (HS256) with a secret to prevent forgery.
- Challenge/Response: WebAuthn uses standard challenge assertion to prove possession of credentials.
- One-time link protection: Magic link tokens are marked used and expire.
- TOTP windowing: Small clock skew tolerance while preventing reuse.
- Refresh token revocation: Stored server-side to allow invalidating sessions.
flowchart TD
%% Entry
User["User / Client"]
Choose["Select auth method"]
User --> Choose
style User fill:#e2f0ff,stroke:#005fa3,stroke-width:2px,color:#000
style Choose fill:#fff3d9,stroke:#d4a017,stroke-width:1px,color:#000
%% Magic Link Flow
subgraph MagicLink["Magic Link Flow"]
direction TB
MLRequest["POST /request/magic"]
EnsureUserML["Ensure user record"]
CreateToken["Generate one-time token"]
SaveMLToken["Persist magic link token"]
EnqueueEmail["Enqueue email job"]
EmailQueueDB["Email queue (SQLite)"]
EmailWorker["Email worker picks job"]
SendEmail["Send magic link via SMTP"]
EmailDelivered["User receives link"]
ClickLink["User clicks link"]
VerifyToken["Validate token & mark used"]
IssueJWT_ML["Issue access + refresh JWTs"]
end
Choose --> MLRequest
MLRequest --> EnsureUserML
EnsureUserML --> CreateToken
CreateToken --> SaveMLToken
SaveMLToken --> EnqueueEmail
EnqueueEmail --> EmailQueueDB
EmailQueueDB --> EmailWorker
EmailWorker --> SendEmail
SendEmail --> EmailDelivered
EmailDelivered --> ClickLink
ClickLink --> VerifyToken
VerifyToken --> IssueJWT_ML
IssueJWT_ML --> User
%% TOTP Flow
subgraph TOTP["TOTP Flow"]
direction TB
TOTPStart["Enroll / Verify TOTP"]
EnsureUserTOTP["Ensure user record"]
StoreTOTP["Store / lookup TOTP secret"]
VerifyTOTP["Validate TOTP code"]
IssueJWT_TOTP["Issue access + refresh JWTs"]
end
Choose --> TOTPStart
TOTPStart --> EnsureUserTOTP
EnsureUserTOTP --> StoreTOTP
StoreTOTP --> VerifyTOTP
VerifyTOTP --> IssueJWT_TOTP
IssueJWT_TOTP --> User
%% WebAuthn Flow
subgraph WebAuthn["WebAuthn Flow"]
direction TB
WebRegOptions["POST /webauthn/register/options"]
CompleteReg["POST /webauthn/register/complete"]
SaveWebCred["Persist WebAuthn credential"]
WebLoginOptions["POST /webauthn/login/options"]
CompleteLogin["POST /webauthn/login/complete"]
IssueJWT_WebAuthn["Issue access + refresh JWTs"]
end
Choose --> WebRegOptions
WebRegOptions --> CompleteReg
CompleteReg --> SaveWebCred
SaveWebCred --> WebLoginOptions
WebLoginOptions --> CompleteLogin
CompleteLogin --> IssueJWT_WebAuthn
IssueJWT_WebAuthn --> User
%% Token Management
subgraph Tokens["Refresh / Revocation"]
direction TB
RefreshReq["POST /token/refresh"]
ValidateRefresh["Validate refresh token"]
IssueJWT_Refresh["Issue new JWTs"]
RevokeReq["Revoke refresh token"]
MarkRevoked["Mark token revoked"]
end
User --> RefreshReq
RefreshReq --> ValidateRefresh
ValidateRefresh --> IssueJWT_Refresh
IssueJWT_Refresh --> User
User --> RevokeReq
RevokeReq --> MarkRevoked
%% Persistence
subgraph DB["SQLite Persistence"]
direction TB
Users["Users"]
MagicLinkTokens["Magic Link Tokens"]
TOTPSecrets["TOTP Secrets"]
WebAuthnCredentials["WebAuthn Credentials"]
RefreshTokens["Refresh Tokens"]
EmailJobs["Email Queue"]
end
EnsureUserML --> Users
SaveMLToken --> MagicLinkTokens
TOTPStart --> Users
EnsureUserTOTP --> Users
StoreTOTP --> TOTPSecrets
CompleteReg --> Users
SaveWebCred --> WebAuthnCredentials
ValidateRefresh --> RefreshTokens
IssueJWT_Refresh --> RefreshTokens
EnqueueEmail --> EmailJobs
EmailWorker --> EmailJobs
%% SMTP
SendEmail --> SMTP["SMTP Provider"]
SMTP --> EmailDelivered["User receives link"]
style SMTP fill:#d4f7d4,stroke:#2d8f2d,color:#000
style EmailDelivered color:#000
%% Styling
style User fill:#e2f0ff,stroke:#005fa3,stroke-width:2px
style Choose fill:#fff3d9,stroke:#d4a017,stroke-width:1px
style MagicLink fill:#fff8e1,stroke:#d4a017
style TOTP fill:#f0e6ff,stroke:#9a6bcb
style WebAuthn fill:#e6f7ff,stroke:#4091c4
style Tokens fill:#d0f7d0,stroke:#2d8f2d
style DB fill:#f5f5f5,stroke:#888888
style SMTP fill:#d4f7d4,stroke:#2d8f2d
%% Class diagram for core components
classDiagram
class AuthServer {
+Config config
+start()
}
class MagicLinkService {
+request_link(email)
+verify(token)
}
class TOTPService {
+enroll(email)
+verify(email, code)
}
class WebAuthnService {
+begin_registration(email)
+complete_registration(pending_id, resp)
+begin_login(email)
+complete_login(pending_id, resp)
}
class JWTService {
+create_access(user_id)
+create_refresh(user_id)
+verify(token)
}
class EmailQueueWorker {
+enqueue(to, subject, text, html)
+process_due()
}
class SMTPProvider {
+send(to, subject, body)
}
class SQLiteDB {
+execute(query)
+query(query)
}
class User {
-id
-email
-totp_secret
-webauthn_credentials
}
class MagicLinkToken {
-token
-expires_at
-used
}
class RefreshToken {
-token
-expires_at
-revoked
}
class WebAuthnCredential {
-credential_id
-public_key
-sign_count
}
AuthServer --> MagicLinkService
AuthServer --> TOTPService
AuthServer --> WebAuthnService
AuthServer --> JWTService
AuthServer --> SQLiteDB
MagicLinkService --> SQLiteDB
MagicLinkService --> EmailQueueWorker
EmailQueueWorker --> SMTPProvider
TOTPService --> SQLiteDB
WebAuthnService --> SQLiteDB
JWTService --> SQLiteDB
SQLiteDB --> User
SQLiteDB --> MagicLinkToken
SQLiteDB --> RefreshToken
SQLiteDB --> WebAuthnCredential
WebAuthnService --> WebAuthnCredential
JWTService --> RefreshToken
# Build everything
make build
# Start server
./target/release/passwordless-auth
# Request a magic link
curl -X POST http://localhost:3000/request/magic \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com"}'
# Simulate clicking the link (token retrieved from DB or email)
curl "http://localhost:3000/verify/magic?token=<token>"
# Refresh
curl -X POST http://localhost:3000/token/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token":"<refresh_jwt>"}'
- Rust toolchain (recommended via
rustup
) cargo
make
- Optional: Docker & Docker Compose for containerized deployment
git clone <repo-url> passwordless-auth
cd passwordless-auth
make build
This produces the binaries:
target/release/passwordless-auth
β main auth servertarget/release/email-worker
β background email queue worker
make test
Configuration is read from config.toml
in the project root. Example:
# JWT
jwt_secret = "supersecretandlongenoughforhs256"
access_token_expiry_seconds = 900
refresh_token_expiry_seconds = 604800
# Magic link
magic_link_expiry_seconds = 600
magic_link_base_url = "http://localhost:3000/verify/magic"
# SMTP
smtp_host = "smtp.example.com"
smtp_port = 587
smtp_username = "user@example.com"
smtp_password = "password"
email_from = "no-reply@example.com"
# WebAuthn
webauthn_rp_id = "localhost"
webauthn_origin = "http://localhost:3000"
webauthn_rp_name = "Passwordless Auth Server"
# Storage
database_path = "auth.db"
Copy config.toml
and adjust values to match your environment (especially jwt_secret
and SMTP credentials).
All endpoints are JSON over HTTP. Default server listening port is 3000
.
POST /request/magic
Request body:
{
"email": "alice@example.com"
}
Response: 200 OK
(always succeeds silently to avoid enumeration). Magic link sent to email.
GET /verify/magic?token=<token>
Returns:
{
"access_token": "...",
"refresh_token": "..."
}
Tokens are JWTs; access token is short-lived, refresh token can be used to obtain new access tokens.
POST /totp/enroll
Body:
{
"email": "alice@example.com"
}
Response includes the secret and otpauth://
URL:
{
"secret": "...",
"otpauth_url": "otpauth://totp/PasswordlessAuth:alice@example.com?secret=..."
}
Load into authenticator app (e.g., Google Authenticator).
POST /totp/verify
{
"email": "alice@example.com",
"code": "123456"
}
Returns access and refresh tokens if the provided TOTP code is valid.
POST /webauthn/register/options
Body:
{ "email": "alice@example.com" }
Returns WebAuthn creation options (challenge, rp, user, etc.) for the client.
POST /webauthn/register/complete
{
"pending_id": "<from options response>",
"response": { /* client attestation object */ }
}
Creates a credential tied to the user.
POST /webauthn/login/options
Body:
{ "email": "alice@example.com" }
Returns assertion options.
POST /webauthn/login/complete
{
"pending_id": "...",
"response": { /* client assertion */ }
}
On success, returns JWTs.
POST /token/refresh
Body:
{
"refresh_token": "<refresh_jwt>"
}
Returns new access and refresh tokens.
An OpenAPI spec (openapi.yaml
) is provided at the repo root describing all endpoints, request/response schemas, and authentication semantics. You can generate clients:
# Example using openapi-generator-cli (Java needed)
openapi-generator-cli generate -i openapi.yaml -g javascript -o client/js
const fetch = (...args) => import('node-fetch').then(({default: f}) => f(...args));
async function requestMagic(email) {
await fetch('http://localhost:3000/request/magic', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({email})
});
}
async function verifyMagic(token) {
const res = await fetch(`http://localhost:3000/verify/magic?token=${encodeURIComponent(token)}`);
return res.json();
}
To improve reliability of magic link delivery, emails are enqueued in email_queue
and retried with exponential backoff. The email-worker
binary continuously:
- Fetches due pending emails
- Marks them as sending
- Attempts delivery via SMTP
- On failure, updates
next_try_at
with backoff and records error - On success, marks as sent
This makes the system resilient to transient SMTP issues.
Covers:
- JWT creation/verification
- Magic link lifecycle (generation, usage, expiration)
- TOTP generation and validation
- Refresh token session validation and revocation
Run:
make test
Or directly:
cargo test
Simulate full flows:
- Magic link end-to-end (request, retrieve token from DB, verify)
- Refresh token issuance and swap
- TOTP enrollment + verification
- WebAuthn option retrieval and error handling
Tests are under tests/
(integration_test.rs
, unit_tests.rs
) and spawn the server in a temporary environment to avoid state collisions.
make docker-build
docker compose up --build
Services:
auth
: Primary authentication serveremail-worker
: Background worker processing email queue
docker run --rm -v "$(pwd)/config.toml":/app/config.toml -v "$(pwd)/migrations":/app/migrations passwordless-auth
Mount host volumes to retain:
auth.db
(SQLite)config.toml
(override or secrets management)
Provided helpers:
scripts/start.sh
β builds and launches Docker composition.scripts/test.sh
β runs all tests.scripts/request_magic.sh
β convenience wrapper to request a magic link.scripts/verify_magic.sh
β verify a magic link token.
Make them executable:
chmod +x scripts/*.sh
.
βββ config.toml
βββ auth.db # SQLite database
βββ migrations/
β βββ init.sql # schema
βββ target/ # compiled Rust binaries
β βββ release/
β βββ passwordless-auth # main server
β βββ email-worker # worker
βββ openapi.yaml # API spec
βββ scripts/
β βββ start.sh
β βββ test.sh
β βββ request_magic.sh
β βββ verify_magic.sh
βββ tests/
β βββ integration_test.rs
β βββ unit_tests.rs
βββ Dockerfile
βββ docker-compose.yml
βββ README.md
Problem | Likely Cause | Remedy |
---|---|---|
Magic link email not arriving | SMTP misconfiguration or transient failure | Check email_queue , run email-worker , inspect SMTP logs |
Token expired | Link/token lifetime passed | Request new magic link or refresh appropriately |
JWT verification fails | Wrong secret or malformed token | Confirm jwt_secret matches between issuance and verification |
WebAuthn registration/login errors | Origin/RP mismatch or stale challenge | Ensure webauthn_origin /rp_id align with client, retry flow |
Refresh token invalid | Revoked or expired | Re-authenticate via magic link / TOTP / WebAuthn |
Database locked | Concurrent access on SQLite | Use WAL mode (enabled), avoid long transactions |
- Secret management:
jwt_secret
must be long, random, and protected; rotate periodically. - Token expiry: Access tokens are short-lived; refresh tokens stored and revocable to mitigate theft.
- Magic link replay: Links are single-use and expire; used tokens are marked.
- WebAuthn integrity: Verifies sign count and challenge to prevent replay.
- TOTP skew: Limited tolerance; ensure server clock is accurate (NTP).
- Email queue abuse: Rate limit magic link requests per email to avoid spam or enumeration.
- Transport security: Deploy behind TLS (use reverse proxy like Caddy/Nginx or terminate TLS externally).
- Auditability: Extend to log issuance and failed attempts for anomaly detection.
- Backend swap: Replace SQLite with Postgres or remote store for larger teams.
- Session introspection: Add endpoint to list/kill active refresh tokens per user.
- Rate limiting: Incorporate per-IP/email throttling (e.g., via middleware).
- Email templates: Upgrade to templating engine for rich HTML.
- Metrics: Export Prometheus metrics for auth success/failure, queue depth, token issuance.
- UI dashboard: Simple internal dashboard to view users, pending WebAuthn challenges, and revoke tokens.
- Federation: Use signed JWTs with external trust anchors for cross-service identity.
- Fork the repository.
- Create a feature branch (
feature/your-thing
). - Add or update tests demonstrating the new behavior.
- Open a pull request with a clear description and migration notes.
High-value contributions:
- Pluggable storage backends (Redis/Postgres)
- WebAuthn UX improvements (client-side helpers)
- Optional email delivery fallback (e.g., webhook, SMS)
- Admin UI for user/session management
- Magic Link: One-time login URL sent by email.
- TOTP: Time-based One-Time Password used for second-factor or fallback.
- WebAuthn: Browser-native cryptographic authentication (passkeys/hardware keys).
- JWT: JSON Web Token, signed token representing session identity.
- Access Token: Short-lived JWT for API access.
- Refresh Token: Longer-lived token allowing issuance of new access tokens; stored server-side.
- Email Queue: Persistent retrying mechanism for reliable delivery.
- RP (Relying Party): The service (this server) in WebAuthn terminology.
MIT License. See LICENSE
for full terms.
Thank you for using Passwordless Auth! If you have questions, ideas, or want to contribute, check the Contributing section.