Full STRIDE analysis.
The per-subsystem STRIDE (Spoofing, Tampering, Repudiation, Information
Disclosure, Denial of Service, Elevation of Privilege) tables and the threat
catalog are further down this page. This is a design document, not an audit
report or a certification.
Trust boundaries
Config.TrustedProxies is explicitly set. By default, X-Forwarded-For is ignored and r.RemoteAddr is used for all rate-limiting and audit IP capture.
Session tokens
- Raw session tokens are opaque (cryptographically random,
crypto/rand). - Only a SHA-256 hash is stored in the database. A full database dump does not yield usable session tokens.
- Cookies are set with
HttpOnly,Secure(whenConfig.SecureCookieis true), andSameSite=Lax. - Revocation is a single
UPDATEthat marks the session hash as invalid. The raw token in the cookie becomes useless immediately.
Password handling
- Argon2id with OWASP 2026 recommended parameters.
- Minimum 12-character enforcement at sign-up.
- Anti-enumeration: unknown-email and empty-password branches pay the full Argon2id verify cost against a dummy hash synthesized once at
Newtime. Timing side-channel is closed. - Password reset tokens are single-use and short-lived (
PasswordResetTTL, default 1 hour). Consuming a reset token revokes all sessions for the user.
Rate limiting
- Per-IP and per-email rate limits on every credential endpoint.
- Rate limit buckets use
r.RemoteAddrby default;X-Forwarded-Foris only consulted when the proxy is inConfig.TrustedProxies. - POST
/oauth/registerhas a separate per-IP rate limit: 1 req/min in anonymous mode, 5 req/min in bearer-gated mode.
OAuth 2.1 AS
- PKCE S256 is mandatory. Requests without
code_challengeare rejected. - Audience binding (RFC 8707) is mandatory. Tokens are bound to a specific resource URI at mint time and verified at introspection.
- Refresh token family revocation: if a used (rotated) refresh token is presented again, the entire family is invalidated (RFC 9700).
- DCR bearer tokens are compared with
crypto/subtle.ConstantTimeCompareagainst pre-hashed SHA-256 digests. Timing oracle on token presence is closed. - Remote JWKS documents (a client’s
jwks_uriforprivate_key_jwt, or a trusted issuer’sJWKSURL) are fetched through a guarded client since v2.7.1: https only, non-public addresses refused at dial time, no redirects, a 5 second timeout, a 512 KiB cap, and a bounded cache with a TTL.JWTBearerConfig.AllowPrivateJWKSNetworksrelaxes this for local development only. - Signing keys are AES-256-GCM encrypted at rest (requires
Config.EncryptionKey).
Agent and delegation
- Agent status transitions are audited. Suspended and revoked agents fail authentication immediately after the
clientauthcacheTTL (default 5 minutes) expires. The cache is busted synchronously onSuspendAgentandRevokeAgent(introduced in v2.1.0 PR #33). - Delegation grants verify
body.userIdis a member of the admin’s organization before creation (cross-org grant attack closed, audit H3 2026-06-20). - Actor chain depth is capped at 3 at both the mint path and the config validation path (
ErrAgentChainDepthTooHigh).
WebAuthn / passkeys
- Sign-count replay protection: a new sign count must be strictly greater than the stored value. Clone-attempt returns
ErrReplayDetected. - Challenge storage is ephemeral and GC’d after
ChallengeTTL.
TOTP
- TOTP secrets are AES-256-GCM encrypted at rest.
- Recovery codes are SHA-256 hashed.
- 10 single-use recovery codes per user.
SAML
- Only signed assertions are accepted (
ErrSAMLUnsignedAssertionon unsigned). - AuthnRequest replay tracking with a configurable TTL.
SCIM
- Bearer tokens are SHA-256 hashed. Full token never stored.
- Per-organization isolation enforced at every CRUD endpoint.
Out of scope
The following are explicitly not covered by theauth-go:- TLS termination. Run a TLS-terminating reverse proxy (nginx, Caddy, AWS ALB) in front.
- DDoS and volumetric flood protection. Use a WAF or CDN-level rate limiter for sustained volumetric attacks.
- Physical security of the host running Postgres.
- Audit tamper-evidence. The
audit_eventstable is append-only (noUPDATE/DELETEin theStorageinterface) but not cryptographically tamper-evident: it is not Merkle-chained or signed. This is not currently ondocs/ROADMAP.md; operators who need tamper-evidence should layer Merkle signing or stream to an append-only SIEM sink themselves. - ABAC and hierarchical roles. RBAC is a closed permission catalog with flat roles. Time-bound and attribute-based access control are deferred.
- Hardware-attested agent credentials.
AgentCredentialKindJWKandAgentCredentialKindX509paths exist but returnErrNotImplemented. TPM-backed signing is a planned future feature.
Reporting security issues
See SECURITY.md. Report security vulnerabilities tosecurity@glincker.com. Do not open public issues for security findings.
Full STRIDE threat analysis
Scope: theauth-go library, as deployed by an operator in a Go HTTP application. Method: STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege). Last reviewed: 2026-10-07, against v2.7.1 and the CHANGELOG since v2.4.0. Re-checked claims are those about cookies, OAuth state, CIMD fetching, login throttling, storage backends, SAML and dependency versions; the rest was spot-checked against the code, not re-derived. This is a design document, not an audit report or a certification.Table of Contents
- Trust Boundaries
- Components in Scope
- Out of Scope
- Authorization Server (AS)
- Resource Server / mcpresource
- Storage Backend
- Authentication Flows
- Grant Types
- Audit Subsystem
- Session Subsystem
- Agent Identity Flow
- Delegation Chains
- CIMD
- JWT-Bearer and private_key_jwt Key Fetches
- Threat Catalog
Trust Boundaries
The library does not control what crosses the operator’s TLS terminator, the operator’s secret store, or the operator’s hosting environment. Those are explicitly out of scope.
Components in Scope
- Authorization Server (AS): issues JWTs (authorization_code, refresh_token, client_credentials, token_exchange grants). Lives in
internal/as/. - Resource Server / mcpresource: validates inbound JWTs at protected API endpoints. Lives in
mcpresource/. - Storage backend: memory (
storage/memory/), Postgres (storage/postgres/), MySQL (storage/mysql/) and SQLite (storage/sqlite/, a separate module) stores. Pluggable via theStorageinterface and its capability interfaces instorage.go. - Authentication flows: password, OAuth social login, magic link, WebAuthn, TOTP, SAML, SCIM.
- Audit subsystem: event emission, redaction, sinks. Lives in
internal/audit/,audit/. - Session subsystem: session minting, lookup, expiry, revocation. Lives in
internal/session/. - Agent identity flow: machine credential issuance and revocation. Lives in
internal/agent/. - Delegation chains: scoped delegation grants. Lives in
internal/delegation/. - CIMD (Client Identity Metadata Document): fetches and caches machine-client metadata. Lives in
internal/cimd/. - RBAC: role and permission enforcement. Lives in
internal/rbac/.
Out of Scope
The following are explicitly not mitigated by this library. They are the deploying operator’s responsibility.- TLS termination and certificate management. The library emits a
slog.Warnat startup whenSecureCookie=falseandBaseURLis not https, and addsSecureto cookies per request whenBaseURLis https, the connection is TLS, or aTrustedProxiespeer sendsX-Forwarded-Proto: https. It cannot enforce TLS on the transport layer itself. - Secret management at rest. Encryption keys (
AuditConfig.EncryptionKey, TOTP key, etc.) must be supplied by the operator; the library does not manage a secret store. - Container/VM/Kubernetes pod isolation. Network policies, pod security admission, and kernel security modules are outside library scope.
- Operator’s authentication device security. TOTP app compromise, WebAuthn key theft, or phone compromise for CIBA-style flows are not addressable at the library layer.
- DDoS at the network layer. The library provides application-layer limits (
RateLimitByIP,RateLimitByEmail, and the password login throttle) but not TCP/UDP-layer flood protection. - Side channels in Go runtime or
crypto/subtle. The library usescrypto/subtlefor all sensitive comparisons but cannot guarantee the Go runtime’s memory allocator or garbage collector do not introduce timing channels.
Authorization Server (AS)
Trust Boundaries
- Receives
client_id,client_secret,code_verifier, andDPoPheader values over HTTP. - Reads client rows and authorization code rows from storage.
- Holds Ed25519 private keys in process memory.
- Writes access tokens and refresh tokens to storage.
STRIDE Analysis
Spoofing
Tampering
Repudiation
Information Disclosure
Denial of Service
Elevation of Privilege
Resource Server / mcpresource
Trust Boundaries
- Receives HTTP requests with
Authorization: Bearer <token>orAuthorization: DPoP <token>plus optionalDPoP:proof header. - Fetches public keys from the JWKS endpoint (operator-configured URL, cached in process).
- No storage writes.
STRIDE Analysis
Spoofing
Tampering
Information Disclosure
Denial of Service
Storage Backend
Trust Boundaries
- Library code speaks to storage via the
Storageinterface and its capability interfaces (defined instorage.go). - The Postgres, MySQL and SQLite stores pass values as bound parameters (Postgres uses sqlc-generated and hand-written queries; MySQL and SQLite use hand-written ones). Where SQL text is assembled with
fmt.Sprintf, the pieces are constants such as the migration ledger name and a shared liveness predicate. - Memory store is in-process only; no network trust boundary.
- The shared contract suite (
storagetest) is authoritative against the memory backend in CI. Running it against Postgres and MySQL is opt-in and currently fails on constraint and foreign-key edge cases, so those adapters are less proven against the contract than the memory store (see the roadmap).
STRIDE Analysis
Tampering
Information Disclosure
Denial of Service
Authentication Flows
Password
Trust Boundaries: Receives plaintext password over HTTP (must be HTTPS at operator’s transport layer). Reads password hash from storage.
Residual risk: Operator must enforce HTTPS to prevent plaintext password capture.
OAuth Social Login
Trust Boundaries: Operator’s app redirects user to provider; provider redirects back withcode + state. The state is a crypto/rand token held in a server-side state store; the theauth_oauth_state HttpOnly cookie carries a separate browser-binding secret, not the state value (changed in v2.6.0).
Magic Link
Trust Boundaries: Token emailed to user; operator’s email delivery path is outside library scope.WebAuthn
Trust Boundaries: Browser generates a cryptographic assertion over the challenge; assertion verified against stored public key.TOTP
Trust Boundaries: User supplies 6-digit code generated by an authenticator app.SAML
Trust Boundaries: IdP posts a signed XML assertion to the library’s SP assertion consumer service.SCIM
Trust Boundaries: SCIM client presents a bearer token; library validates it against a stored hash.Grant Types
Authorization Code + PKCE
Refresh Token
Client Credentials
Token Exchange (RFC 8693)
DPoP-Bound Tokens (RFC 9449)
PAR (Pushed Authorization Request)
PAR (RFC 9126) is implemented (internal/as/par.go). When AuthorizationServerConfig.PAR is set and the configured storage backend implements the optional PARStorage interface, POST /oauth/par accepts the full set of authorization parameters over a back-channel POST and returns a request_uri reference; GET /oauth/authorize then accepts that request_uri in place of inline query parameters. PARConfig.RequirePAR (default false) can be set to reject any /oauth/authorize request that still passes inline parameters, forcing every client through PAR.
Residual risk: PAR is opt-in and not mounted at all unless the storage backend satisfies PARStorage (the memory and Postgres adapters do; the MySQL and SQLite adapters do not). Deployments that have not enabled AuthorizationServerConfig.PAR still pass authorization parameters as query string values on the redirect; an attacker who can intercept the redirect URL can observe but not forge them (PKCE protects the exchange). Those deployments should combine RequireState=true with PKCE S256 as an interim mitigation.
Audit Subsystem
Trust Boundaries
- Events are emitted in-process; sink goroutines forward them out-of-process to OTLP, Splunk, or webhook endpoints.
- The webhook sink signs each request body with HMAC-SHA256 (
audit/sinks/webhook/webhook.go).
STRIDE Analysis
Repudiation
Information Disclosure
Denial of Service
Session Subsystem
Trust Boundaries
- Session token is a
crypto/randopaque value stored as SHA-256 hash in the database. - Token is transmitted as an HttpOnly cookie.
STRIDE Analysis
Spoofing
Tampering
Information Disclosure
Agent Identity Flow
Trust Boundaries
- Machine clients authenticate with
client_id+client_secret; secret is Argon2id-hashed. - Agents issue tokens via the
client_credentialsgrant.
STRIDE Analysis
Spoofing
Elevation of Privilege
Delegation Chains
Trust Boundaries
- An agent acting on behalf of a user (RFC 8693 token exchange) carries a delegation grant that specifies the permitted scope.
STRIDE Analysis
Elevation of PrivilegeCIMD (Client Identity Metadata Document)
Trust Boundaries
- The AS fetches a JSON document from an HTTPS URL supplied as the
client_id. - The document is cached in process memory for the configured
CacheTTL.
STRIDE Analysis
Spoofing
Server-Side Request Forgery
Since v2.6.0, CIMD fetches also refuse non-public destinations: the dialer rejects loopback, private, link-local (including cloud metadata addresses), CGNAT and similar ranges at connect time (so a resolver that rebinds after validation cannot swap in a private address), redirects are never followed, and no proxy is used.
CIMDConfig.AllowPrivateNetworks is a development opt-in that turns the check off, and CIMDConfig.DenyHost adds a host deny hook.
Residual risk: TrustPolicy is still the primary gate. With AllowAnyHTTPS() any public HTTPS host can be fetched, so prefer AllowHTTPSHost allowlists in production, and leave AllowPrivateNetworks off outside local development.
JWT-Bearer and private_key_jwt Key Fetches
Trust Boundaries
- For clients using
private_key_jwtor the JWT-bearer grant, the AS fetches a JWKS document from a client’s registeredjwks_urior a trusted issuer’sJWKSURL(internal/as/jwtbearer.go).