Security posture

What axess chooses for you, what it leaves to you, and what an auditor will ask about. Read it before launch, not after the questionnaire arrives.

Crypto backends

Axess uses RustCrypto for every primitive it implements itself, unconditionally: AES-256-GCM (the session envelope), HMAC-SHA256 (cookie signing, fingerprint binding), Argon2id (password hashing), TOTP and HOTP (RFC 6238 and RFC 4226), and SHA-256 (refresh token hashing). These are plain dependencies. There is no feature that swaps them for another implementation, and no cfg in the source that selects between backends.

The one backend an adopter chooses is for JWT signature verification, because jsonwebtoken takes its provider from a cargo feature and will not pick one for you. Anything enabling jwt names one of:

[dependencies]
# Pure Rust, builds anywhere. The default choice.
axess = { version = "0.7.0", features = ["jwt", "jwt-rust-crypto"] }

# aws-lc-rs: wraps the FIPS-validated aws-lc, needs a C toolchain
# (and NASM on Windows), and does not build on every target.
axess = { version = "0.7.0", features = ["jwt", "jwt-aws-lc"] }

Naming neither is a compile error. Naming both is allowed, because cargo can enable the second one when another crate in the build asks for it, and axess installs one provider rather than letting jsonwebtoken panic.

ring still appears in the dependency graph through TLS-adjacent crates (rustls and its consumers), not through axess's own code.

FIPS targeting

Axess does not today offer a FIPS-validated build. A deployment that needs one should read this section as a statement of the gap rather than a route through it.

The reason is the first of the three things a FIPS 140-3 deployment requires: every cryptographic operation must run through a validated module. Axess's own primitives are RustCrypto, which is not validated, and they are not switchable. The jwt-aws-lc feature routes JWT signature verification through aws-lc-rs, and that is the only operation it covers. The session envelope, password hashing, refresh-token hashing and HMAC fingerprint binding do not move with it.

The second requirement is that the compile and link chain introduces no non-validated crypto. Cargo's dependency graph is the source of truth: cargo tree and inspecting for non-aws-lc crypto crates (rustls, ring, the RustCrypto crates) shows what the deployment actually pulls in.

The third is that the validation certificate covers the platform the deployment runs on. NIST publishes FIPS validation certificates per platform-binary combination; a certificate for Linux x86-64 does not cover macOS ARM. The deployment's compliance evidence must include the certificate matching the production platform.

Closing the gap means making the remaining primitives selectable, which is a design change and not a feature flag. Raise it as an adopter requirement if you need it.

PII classification

The application records PII across several stores. The classification matters for GDPR (the data subject's rights), for SOC 2 (the control objectives), and for the retention sweep (Device identity's device_retention_days). The classification:

ClassWhatWhereRetention
PrimaryIdentifier (email, username), password hash, TOTP secret, FIDO2 credentials, the IP seen at authentication, device fingerprintIdentity store, device storeYours to choose, within whatever regulatory bounds apply
SecondaryThe audit-event log, which reaches the primary through user_id, tenant_id, device_id and client_ipAudit storeAudit pipeline. The usual GDPR pattern keeps it longer than primary PII but scrubs or hashes the IPs once the hot window closes
PseudonymousSession id, refresh-token hash, device id (a UUID that names no user)Session store, device storeLonger than primary PII, with no GDPR implication

Pseudonymous is a claim about the data on its own. Each of those values becomes PII the moment it is joined to the primary set, and the join needs access to the identity store, so that store's access control is what keeps the classification true.

The GDPR right-to-erasure verb is IdentityAdmin::delete_user, and what it does is your implementation's decision rather than a cascade axess runs. The trait states the contract: delete or irreversibly anonymise the user row, every factor config under AuthnScope::User, the refresh tokens, the persisted sessions, the password history, and any application rows whose retention basis was the consent now withdrawn. After Ok(()), get_user, find_user and account_status must report the user gone, and any in-flight session must fail its next is_valid check.

