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:
| Class | What | Where | Retention |
|---|---|---|---|
| Primary | Identifier (email, username), password hash, TOTP secret, FIDO2 credentials, the IP seen at authentication, device fingerprint | Identity store, device store | Yours to choose, within whatever regulatory bounds apply |
| Secondary | The audit-event log, which reaches the primary through user_id, tenant_id, device_id and client_ip | Audit store | Audit pipeline. The usual GDPR pattern keeps it longer than primary PII but scrubs or hashes the IPs once the hot window closes |
| Pseudonymous | Session id, refresh-token hash, device id (a UUID that names no user) | Session store, device store | Longer 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:
| Frame | What axess gives you | What 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 thresholds | Data 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 metrics | Policy and procedure documentation |
| PCI-DSS (card data) | Strong authentication for administrative access, audit retention of at least one year, session data encrypted at rest | The 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 transit | The 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
CsrfLayeron 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-siteGET-triggered mutations; keepCsrfLayerengaged. -
For API-only endpoints, validate
Origin/Refererheaders 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::layerwith aTrustedProxiesset naming your proxies, and readClientIpwherever 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-ForandX-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 returningAuditOutcome::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
IdentityStorecachesfind_useroraccount_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 viaSqliteSessionStore::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-Policyheader on all HTML responses to mitigate XSS impact. At minimum:default-src 'self'; script-src 'self'; style-src 'self'. -
Avoid
unsafe-inlineandunsafe-evalin 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_accessunless 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
SocialProvideronly 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_pkceis the opt-out and should be used sparingly. -
Verify
csrf_stateecho on the callback before callingexchange_code;SocialProvider::mint_csrf_stateproduces 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 expectedTrustDomainand rejects tokens / certs whose synthesisedWorkloadIdlives 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 whichsubjectpaths the application admits. The recipes inexamples/workload-identity/are templates, not policy. -
When fetching SVIDs from a local SPIRE agent, use the
spire-workloadcrate today; seedocs/workload-identity/jwt-svid.mdfor the fetch-side recipe.
Dependencies
-
Regularly update Axess and its dependencies (
cargo update). -
Run
cargo auditin 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:
- Strip incoming
X-Forwarded-ForandX-Real-IPfrom client requests. - Set
X-Real-IPto the immediate client address (TCP peer). - Append
X-Forwarded-Forwith 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):
- Returns the TCP peer immediately if the peer is not itself a trusted proxy: an unproxied client's headers are never read.
- Otherwise joins every
X-Forwarded-Forfield 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. - Stops at a malformed entry and returns the peer, rather than trusting past it.
- Reads
X-Real-IPonly when noX-Forwarded-Foris 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
| Feature | Notes |
|---|---|
| Password | Argon2id 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 OTP | 8-digit default, Argon2-hashed codes at rest, TTL-bound, single-use. |
| FIDO2 / WebAuthn | Registration + authentication + clone detection + discoverable / passwordless. Per-ceremony UV / attestation policy waits on webauthn-rs 0.6 stable; see ROADMAP.md. |
| LDAP bind | Verifier over ldap3 with TLS via rustls. Bind only; schema mapping is the application's responsibility. |
| mTLS factor | X.509 certificate verification against a configured trust anchor; SAN URI extraction for SPIFFE / regular identity binding. |
| JWT bearer | Generic JWT verifier with JWKS rotation, iss / aud / exp / nbf / alg allowlist, clock-injected for DST. |
| Multi-factor chains | Ordered factor pipeline (FactorStep::AnyOf for choice steps), session state machine enforces sequencing at compile time. |
Sessions
| Feature | Notes |
|---|---|
| Session cookies | HMAC-SHA256 signed, Secure; HttpOnly; SameSite=Lax by default. Configurable via SessionLayer::with_secure / with_same_site. |
| Session binding | HMAC-keyed fingerprint (not a plain hash); UserAgentBinding + extension points for IP / TLS channel binding. |
| Session registry + forced logout | SessionRegistry::invalidate_user is error-observable and fail-closed; cooperates with suspend_user for combined identity + session revocation. |
| ID cycling | Automatic 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 tokens | Rotation with family revocation on reuse; integration with device-binding cascade. |
| In-memory store | Testing only; no persistence, no encryption, non-constant-time lookups. |
| SQLite session store | Optional AES-256-GCM via SqliteSessionStore::new(pool, SessionCrypto::new(key)). Key rotation via with_previous_key. |
| Postgres session store | Same encryption model. Recommended for multi-instance deployments. Validated against CockroachDB via the cockroach_compat CI job. |
| MySQL / MariaDB session store | Same encryption model. Tested against MySQL 8.x and MariaDB 10.5+. |
| Valkey session store | AES-256-GCM, key rotation, TTL-managed eviction. |
Cross-backend Store<K, V> trait | All five backends implement it for adopters that want backend-agnostic dispatch via Arc<dyn Store<…>>. |
Device identity
| Feature | Notes |
|---|---|
| Three-stage trust ladder | Unknown → Seen → Trusted (plus terminal Revoked); retention sweep demotes idle devices and purges revoked rows past the grace window. |
| Per-tenant fingerprint pepper | Stops cross-tenant fingerprint correlation; TenantPepperResolver is adopter-provided. |
| Cascade revocation | Refresh-token family compromise revokes every device that carried that family's binding. |
CachedDeviceStore decorator | LRU + clock-driven TTL eviction; revocation propagates through set_trust_level. |
Five DeviceStore backends | Memory, 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 recipe | Documented contracts (tenant scoping, atomic save, hot-path sighting, required sweep) in docs/identity/device.md for adopters with non-shipped backends (DynamoDB, MongoDB, …). |
| PII tokenisation | MemoryDevicePiiStore reference impl + adopter trait for the GDPR-scoped fields (label, last-seen IP). |
Workload identity
| Feature | Notes |
|---|---|
Principal::{Human, Workload} unified abstraction | Same ToCedarEntity bridge for both shapes; Cedar policies authorise across without branching. |
JwtSvidResolver | SPIFFE JWT-SVID spec adherence; mandatory spiffe:// URI in sub, trust-domain extracted and pinned. |
MtlsResolver | SPIFFE 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 exchange | aws-sts, gcp-wif, azure-fic adapters for exchanging federated workload identity for short-lived cloud credentials. |
| Outbound identity | outbound-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
| Feature | Notes |
|---|---|
| Authorization Code + PKCE | Discovery, token exchange, nonce validation; HTTPS-enforced (localhost / 127.0.0.1 / [::1] exemption for dev). |
| Client Credentials | Real HTTP token exchange via OAuthProviderConfig. |
| Device Code (RFC 8628) | Real HTTP; device endpoint configured via with_device_authorization_endpoint. Nonce-bindable per RFC. |
| Token refresh | Provider-delegated refresh with audit logging. |
| FAPI 2.0 Baseline Profile | Pushed Authorization Requests (PAR, RFC 9126), DPoP (RFC 9449), JARM, RP-initiated logout. Strict nbf enforcement on ID tokens with clock-injected validation. |
| Back-Channel Logout | JWT signature verified via cached JWKS; sid-based session invalidation. |
| Front-Channel Logout | GET handler with sid query parameter; shared SidMap with back-channel. |
| Plain-OAuth-2.0 social login | SocialProvider (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. |
| LocalIdpFixture | In-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)
| Feature | Notes |
|---|---|
delegated-stored | RFC 6749 §4.1 Authorization Code + PKCE with persisted refresh token for long-lived offline access. |
delegated-exchange | RFC 8693 Token Exchange for short-lived per-request exchange. |
delegated-stored-encrypted | EncryptedDelegatedCredentialStore<S, K> decorator wraps any delegated-credential backend with AES-256-GCM at rest. |
Authorization
| Feature | Notes |
|---|---|
| Cedar Policy engine | RBAC + ABAC + ReBAC. AuthzStore orchestrates evaluation; ToCedarEntity bridges principals, resources, and contexts. |
| Layered policy bundle | Base + overlay; adopters drop additional .cedar and .schema.cedar.json files into an overlay/ directory. |
| Procedural macros | require_authn!, require_partial_authn!, require_authz! guard handler functions at compile time. |
| Entity caching | EntityCache (in-process, default), MokaEntityCache, ValkeyEntityCache (cross-node). Asymmetric defaults: cache authz, not authn. |
Middleware
| Feature | Notes |
|---|---|
| CSRF | Signed double-submit cookie; required for state-changing form posts. |
| Rate limiting | Token-bucket via RateLimitLayer. KeyExtractor::{PeerIp, ClientIp} for direct vs proxied deployments; ClientIp reads what client_ip::layer resolved and no header. |
| Request ID | X-Request-Id extraction + generation. |
| Trace ID | W3C Trace Context (traceparent) propagation. |
| WebSocket | Revocation-aware wrapper that closes connections on session invalidation. |
Audit and observability
| Feature | Notes |
|---|---|
AuthEvent regulatory audit trail | Six device-identity event variants + the full authn event surface. |
AuthnMetrics | 17-method trait (counters + timers) with no-op defaults. |
AuditArchiver + AuditRetentionPolicy | Hot / cold tiering with three-stage retention (90d / 7d / never defaults). FilesystemAuditArchiver reference impl behind audit-archive-fs. |
AuthnAnalyticsSink + RichAuthnEvent | Denormalised analytics path parallel to the regulatory AuthEvent. serde + rkyv derives for Apache Iggy / ClickHouse / DuckDB / Snowflake. |
TracingCapture | Test 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)
| Field | Where | Purpose | PII? |
|---|---|---|---|
user_id | Structured log spans, AuthEvent | Correlate events to accounts | Pseudonymous; opaque ID, not directly identifying |
tenant_id | Structured log spans, AuthEvent | Multi-tenant correlation | No |
session_id | AuthEvent, tracing spans | Session correlation | No (random UUID) |
IP address | AuditContext (extracted from headers) | Geo/fraud detection, compliance | Yes; personal data under GDPR |
User-Agent | AuditContext, session binding | Client identification, hijack detection | Indirect; device fingerprint |
event_type | AuthEvent | Audit trail (login, factor verified, logout) | No |
factor_kind | AuthEvent | Which factor was attempted | No |
success/failure | AuthEvent | Security monitoring | No |
request_id | AuditContext | Request tracing | No |
What axess stores in session data
| Field | Storage | PII? |
|---|---|---|
user_id / tenant_id | Session store (Memory / SQLite / Postgres / MySQL / Valkey) | Pseudonymous |
auth_state | Session store | No |
fingerprint | Session store (HMAC hash) | No (one-way hash) |
custom | Session 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 useValkeySessionStore::new(client, key)so session data (which containsuser_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. Thecustomsession field is the application's responsibility.
Compliance framework mapping
GDPR
| Requirement | How Axess addresses it |
|---|---|
| Lawful basis for processing | Application's responsibility. Axess processes only what the app sends. |
| Data minimization | Sessions store only user_id, tenant_id, auth_state, and fingerprint (HMAC hash). |
| Right to erasure | SessionRegistry::invalidate_user() + database record deletion. |
| Data protection by design | AES-256-GCM encryption at rest, zeroization of secrets in memory. |
| Breach notification | Application responsibility. Axess provides audit trail via AuthEvent. |
| DPA (Data Processing Agreement) | Not applicable; Axess is a library, not a service. |
SOC2
| Trust service criteria | How Axess addresses it |
|---|---|
| CC6.1; Logical access security | MFA, session binding, Cedar policy authorization |
| CC6.3; Access revocation | SessionRegistry::invalidate_user(), session TTL |
| CC7.2; Monitoring | AuthnMetrics trait (17 hooks), AuthEvent audit trail, tracing |
| CC8.1; Change management | Application responsibility (CI/CD, version pinning) |
PCI-DSS
| Requirement | How Axess addresses it |
|---|---|
| 8.3; MFA for admin access | Multi-factor chain support (password + TOTP/FIDO2) |
| 8.6; Session management | Signed cookies, TTL, session binding, forced logout |
| 3.4; Encryption of cardholder data | AES-256-GCM session encryption (session store, not card data) |
| 10.2; Audit trails | AuthEvent records login attempts, factor verifications, logouts |
HIPAA
| Safeguard | How 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.