Skip to main content

OWASAKA Authentication Model

This document describes how authentication works inside OWASAKA. It is the operational counterpart to ADR-0059 (which captures the design rationale, threat model, and compliance posture). When the two disagree, the ADR is the source of truth and this doc is the bug.


At a glance

┌──────────────────────────────────────────────┐
│ internal/identity │
│ │
┌─ Human ─┐ │ Principal {ID, Type, Subject, Status, …} │
│ pwd+TOTP│ ───┼──▶ ▲ │
│ WebAuthn│ │ │ │
└─────────┘ │ Credential │
┌ Service ┐ │ ▲ │
│ mTLS │ ───┼──▶ │ │
└─────────┘ │ AuthFactor ──▶ Authenticator │
┌ Agent ┐ │ │
│ API key │ ───┼──▶ │ │
│ / mTLS │ │ │
└─────────┘ │ │ │
┌ OIDC ┐ │ ▼ │
│ Zitadel │ ───┼──▶ DefaultMapper ──▶ identity.Principal │
└─────────┘ └──────────────────────────────────────────────┘


internal/identity/jwt
├── Issuer: Ed25519 sign, 15min/24h TTL
├── Verifier: parse + sig + revocation
└── JWKS: /.well-known/jwks.json

internal/identity/middleware
├── RequireAuth(handler) — HTTP gate
├── AuthorizeWS(req) — WS upgrade gate
├── WithDevMode(token, p) — escape hatch (env-gated)
└── PrincipalFromContext(ctx)

internal/identity/revocation
└── BoltDB-persisted JTI denylist + in-memory cache

Principal types

TypeTypical actorDefault factor
HumanSecurity operatorPassword + TOTP. WebAuthn is an opt-in upgrade. OIDC (Zitadel) optional, feature-flagged.
ServiceSpectre, Cerebro, future peersmTLS client cert chained to the internal CA.
AgentCLI, CI runner, automation scriptAPI key (oswk_<keyID>_<secret>). mTLS optional when an agent wants the stronger guarantee.

Every authenticated request carries a *identity.Principal in the request context (see middleware.PrincipalFromContext). All audit logs, signed events, and stored material reference that Principal's stable ID — credentials may rotate, the ID does not.


Token model (JWT)

PropertyValue
AlgorithmEdDSA / Ed25519
Access TTL15 minutes (configurable via jwt.WithAccessTTL)
Refresh TTL24 hours (configurable via jwt.WithRefreshTTL)
Signing keypki.PurposeJWTSigning keypair, rotated every 24h with a 1-hour verify-only overlap
Issuer claimowasaka
Access audienceowasaka-api
Refresh audienceowasaka-refresh (rejected if presented as access)
Public verificationGET /.well-known/jwks.json — JWKS over all currently-verifyable keys
RevocationPersistent denylist in BoltDB, indexed by JTI; cache primed at startup; O(1) hot path

Claims emitted

{
"iss": "owasaka",
"sub": "<principal.ID>",
"aud": ["owasaka-api"],
"iat": 1716000000,
"nbf": 1716000000,
"exp": 1716000900,
"jti": "<uuid>",
"ptype": "human|service|agent",
"psub": "<principal.Subject>",
"ttype": "access|refresh"
}

Downstream consumers (Spectre, Cerebro) verify against the JWKS and match iss=owasaka exactly. Audience gating prevents refresh tokens from being used as access and vice versa.


Wiring a service

The wiring lives in your application bootstrap (eventually internal/app/app.go, but for now any test or harness can reproduce it):

import (
"github.com/marcosfpina/O.W.A.S.A.K.A/internal/identity"
"github.com/marcosfpina/O.W.A.S.A.K.A/internal/identity/jwt"
"github.com/marcosfpina/O.W.A.S.A.K.A/internal/identity/middleware"
"github.com/marcosfpina/O.W.A.S.A.K.A/internal/identity/revocation"
"github.com/marcosfpina/O.W.A.S.A.K.A/internal/storage/pki"
)

// 1. PKI authority + JWT signing key.
authority := pki.NewAuthority(keystore)
if _, err := authority.EnsureRootCA(ctx, 365*24*time.Hour); err != nil { ... }
if _, err := authority.GenerateKeyPair(ctx, pki.PurposeJWTSigning, 24*time.Hour); err != nil { ... }