Its default body panics rather than returning an error, so a backend that never overrode it fails loudly the first time an erasure request arrives instead of reporting success and deleting nothing. The audit-event entries that reference the user are not removed (the audit trail is load-bearing for compliance); the user's identifier in the events is hashed to a pseudonymous token, which makes the events non-PII without losing the ability to correlate them.

Compliance touch-points

The deployment will face one or more of these regulatory frames. Axess does not provide compliance on its own; it provides the controls each framework requires. The touch-points:

FrameWhat axess gives youWhat stays yours
GDPR (EU data protection)The erasure verb above, audit retention configuration, IP scrubbing in the cold-tier archive, DeviceStore::sweep against your SweepConfig thresholdsData subject notices, the privacy policy, the legal basis for processing
SOC 2 (operational controls)The audit catalogue, the lockout policy against credential stuffing, session and refresh-token security, the operational metricsPolicy and procedure documentation
PCI-DSS (card data)Strong authentication for administrative access, audit retention of at least one year, session data encrypted at restThe cardholder data environment. Axess covers the authentication boundary into it, not the environment
HIPAA (US healthcare)Strong authentication for access to protected health information, audit retention of at least six years, session data encrypted at rest and in transitThe HIPAA-covered systems, on the same boundary split

One gap in that first column is easy to miss under SOC 2: every authentication decision produces an AuthEvent, but authorisation decisions go to a tracing target instead. They are not in the catalogue, so an evidence pipeline that reads only AuthEvent rows will not have them. Wire the target separately.

For the mechanism behind a specific control: Session lifecycle and crypto envelope for encryption at rest, Audit pipeline for retention, Refresh tokens and session continuity for refresh-token hygiene, Multi-tenancy for the lockout policy.

Failing closed

Two stores sit behind the login path, and what happens when each one is down is a decision rather than an accident.

The lockout counter is the first. record_failed_attempt is a write, and the read-replica split this library encourages puts reads on a replica and writes on the primary, so a primary outage leaves logins working and the counter dead. LockoutPolicy::on_counter_unavailable decides what happens then. It defaults to CounterUnavailable::Lock, which treats the attempt as locked and keeps brute force bounded. CounterUnavailable::Allow keeps those users logging in and leaves lockout disabled until the counter returns. Alert on AuthnMetrics::factor_counter_store_outage either way: under Lock it explains the support calls, and under Allow it is the only signal that a control is off.

The audit store is the second, and it fails closed with no switch. If IdentityAuthnLog::record_event returns an error, the flow returns AuthnError::Store and the login does not succeed. An authentication that leaves no evidence has not, for evidence purposes, happened, and a catalogue offered as SOC 2 or PCI-DSS evidence cannot be allowed to develop holes quietly.

The cost is availability, and it is not small: logins fail while the audit store does. Put the sink behind something durable. Write locally and ship asynchronously, so record_event only fails when a local write fails, rather than calling a remote service on the request path. AuthnMetrics::audit_store_outage fires on this path and should page rather than feed a dashboard, because every login is failing while it does.

One consequence is worth stating, because the obvious implementation gets it wrong. Every path that rejects a login emits before it returns, including the ones where the tenant or the identifier does not exist. That is what stops an audit outage from becoming a user-enumeration oracle: if only known users triggered an audit write, an attacker who could degrade the audit store would see Err for real accounts and an ordinary rejection for everything else, and could read off which identifiers are registered. Both paths write, so both fail identically.

The same change closes a blind spot that had nothing to do with outages: a credential-stuffing run against a list of addresses, none of which are registered, previously left no audit rows at all. Those attempts now appear as LoginAttempt / Failure with an error of unknown_tenant or unknown_identifier, unattributed or attributed to the tenant only.

Disclosure protocol

The vulnerability disclosure protocol lives in the canonical SECURITY.md at the repo root. The summary:

Vulnerability reports go through the private channel described in SECURITY.md (typically a security email or GitHub Security Advisories). Do not file vulnerabilities on the public issue tracker.

The maintainers acknowledge reports within a few business days and triage to a severity level. Critical and high-severity issues get a private fix in a security branch, a coordinated disclosure window, and a CVE if the issue warrants one. Lower severity issues fix in the normal development cycle.

