class

KemalIdentity::Passwords::Authenticator

Inherits Reference < Object

Verifies a login and a password, and answers who — if anyone — that proves.

A CredentialAuthenticator, not a RequestAuthenticator: it runs at login only, takes an identifier and a secret, and establishes an identity rather than reading one that was already established. Passport.js is the cautionary example of forcing both into one abstraction (docs/01-architecture.md).

It does not create a session. It returns a Principal with no session_id, and the caller passes the account to Sessions::Service#start, which is what makes rotation on login — the session fixation defence — the session layer's decision rather than this one's.

Two ways this class avoids being an account oracle

Every failure is one failure. reason distinguishes them for the audit log; the response must not. DisabledAccount and InvalidCredential rendering differently tells an attacker which logins exist (docs/04-kemal-integration.md).

Every path costs the same. The naive implementation returns early when no account matches, having done no hashing, and the response arrives a hundred-odd milliseconds sooner — a reliable oracle no matter how identical the body is. So an unknown login is verified against Hasher#dummy_digest, and the disabled check happens after the verification rather than in front of it, so that disabled accounts are not distinguishable by timing either.

Constructors

new(accounts : Accounts::Repository, hasher : Hasher, clock : Clock, rate_limiter : RateLimiter = NullRateLimiter.new)
Source

Instance methods

authenticate(login : String, password : String, tenant_id : String | Nil = nil, ip : String | Nil = nil) : Outcome

Verifies password against the account identified by login.

ip keys the source-address half of the rate limit, and is recorded in the audit event.

It must be the client's address. Behind a reverse proxy, remote_address is the proxy, so every request in the deployment shares one address key and the first attacker to reach the limit denies logins to everybody — the limiter becomes an availability problem rather than a defence. An application terminating behind one derives this from its own trusted-proxy configuration, counting from the right of X-Forwarded-For by the number of proxies it actually operates. Never the leftmost value and never the whole header: a client picks those, so a spoofed one mints a fresh allowance per request.

nil when there is genuinely no address to attribute — a request off a unix socket, an internal caller. That consumes the login key alone, which is the honest reading rather than a placeholder every caller would share.

Source