// 2. Issuer + Verifier.
issuer := jwt.NewIssuer(authority)
revocations, _ := revocation.Open(boltDB)
verifier := jwt.NewVerifier(authority, jwt.WithRevocationChecker(revocations))

// 3. Middleware.
mw := middleware.New(verifier, principalStore, logger)
mux.Handle("/api/", mw.RequireAuth(apiHandler))
mux.Handle("/.well-known/jwks.json", jwt.Handler(authority, nil))

Add middleware.WithDevMode(devToken, devPrincipal) only when the application has confirmed it is in development mode (e.g. OSWAKA_ENV=development); production builds never call this option.


Login flows

Password + TOTP (default human)

client → POST /auth/login { username, password, totp }
→ identity.Authenticator verifies factors
→ jwt.Issuer.Issue(principal) -> { access, refresh, jti }
→ 200 OK with tokens (or HttpOnly cookies)

API key (agent)

client → Authorization: Bearer oswk_<keyID>_<secret>
→ middleware splits via identity.ParseAPIKey
→ credentialStore.FindBySubject(CredentialAPIKey, keyID)
→ Credential.Verify against bcrypt hash
→ request enters handler with Principal in context

Agents typically receive a long-lived access token (or use the API key directly as a bearer). Refresh is unnecessary; rotate the key itself via identity.NewAPIKey and revoke the old key in the store.

mTLS (service)

The TLS handshake at the connection layer validates the leaf cert against pki.Authority root. Inside the handler, the cert's SPKI fingerprint (identity.SPKIFingerprint(cert)) is looked up in the MTLSCredential store to resolve the Principal.

OIDC (Zitadel, opt-in)

client → GET /auth/oidc/login?return=/dashboard
← 302 to Zitadel /authorize with signed state
... user authenticates at Zitadel ...
← 302 back to /auth/oidc/callback?code=…&state=…
→ state cookie verified against query state
→ oauth code exchanged at Zitadel
→ ID token verified against Zitadel JWKS
→ DefaultMapper resolves claims → identity.Principal
→ identity/jwt.Issuer mints OWASAKA's own JWT pair
← Set-Cookie: owasaka_access, owasaka_refresh + 302 to /dashboard

The OIDC integration never accepts Zitadel's tokens as OWASAKA tokens; it federates identity proof, then issues its own short-TTL JWTs that the rest of the system understands.


What lives where

ConcernPackage
Principal type, statusesinternal/identity (principal.go)
Credential interface + factorsinternal/identity (credential.go)
Password + TOTP, API key, mTLSinternal/identity (password_totp.go, apikey.go, mtls.go)
In-memory storesinternal/identity (memory_store.go)
JWT issuer / verifier / JWKSinternal/identity/jwt
Revocation denylistinternal/identity/revocation (BoltDB-backed)
HTTP / WS middlewareinternal/identity/middleware
OIDC clientinternal/identity/oidc
Root CA + Ed25519 keystoreinternal/storage/pki

Compliance cross-reference (recap from ADR-0059)

Control areaWhere implemented
LGPD Art. 46 (security measures)TLS layers, sops at-rest, audit-tagged Principal claims
LGPD Art. 18 (subject rights)Principal lifecycle history retained; subject access via Principal.ID
ISO 27001 A.9.2 (user lifecycle)PrincipalStore + credential lifecycle (provision, rotate, revoke)
ISO 27001 A.9.4 (system access)RBAC (Sprint 2) + MFA + token TTL
SOC 2 CC6.1 / CC6.6Mandatory auth on every endpoint, TTL, revocation
NIST 800-53 AC-2 (account mgmt)Principal type + status state machine
NIST 800-53 AC-3 (enforcement)middleware.RequireAuth
NIST 800-53 IA-2 (multi-factor)Password+TOTP default; WebAuthn opt-in
NIST 800-53 IA-5 (authenticators)24h key rotation, runbook in OPERATIONS.md
NIST CSF PR.AC-1Principal abstraction unifies all credentialed actors
NIST CSF PR.AC-7 (risk-tiered)TOTP standard, WebAuthn high-assurance, mTLS for services
EU AI Act Art. 12 (record-keeping)Identity-tagged events; persistent JTI revocation log
EU AI Act Art. 14 (human oversight)RBAC roles (Sprint 2) tied to verified Principal
EU AI Act Art. 26 (deployer)admin Principal is a natural person; WebAuthn recommended

See also