Adopters are expected to keep their axess dependency current. Vulnerability fixes ship in the next patch release; the changelog notes which fixes are security-relevant. Deployments behind on patches accept the risk of the unfixed vulnerabilities.

Canonical SECURITY.md

The rest of this chapter is the canonical SECURITY.md from the repo root, included so the production checklist is in one place.

Security Policy

Reporting a Vulnerability

If you discover a security issue in Axess, please report it through GitHub's private vulnerability reporting (the Report a vulnerability button under the repository's Security tab) or by emailing security@gnomes.ch. Do not open a public issue.

Response targets (best-effort while the project is pre-1.0):

  • Acknowledgement: within 48 hours of report
  • Triage and severity assessment: within 7 calendar days
  • Critical / High fix: patch release within 7 calendar days of confirmation
  • Medium fix: patch in the next scheduled release (typically within 30 days)
  • Advisory: published via GitHub Security Advisory once a fix is available

Only the latest 0.x minor receives security patches. If you are on an older version, upgrade to receive fixes.

Using Axess Securely

Axess is a library for authentication and authorization. Its security depends on correct integration and configuration in your application.

Production integration checklist

Transport and cookies

  • Terminate TLS before Axess sees requests. All session cookies default to Secure; HttpOnly; SameSite=Lax.
  • Set an HSTS header (Strict-Transport-Security: max-age=63072000; includeSubDomains) at the reverse-proxy or application layer so browsers never downgrade to HTTP.
  • Use a cryptographically random 32-byte signing key loaded from a secrets manager. Never hard-code or re-use the all-zero example key.

CSRF

  • Mount CsrfLayer on state-changing routes. The shipped middleware implements signed double-submit cookie protection; CsrfConfig::new(signing_key) is the entry point.
  • SameSite=Lax (the cookie default) mitigates the most common vectors, but is not sufficient on older browsers or cross-site GET-triggered mutations; keep CsrfLayer engaged.
  • For API-only endpoints, validate Origin / Referer headers or use a custom request header as a CSRF defence in addition.

Session binding and hijacking

  • Enable session binding (e.g. UserAgentBinding) to detect cookie theft from a different browser/client.
  • Understand the trade-off: session binding raises the bar for opportunistic theft but does not protect against an attacker who copies the User-Agent string along with the cookie.
  • Consider combining with IP-subnet or TLS channel binding for higher-security environments.

Session registry and forced logout

  • If using a session registry for forced logout, guard all authenticated routes with registry validity checks; not just require_authn!; so suspended or force-logged-out users cannot continue using stale sessions.
  • Call suspend_user (which automatically invalidates registry entries) rather than updating store status manually.

Rate limiting

  • Apply per-IP rate limiting on login, factor verification, and OAuth callback endpoints using the built-in RateLimitLayer. Axess enforces per-user lockout, but distributed brute-force across many usernames requires IP-level throttling.

Recommended configuration for authentication endpoints:

use axess::{RateLimitLayer, RateLimitConfig, KeyExtractor};
use std::time::Duration;

// Tight limit for login / factor verification (5 attempts per 60 s per IP).
let auth_rate_limit = RateLimitLayer::new(
    RateLimitConfig::builder()
        .max_requests(5)
        .window(Duration::from_secs(60))
        .key(KeyExtractor::ClientIp)
        .build(),
);

// Separate, tighter limit for OTP verification (3 attempts per 60 s).
let otp_rate_limit = RateLimitLayer::new(
    RateLimitConfig::builder()
        .max_requests(3)
        .window(Duration::from_secs(60))
        .key(KeyExtractor::ClientIp)
        .build(),
);

let app = Router::new()
    .route("/login", post(login_handler))
    .route("/verify-totp", post(totp_handler))
    .layer(auth_rate_limit)
    // Or apply per-route:
    .route("/verify-email-otp", post(otp_handler))
    .route_layer(otp_rate_limit);
  • Rate-limit OTP verification endpoints separately; 8-digit email OTPs have 10^8 possibilities but a tighter window reduces feasibility further.

