Passwordless-Auth-Rust

Passwordless Auth β€” Self-Hosted Passwordless Authentication Server

Rust Axum Cargo JWT WebAuthn TOTP SQLite SMTP OpenAPI Docker Docker Compose Makefile Testing Shell Scripting Rustfmt Clippy SQL C C# MIT License

Table of Contents

  1. Overview
  2. Goals & Motivation
  3. Key Concepts
  4. Architecture Diagrams
  5. Quickstart
  6. Installation & Build
  7. Configuration
  8. HTTP API Reference & Usage
  9. OpenAPI Specification & Client Example
  10. Email Queue Worker
  11. Testing
  12. Docker & Orchestration
  13. Shell Helpers & Scripts
  14. Example File Layout After Run
  15. Troubleshooting & Common Issues
  16. Security Considerations
  17. Extension Points / Developer Notes
  18. Contributing
  19. Glossary
  20. License

Overview

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.

Goals & Motivation

Key Concepts

Core Technologies

Authentication Flows

Security Primitives

Architecture Diagrams

High-level architecture overview

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

%% 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

Quickstart

# 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>"}'

Installation & Build

Prerequisites

Local Build

git clone <repo-url> passwordless-auth
cd passwordless-auth
make build

This produces the binaries:

Run Tests

make test

Configuration

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).

HTTP API Reference & Usage

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.

TOTP Flow

Enroll

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).

Verify

POST /totp/verify

{
  "email": "alice@example.com",
  "code": "123456"
}

Returns access and refresh tokens if the provided TOTP code is valid.

WebAuthn Flow

Registration Options

POST /webauthn/register/options

Body:

{ "email": "alice@example.com" }

Returns WebAuthn creation options (challenge, rp, user, etc.) for the client.

Registration Complete

POST /webauthn/register/complete

{
  "pending_id": "<from options response>",
  "response": { /* client attestation object */ }
}

Creates a credential tied to the user.

Login Options

POST /webauthn/login/options

Body:

{ "email": "alice@example.com" }

Returns assertion options.

Login Complete

POST /webauthn/login/complete

{
  "pending_id": "...",
  "response": { /* client assertion */ }
}

On success, returns JWTs.

Token Refresh

POST /token/refresh

Body:

{
  "refresh_token": "<refresh_jwt>"
}

Returns new access and refresh tokens.

OpenAPI Specification & Client Example

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

Sample Node.js Client Snippet

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();
}

Email Queue Worker

To improve reliability of magic link delivery, emails are enqueued in email_queue and retried with exponential backoff. The email-worker binary continuously:

This makes the system resilient to transient SMTP issues.

Testing

Unit Tests

Covers:

Run:

make test

Or directly:

cargo test

Integration Tests

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.

Docker & Orchestration

Build Image

make docker-build

Run Containers

docker compose up --build

Services:

Example Docker Invocation

docker run --rm -v "$(pwd)/config.toml":/app/config.toml -v "$(pwd)/migrations":/app/migrations passwordless-auth

Persisting State

Mount host volumes to retain:

Shell Helpers & Scripts

Provided helpers:

Make them executable:

chmod +x scripts/*.sh

Example File Layout After Run

.
β”œβ”€β”€ 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

Troubleshooting & Common Issues

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

Security Considerations

Extension Points / Developer Notes

Contributing

  1. Fork the repository.
  2. Create a feature branch (feature/your-thing).
  3. Add or update tests demonstrating the new behavior.
  4. Open a pull request with a clear description and migration notes.

High-value contributions:

Glossary

License

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.