KemalIdentity
Authentication for Crystal web applications.
The core is framework-agnostic: nothing under src/kemal_identity/core knows that
HTTP::Server::Context exists. Require kemal_identity/kemal for the Kemal adapter.
Constants
The audit trail.
docs/02-security-model.md lists what must reach a structured log — login success and
failure with reason, logout, session rotation, bulk revocation, rate-limit denials,
replay detection — and, more importantly, what must never: passwords, raw tokens of any
kind, session cookies, Authorization headers, password digests, Set-Cookie values.
Emitting through a named source rather than puts is what lets an application route
these somewhere durable, filter them by severity, or ship them to a SIEM without parsing
prose. src/CLAUDE.md bans puts for exactly this reason.
The login is deliberately absent
Events carry subject — the account id — and never the login that was typed. An email
address in a log file is a disclosure that outlives the request, gets copied into
aggregators, and is read by people who never authenticated to anything. The account id
identifies the account for anyone who can already query the database, which is the
audience an audit trail is for.
The cost is that a failed attempt against an unknown login records no identifier at
all, since there is no account to name. Detecting credential stuffing is the rate
limiter's job (RateLimiter, v0.1 step 8), which keys on the login without writing it
down.
Class methods
The configured application.
Raises ConfigurationError rather than returning nil: reaching this unconfigured means a
handler is about to authenticate a request against nothing, and that must be a loud
startup-shaped failure rather than a nil check at every call site.
Installs an already-built application. Useful when the application object is assembled elsewhere, and in specs.
Builds the application and installs it as the process-wide one.
Called once, at boot, before any request is served. The handlers reach for
KemalIdentity.app, so an application that never calls this gets a clear error rather
than a nil.
Whether an application has been installed. For a handler that wants to fail helpfully.
Sends this shard's security events to sink, in addition to wherever they already go.
KemalIdentity.event_sink = SiemSink.new
Binds an EventBridge to the kemal_identity source at :debug, so the sink sees every
event the catalogue in README.md lists rather than only the ones a severity filter let
through — a SIEM decides what is interesting, and authentication.succeeded at info is as
much a security event as a failure.
This calls ::Log.setup_from_env semantics on one source only, leaving whatever else the
application configured alone: ::Log.builder.bind adds a backend rather than replacing the
configuration. An application that has not configured Log at all still gets its own default
behaviour for everything outside kemal_identity.
Call this after your own Log setup, and check it
::Log.setup, ::Log.setup_from_env and anything that calls them replace the whole
configuration, binding included. So an application that wires a sink and then configures its
logging has no sink any more, with nothing raised and nothing logged: the feed OPS-02 exists
to keep alive goes quiet in the one way #failures cannot show, because no event ever
reaches the bridge to fail.
Measured, from a consumer project: a sink bound at the top of a spec file received zero
events, because Crystal's spec runner configures Log after the file loads. The same code in
a plain program received everything. blueprints/0025, OPS-03.
event_sink_delivering? is the check, and a boot that depends on the trail should assert it.
Returns the bridge, so #failures is reachable for an alarm.
Whether events are actually reaching the sink right now.
Emits one EventBridge::PROBE event and waits for it to arrive, up to within. Answers
false when a later ::Log.setup dropped the binding, which is the failure this exists for
and which nothing else can see: an unbound bridge is not a failing bridge, so #failures
stays at zero and the trail is simply empty.
Log.setup_from_env
KemalIdentity.event_sink = SiemSink.new
unless KemalIdentity.event_sink_delivering?
abort "the security event sink is not receiving events; check Log setup order"
end
Delivery is asynchronous — EventBridge is an :async backend so a slow SIEM cannot sit on
a request fiber — so this hands control to the dispatcher fiber rather than blocking. The
budget is a number of yields rather than a duration on purpose: it needs no clock (this
shard reads time only through an injected Clock), it cannot hang, and what is being waited
for is a fiber handoff rather than an interval. Measured: nothing had arrived after ten
yields and everything after fifty, so the default is twenty times what it took.
The sink sees one extra event per call, by design.
Nested types
- KemalIdentity::Accounts
- KemalIdentity::Anonymous
- KemalIdentity::ApiTokens
- KemalIdentity::Application
- KemalIdentity::AssuranceLevel
- KemalIdentity::Authenticated
- KemalIdentity::AuthenticationMethod
- KemalIdentity::AuthenticationResult
- KemalIdentity::AuthenticatorChain
- KemalIdentity::Authz
- KemalIdentity::CSRF
- KemalIdentity::CSRFConfig
- KemalIdentity::CSRFError
- KemalIdentity::Clock
- KemalIdentity::ConfigurationError
- KemalIdentity::CredentialKind
- KemalIdentity::CredentialRef
- KemalIdentity::Error
- KemalIdentity::EventBridge
- KemalIdentity::ExponentialBackoffRateLimiter
- KemalIdentity::FailOpenRateLimiter
- KemalIdentity::Failed
- KemalIdentity::FailureReason
- KemalIdentity::Federation
- KemalIdentity::FixedWindowRateLimiter
- KemalIdentity::ForbiddenError
- KemalIdentity::FreshAuthenticationRequiredError
- KemalIdentity::InfrastructureError
- KemalIdentity::JWT
- KemalIdentity::Kemal
- KemalIdentity::MFA
- KemalIdentity::NotAuthenticatedError
- KemalIdentity::NullRateLimiter
- KemalIdentity::OIDC
- KemalIdentity::OpaqueToken
- KemalIdentity::Outcome
- KemalIdentity::Passwords
- KemalIdentity::Postgres
- KemalIdentity::Principal
- KemalIdentity::RandomSource
- KemalIdentity::RateLimiter
- KemalIdentity::RequestAuthenticator
- KemalIdentity::SQLite
- KemalIdentity::Secret
- KemalIdentity::SecureRandomSource
- KemalIdentity::SecurityEvent
- KemalIdentity::SecurityEventSink
- KemalIdentity::Sessions
- KemalIdentity::StepUpRequirement
- KemalIdentity::SweepResult
- KemalIdentity::Sweeper
- KemalIdentity::SystemClock
- KemalIdentity::Testing
- KemalIdentity::Verdict