Trusted proxy and IP extraction

  • Wrap the router in axess::client_ip::layer with a TrustedProxies set naming your proxies, and read ClientIp wherever an address is needed. Nothing reads a forwarded header directly any more: there is no function that takes one and believes it.
  • Still configure your reverse proxy to strip client-supplied X-Forwarded-For and X-Real-IP. The trusted-proxy walk is defence in depth, not a substitute.

Rate limiting is required in front of login routes

  • Apply the rate-limit layer to every route that reaches begin_login. Axess does not enforce it inside the service, and since 0.6.0 every failed login, including for identifiers that do not exist, writes an audit row, while a failed audit write fails the login. Without a limit, an unauthenticated caller can drive unbounded writes at your audit store, and exhausting it turns every login into a failure for every user. Your sink can also shed under load by returning AuditOutcome::Shed, but shed on a global rate or watermark, never on anything derived from which identifier was tried, or the drop becomes a user-enumeration signal.
  • Alert on AuthnMetrics::audit_store_outage. It fires on exactly that path.
  • Put the audit sink behind something durable: write locally, ship asynchronously, rather than a remote service on the request path.

Identity store lookup latency

  • If your IdentityStore caches find_user or account_status, cache negative results on the same terms as positive ones. Axess equalises login response time between known and unknown identifiers using a fresh random id, which can never hit a cache; caching only real users inverts the timing signal and restores a user-enumeration oracle.

Session store selection

  • In-memory stores (MemorySessionStore, MemoryRefreshTokenStore) are for testing only. They use non-constant-time lookups and do not persist across restarts.
  • SQL stores (SqliteSessionStore, PostgresSessionStore, MysqlSessionStore) support optional AES-256-GCM encryption at rest via SqliteSessionStore::new(pool, SessionCrypto::new(key)); opt out only via the explicit ::plaintext(pool) constructor (dev/test only).
  • Valkey store supports AES-256-GCM encryption via ValkeySessionStore::new(client, key). Plaintext available via ::plaintext(client) for dev/test.
  • All encryption-capable stores support key rotation via SessionCrypto::with_previous_key(old_key); sessions encrypted with the previous key are transparently re-encrypted on the next access.

Content Security Policy

  • Set a Content-Security-Policy header on all HTML responses to mitigate XSS impact. At minimum: default-src 'self'; script-src 'self'; style-src 'self'.
  • Avoid unsafe-inline and unsafe-eval in CSP directives.

OAuth / OIDC

  • Register only HTTPS issuer URLs. Axess rejects http:// issuer URLs in discovery (localhost / 127.0.0.1 / [::1] exemption for dev).
  • Request the minimum scopes needed; avoid offline_access unless refresh tokens are required.
  • Validate that the OAuth redirect URI matches exactly; do not use wildcard patterns.

Social login (plain OAuth 2.0)

  • Prefer OIDC whenever the provider supports it. Reach for SocialProvider only for IdPs that explicitly don't (GitHub user login, Twitter/X, Discord, Reddit, Spotify, …).
  • Understand the weaker security model: identity comes from a userinfo HTTPS GET, not from a signed assertion. A compromised IdP can impersonate any of its users to your service; you accept that blast radius when you adopt the provider.
  • Keep PKCE on (the default). A handful of providers reject the extra parameter; SocialProvider::without_pkce is the opt-out and should be used sparingly.
  • Verify csrf_state echo on the callback before calling exchange_code; SocialProvider::mint_csrf_state produces a fresh value routed through the same injectable RNG as PKCE.

Workload identity

  • Pin the trust domain at resolver construction. Every shipped resolver (JwtSvidResolver, MtlsResolver, WorkloadResolver) accepts an expected TrustDomain and rejects tokens / certs whose synthesised WorkloadId lives under a different one; defense in depth against a confused-deputy where the JWKS or CA happens to be shared across trust domains.
  • For the generic WorkloadResolver, keep adopter-supplied claim mappers strict about which subject paths the application admits. The recipes in examples/workload-identity/ are templates, not policy.
  • When fetching SVIDs from a local SPIRE agent, use the spire-workload crate today; see docs/workload-identity/jwt-svid.md for the fetch-side recipe.

