Skip to content

Authentication Overview

Craft Easy ships with a complete authentication stack covering both human users and machine-to-machine (M2M) callers. Every mechanism is implemented in craft_easy.core.auth and exposed by the built-in /auth/* routes — no extra libraries to pull in, no hand-rolled token handling.

Authentication types

Craft Easy supports two distinct authentication tracks:

  1. User authentication — humans logging in via OTP, OAuth2 (Google/Microsoft), BankID, TOTP/2FA, or WebAuthn/Passkeys. Sessions are tracked server-side and tokens issued via Authorization: Bearer <session_token>.

  2. Machine-to-machine (M2M) authentication — services calling Craft Easy APIs via OAuth 2.0 Client Credentials with private_key_jwt assertion (RFC 7523), optionally bound to the client key with DPoP (RFC 9449). ApiClients have capability lists that map to the same Capability Registry as human roles.

Choose the track that matches the caller type.

User auth

Method Use case Page
OTP (Email/SMS) Primary login flow for end users. Sends a one-time code over email or SMS. OTP
OAuth2 Social login via Google, Microsoft and GitHub. Pluggable for any custom provider. OAuth2
TOTP / 2FA Second factor with Google Authenticator, Authy or any RFC 6238 client. TOTP
WebAuthn / Passkeys Hardware-backed, phishing-resistant authentication. WebAuthn
Sessions & JWT Stateless ES512-signed tokens with blacklisting and refresh. Sessions

A user can enroll in several factors at once — OTP is always available as the first factor, TOTP or WebAuthn can be added as an upgrade.

M2M auth

Component Use case Page
Overview Architecture, flow diagram, and getting started guide. M2M Overview
API Clients Register services, manage keys, assign capabilities. API Clients
Private Key JWT OAuth 2.0 Client Credentials with private_key_jwt assertion (RFC 7523). Private Key JWT
DPoP Optional token binding to prevent stolen-token abuse (RFC 9449). DPoP Token Binding
Audit Event logging, Prometheus metrics, anomaly detection. Audit & Monitoring

Choosing a method

┌───────────────────────────────────────────────────────┐
│ Is the caller a human user?                           │
│   Yes → Does the tenant require strong auth?          │
│          Yes → WebAuthn / Passkeys (+ OTP fallback)   │
│          No  → OTP (Email/SMS)                        │
│   No  → Register an ApiClient with the M2M flow:     │
│          POST /api-clients → upload key → POST        │
│          /auth/token with private_key_jwt assertion   │
│                                                       │
│ Also available: OAuth2 for social login               │
│                 TOTP as a second factor               │
│                 DPoP for sender-constrained M2M tokens│
└───────────────────────────────────────────────────────┘

Abuse protection

Rate limiting, disposable email blocking, and CAPTCHA apply to both tracks. See Abuse Protection for the full configuration.

The AUTH_ENABLED flag

Authentication is centrally toggled by a single setting:

# settings.py
class Settings(CraftEasySettings):
    AUTH_ENABLED: bool = True
Value Behaviour
True (default) All /auth/* endpoints are active. require_auth rejects unauthenticated requests with 401.
False require_auth returns an anonymous TokenPayload (user_id="anonymous"). Intended only for local development against a non-production database.

Never ship AUTH_ENABLED=false to any environment that holds real data.

Token shape

User tokens

Every successful user login response follows this shape:

{
  "token": "eyJhbGciOiJFUzUxMiJ9...",
  "token_type": "user",
  "expires": "2026-04-05T12:30:00Z",
  "requires_2fa": false
}
  • token — opaque to the caller, contains the signed TokenPayload.
  • token_type — one of user, pre_auth (awaiting 2FA).
  • expires — ISO-8601 UTC timestamp when the token stops being valid.
  • requires_2fa — when true, the client must complete /auth/2fa/verify before calling any other endpoint.

M2M tokens

The M2M token endpoint returns a standard OAuth 2.0 response:

{
  "access_token": "eyJhbGciOiJFUzUxMiJ9...",
  "token_type": "Bearer",
  "expires_in": 1800,
  "scope": "payments:read settlements:create"
}

Both token types are used as:

Authorization: Bearer <token>

See Sessions & Tokens for the full user token payload layout, refresh rules and blacklisting. See Private Key JWT for M2M token details.

Dependencies in your routes

Pull the current user or service identity into any handler with require_auth:

from fastapi import APIRouter, Depends
from craft_easy.core.auth import require_auth, TokenPayload

router = APIRouter()

@router.get("/me")
async def me(user: TokenPayload = Depends(require_auth)):
    return {"user_id": user.user_id, "tenant_id": user.tenant_id}

Use optional_auth when an endpoint should accept both anonymous and authenticated callers:

from craft_easy.core.auth import optional_auth

@router.get("/public-with-personalization")
async def page(user: TokenPayload | None = Depends(optional_auth)):
    if user is None:
        return {"personalized": False}
    return {"personalized": True, "name": user.user_name}

Layered security

Authentication is layer 0. The subsequent layers are documented separately and all consume the same TokenPayload:

  1. Authentication (this section) — who are you?
  2. Capabilities — endpoint-level permissions, auto-generated from resources.
  3. Roles & Assignments — named capability sets assigned to users within scopes.
  4. Sensitive Fields — field-level read/write control.

Every page in this section links back to the concrete module under craft_easy/core/auth/, so you can always drill into the source when a configuration option is unclear.