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.
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>"}'
rustup
)cargo
make
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 workermake 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:
next_try_at
with backoff and records errorThis makes the system resilient to transient SMTP issues.
Covers:
Run:
make test
Or directly:
cargo test
Simulate full flows:
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 queuedocker 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 |
jwt_secret
must be long, random, and protected; rotate periodically.feature/your-thing
).High-value contributions:
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.