Dependencies

  • Regularly update Axess and its dependencies (cargo update).
  • Run cargo audit in CI to catch known vulnerabilities in the dependency tree.

Trusted proxy configuration (detailed)

Axess can extract client IP addresses from the X-Real-IP and X-Forwarded-For headers for audit logging and rate limiting. These headers are only trustworthy if your reverse proxy strips or overwrites them before forwarding, and if you use the trusted-proxy form described below.

If you don't run behind a trusted reverse proxy, these headers are user-controlled and any IP-based security decision (rate limiting, geo-blocking, audit trails) can be spoofed.

Configure your reverse proxy to:

  1. Strip incoming X-Forwarded-For and X-Real-IP from client requests.
  2. Set X-Real-IP to the immediate client address (TCP peer).
  3. Append X-Forwarded-For with the client address (for multi-hop chains).

Example for nginx:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

What axess does with these headers

Since 0.6.0 axess does walk the forwarded chain and does maintain a trusted-proxy allowlist. trusted.client_ip(headers, peer):

  1. Returns the TCP peer immediately if the peer is not itself a trusted proxy: an unproxied client's headers are never read.
  2. Otherwise joins every X-Forwarded-For field line in order (RFC 9110 §5.3 makes repeated lines one comma-joined list) and walks it right to left, skipping hops that are themselves trusted, returning the first address that is not.
  3. Stops at a malformed entry and returns the peer, rather than trusting past it.
  4. Reads X-Real-IP only when no X-Forwarded-For is present, and only when there is exactly one such line: more than one means something appended rather than overwrote, so the first is whatever the client sent.

Rightmost-untrusted is the only correct reading: X-Forwarded-For is append-only, so the leftmost entry is whatever the client chose to put there. Axess took the leftmost entry before 0.6.0, which was spoofable through a correctly configured proxy.

Build the allowlist with exact addresses or CIDR ranges:

use axess::client_ip::TrustedProxies;

let trusted = TrustedProxies::from_cidrs(["10.0.0.0/8", "2001:db8::/32"])?;
// or, for a same-pod sidecar:
let trusted = TrustedProxies::loopback_only();

A spec with bits set below the prefix is rejected rather than widened: 10.0.0.5/8 is an error, because it reads like one host and would mean sixteen million. Write 10.0.0.0/8 or 10.0.0.5/32.

Since 0.7.0 you do not usually call the walk yourself. client_ip::layer runs it once per request, outside every route, and hands each handler a ClientIp:

let app = axess::client_ip::layer(app, trusted);

ClientIp has a private field and the layer is the only thing that fills it, so no amount of header reading produces one. Everything that wants an address takes it from there: the audit context, the Cedar request context, the rate limiter's KeyExtractor::ClientIp, the device gate, and lockout.

There is no longer a function that reads a forwarded header and believes it. The untrusted forms were removed in 0.7.0: a name carrying its own warning was tried twice and lost both times, once to a consumer that moved seven call sites onto one because its documentation recommended it.

Feature inventory

The shipped security surface, grouped by area. Caveats live in the Notes column.

Authentication factors

FeatureNotes
PasswordArgon2id hashing with workspace-pinned parameters, per-user lockout, password-reuse history, plaintext zeroized after hash.
TOTP (RFC 6238)Constant-time comparison, last-step replay guard, SHA-1 / SHA-256 / SHA-512, 6–8 digit codes.
HOTP (RFC 4226)Counter advancement, zeroized secrets, same algorithm options as TOTP.
Email OTP8-digit default, Argon2-hashed codes at rest, TTL-bound, single-use.
FIDO2 / WebAuthnRegistration + authentication + clone detection + discoverable / passwordless. Per-ceremony UV / attestation policy waits on webauthn-rs 0.6 stable; see ROADMAP.md.
LDAP bindVerifier over ldap3 with TLS via rustls. Bind only; schema mapping is the application's responsibility.
mTLS factorX.509 certificate verification against a configured trust anchor; SAN URI extraction for SPIFFE / regular identity binding.
JWT bearerGeneric JWT verifier with JWKS rotation, iss / aud / exp / nbf / alg allowlist, clock-injected for DST.
Multi-factor chainsOrdered factor pipeline (FactorStep::AnyOf for choice steps), session state machine enforces sequencing at compile time.

