Authentication Overview¶
Craft Easy ships with a complete passwordless authentication stack. 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.
What is included¶
| 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 |
| API keys | Machine-to-machine access for services, integrations and scheduled jobs. | API Keys |
| 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 |
| Abuse protection | Disposable email blocking, rate limiting and CAPTCHA. | Abuse Protection |
All methods terminate in the same place: a signed JWT token issued by JWTService. Once a client holds a token, every downstream feature (access control, tenant isolation, audit logging) behaves identically regardless of how the user logged in.
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 → Is it a partner integration? │
│ Yes → API key (+ signed JWT for OAuth-like) │
│ No → Service account with API key │
│ │
│ Also available: OAuth2 for social login │
│ TOTP as a second factor │
└───────────────────────────────────────────────────────┘
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.
The AUTH_ENABLED flag¶
Authentication is centrally toggled by a single setting:
| 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¶
Every successful 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 signedTokenPayload.token_type— one ofuser,pre_auth(awaiting 2FA), orservice.expires— ISO-8601 UTC timestamp when the token stops being valid.requires_2fa— whentrue, the client must complete/auth/2fa/verifybefore calling any other endpoint.
Include the token in every subsequent request as:
See Sessions & Tokens for the full payload layout, refresh rules and blacklisting.
Dependencies in your routes¶
Pull the current user 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:
- Authentication (this section) — who are you?
- Scope guards — system/partner/tenant boundary.
- Feature guards — endpoint-level capabilities.
- Attribute access — field-level read/write.
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.