Sessions

FeatureNotes
Session cookiesHMAC-SHA256 signed, Secure; HttpOnly; SameSite=Lax by default. Configurable via SessionLayer::with_secure / with_same_site.
Session bindingHMAC-keyed fingerprint (not a plain hash); UserAgentBinding + extension points for IP / TLS channel binding.
Session registry + forced logoutSessionRegistry::invalidate_user is error-observable and fail-closed; cooperates with suspend_user for combined identity + session revocation.
ID cyclingAutomatic on Guest→Authenticated transition (fixation defense) and on logout; explicit AuthSession::regenerate for app-defined privilege boundaries (MFA enrollment, password change, role grant). See docs/sessions/lifecycle.md.
Refresh tokensRotation with family revocation on reuse; integration with device-binding cascade.
In-memory storeTesting only; no persistence, no encryption, non-constant-time lookups.
SQLite session storeOptional AES-256-GCM via SqliteSessionStore::new(pool, SessionCrypto::new(key)). Key rotation via with_previous_key.
Postgres session storeSame encryption model. Recommended for multi-instance deployments. Validated against CockroachDB via the cockroach_compat CI job.
MySQL / MariaDB session storeSame encryption model. Tested against MySQL 8.x and MariaDB 10.5+.
Valkey session storeAES-256-GCM, key rotation, TTL-managed eviction.
Cross-backend Store<K, V> traitAll five backends implement it for adopters that want backend-agnostic dispatch via Arc<dyn Store<…>>.

Device identity

FeatureNotes
Three-stage trust ladderUnknown → Seen → Trusted (plus terminal Revoked); retention sweep demotes idle devices and purges revoked rows past the grace window.
Per-tenant fingerprint pepperStops cross-tenant fingerprint correlation; TenantPepperResolver is adopter-provided.
Cascade revocationRefresh-token family compromise revokes every device that carried that family's binding.
CachedDeviceStore decoratorLRU + clock-driven TTL eviction; revocation propagates through set_trust_level.
Five DeviceStore backendsMemory, SQLite, Postgres, MySQL / MariaDB, Valkey; surface-equivalent across SQL dialects + Valkey hash storage, optional AES-256-GCM envelope on the bindings blob (SQL backends).
Adopter-supplied store recipeDocumented contracts (tenant scoping, atomic save, hot-path sighting, required sweep) in docs/identity/device.md for adopters with non-shipped backends (DynamoDB, MongoDB, …).
PII tokenisationMemoryDevicePiiStore reference impl + adopter trait for the GDPR-scoped fields (label, last-seen IP).

Workload identity

FeatureNotes
Principal::{Human, Workload} unified abstractionSame ToCedarEntity bridge for both shapes; Cedar policies authorise across without branching.
JwtSvidResolverSPIFFE JWT-SVID spec adherence; mandatory spiffe:// URI in sub, trust-domain extracted and pinned.
MtlsResolverSPIFFE X.509-SVID over mTLS via leaf-cert SAN URI extraction.
WorkloadResolver<C, F, R>Generic JWT-bearer workload resolver covering GitHub Actions OIDC, Kubernetes service accounts, GitLab CI, Okta, Azure AD, Auth0, LocalIdP, and any other JWT-issuer via an adopter-supplied claim parser + mapping closure. Ready-made recipes for GitHub Actions + k8s SA ship in examples/workload-identity/.
Cloud STS exchangeaws-sts, gcp-wif, azure-fic adapters for exchanging federated workload identity for short-lived cloud credentials.
Outbound identityoutbound-oauth (axess as OAuth client with client_credentials / private_key_jwt) and outbound-mtls (axess presenting an mTLS identity to downstream services).

OAuth / OIDC ceremonies

FeatureNotes
Authorization Code + PKCEDiscovery, token exchange, nonce validation; HTTPS-enforced (localhost / 127.0.0.1 / [::1] exemption for dev).
Client CredentialsReal HTTP token exchange via OAuthProviderConfig.
Device Code (RFC 8628)Real HTTP; device endpoint configured via with_device_authorization_endpoint. Nonce-bindable per RFC.
Token refreshProvider-delegated refresh with audit logging.
FAPI 2.0 Baseline ProfilePushed Authorization Requests (PAR, RFC 9126), DPoP (RFC 9449), JARM, RP-initiated logout. Strict nbf enforcement on ID tokens with clock-injected validation.
Back-Channel LogoutJWT signature verified via cached JWKS; sid-based session invalidation.
Front-Channel LogoutGET handler with sid query parameter; shared SidMap with back-channel.
Plain-OAuth-2.0 social loginSocialProvider (gated on social, off by default) for IdPs that don't support OIDC (GitHub user login, Twitter/X, Discord, Reddit, Spotify, …). Weaker security model than OIDC; identity comes from a TLS-trusted userinfo endpoint, not from a signed assertion. Parallel types (SocialClaims vs IdTokenClaims) keep the distinction visible at the call site. PKCE on by default.
LocalIdpFixtureIn-process IdP minting workload JWTs against an in-memory RSA-2048 keypair + matching JWKS endpoint. RS256 + ES256, RFC 8414 discovery, multi-key rotation.

On-behalf-of (OBO)

FeatureNotes
delegated-storedRFC 6749 §4.1 Authorization Code + PKCE with persisted refresh token for long-lived offline access.
delegated-exchangeRFC 8693 Token Exchange for short-lived per-request exchange.
delegated-stored-encryptedEncryptedDelegatedCredentialStore<S, K> decorator wraps any delegated-credential backend with AES-256-GCM at rest.

Authorization

FeatureNotes
Cedar Policy engineRBAC + ABAC + ReBAC. AuthzStore orchestrates evaluation; ToCedarEntity bridges principals, resources, and contexts.
Layered policy bundleBase + overlay; adopters drop additional .cedar and .schema.cedar.json files into an overlay/ directory.
Procedural macrosrequire_authn!, require_partial_authn!, require_authz! guard handler functions at compile time.
Entity cachingEntityCache (in-process, default), MokaEntityCache, ValkeyEntityCache (cross-node). Asymmetric defaults: cache authz, not authn.

Middleware

FeatureNotes
CSRFSigned double-submit cookie; required for state-changing form posts.
Rate limitingToken-bucket via RateLimitLayer. KeyExtractor::{PeerIp, ClientIp} for direct vs proxied deployments; ClientIp reads what client_ip::layer resolved and no header.
Request IDX-Request-Id extraction + generation.
Trace IDW3C Trace Context (traceparent) propagation.
WebSocketRevocation-aware wrapper that closes connections on session invalidation.

Audit and observability

FeatureNotes
AuthEvent regulatory audit trailSix device-identity event variants + the full authn event surface.
AuthnMetrics17-method trait (counters + timers) with no-op defaults.
AuditArchiver + AuditRetentionPolicyHot / cold tiering with three-stage retention (90d / 7d / never defaults). FilesystemAuditArchiver reference impl behind audit-archive-fs.
AuthnAnalyticsSink + RichAuthnEventDenormalised analytics path parallel to the regulatory AuthEvent. serde + rkyv derives for Apache Iggy / ClickHouse / DuckDB / Snowflake.
TracingCaptureTest subscriber for asserting on emitted tracing events.

PII classification

Axess processes personal data as part of authentication. This section documents what the library logs, stores, and never touches; useful for GDPR Data Protection Impact Assessments and SOC2 evidence packages.

What axess logs (via tracing and AuthEvent)

FieldWherePurposePII?
user_idStructured log spans, AuthEventCorrelate events to accountsPseudonymous; opaque ID, not directly identifying
tenant_idStructured log spans, AuthEventMulti-tenant correlationNo
session_idAuthEvent, tracing spansSession correlationNo (random UUID)
IP addressAuditContext (extracted from headers)Geo/fraud detection, complianceYes; personal data under GDPR
User-AgentAuditContext, session bindingClient identification, hijack detectionIndirect; device fingerprint
event_typeAuthEventAudit trail (login, factor verified, logout)No
factor_kindAuthEventWhich factor was attemptedNo
success/failureAuthEventSecurity monitoringNo
request_idAuditContextRequest tracingNo

What axess stores in session data

FieldStoragePII?
user_id / tenant_idSession store (Memory / SQLite / Postgres / MySQL / Valkey)Pseudonymous
auth_stateSession storeNo
fingerprintSession store (HMAC hash)No (one-way hash)
customSession store (application-defined)Depends on application

What axess NEVER logs or stores

  • Plaintext passwords (only Argon2id hashes are stored; input is zeroized after hashing)
  • TOTP/HOTP secrets in logs (stored encrypted in FactorConfig, zeroized on drop)
  • Session cookie values
  • OAuth tokens (access, refresh, ID tokens); these stay in memory only during the exchange
  • PKCE verifiers, CSRF state tokens (cleared from session after use)

Recommendations

  • Encrypt at rest: pass a SessionCrypto::new(key) to the SQL session-store constructors (SqliteSessionStore::new(pool, crypto), same for Postgres / MySQL) or use ValkeySessionStore::new(client, key) so session data (which contains user_id) is AES-256-GCM protected. The explicit ::plaintext(pool) constructor opts out and is dev/test only.
  • Log retention: configure your log aggregator to retain auth events per your compliance requirements (MiFID II: 5 years; GDPR: minimize).
  • Right to erasure: deleting a user's sessions (SessionRegistry::invalidate_user) and database records satisfies GDPR erasure for axess-managed data. The custom session field is the application's responsibility.

Compliance framework mapping

GDPR

RequirementHow Axess addresses it
Lawful basis for processingApplication's responsibility. Axess processes only what the app sends.
Data minimizationSessions store only user_id, tenant_id, auth_state, and fingerprint (HMAC hash).
Right to erasureSessionRegistry::invalidate_user() + database record deletion.
Data protection by designAES-256-GCM encryption at rest, zeroization of secrets in memory.
Breach notificationApplication responsibility. Axess provides audit trail via AuthEvent.
DPA (Data Processing Agreement)Not applicable; Axess is a library, not a service.

SOC2

Trust service criteriaHow Axess addresses it
CC6.1; Logical access securityMFA, session binding, Cedar policy authorization
CC6.3; Access revocationSessionRegistry::invalidate_user(), session TTL
CC7.2; MonitoringAuthnMetrics trait (17 hooks), AuthEvent audit trail, tracing
CC8.1; Change managementApplication responsibility (CI/CD, version pinning)

PCI-DSS

RequirementHow Axess addresses it
8.3; MFA for admin accessMulti-factor chain support (password + TOTP/FIDO2)
8.6; Session managementSigned cookies, TTL, session binding, forced logout
3.4; Encryption of cardholder dataAES-256-GCM session encryption (session store, not card data)
10.2; Audit trailsAuthEvent records login attempts, factor verifications, logouts

HIPAA

SafeguardHow Axess addresses it
Access control (§164.312(a))MFA, Cedar RBAC/ABAC, session state machine
Audit controls (§164.312(b))AuthEvent audit trail with timestamps
Integrity controls (§164.312(c))HMAC-signed session cookies, AES-GCM encryption
Transmission security (§164.312(e))Application must terminate TLS; Axess sets Secure cookie flag

These mappings are informational. Compliance certification requires assessment of the complete application stack, not just the authentication library.

Supported Versions

We recommend using the latest release of Axess and actively maintained branches.

Disclaimer

Axess is provided as a library. While we strive for secure defaults, the overall security of your application depends on your usage and integration.

Further reading

Operations runbook covers the production-launch checklist (key rotation, multi-instance considerations, graceful shutdown). Audit events and Audit pipeline cover the audit mechanisms the compliance frames depend on. Migration guide covers cross-version upgrade paths, including security-relevant breaking changes.