class

KemalIdentity::Accounts::Service

Inherits Reference < Object

The account lifecycle flows that surround a password: reset it, and prove an address.

Both are built from the same two pieces -- a single-use ActionToken and a Notifier -- and both are shaped by the same constraint: asking must reveal nothing.

Constructors

new(accounts : Repository, tokens : ActionTokenRepository, notifier : Notifier, sessions : Sessions::Service, hasher : Passwords::Hasher, policy : Passwords::Policy, clock : Clock, random : RandomSource, rate_limiter : RateLimiter = NullRateLimiter.new, remember : Sessions::RememberService | Nil = nil, reset_ttl : Time::Span = 1.hour, confirmation_ttl : Time::Span = 1.day)
Source

Instance methods

confirm_email(raw_token : String) : ActionOutcome

Spends a confirmation link.

Deliberately does not touch sessions or auth_version. Proving an address is not a credential change: nobody's password altered, so logging everybody out would be a surprise with no security benefit.

Source
request_email_confirmation(account_id : String) : Bool

Sends a confirmation link for an account that already exists.

Takes an account id rather than a login: confirmation is triggered by the application after it creates an account or changes an address, so there is no untrusted identifier to enumerate with, and no reason to be silent about an unknown one.

Source
request_password_reset(login : String, tenant_id : String | Nil = nil, ip : String | Nil = nil) : Nil

Starts a password reset, if there is an account to start one for.

Returns nothing, always, and takes the same time either way. The caller cannot tell whether the address exists, and neither can whoever is watching the response -- which is the whole point, because a forgot-password form is the easiest place in an application to enumerate a customer list.

What is equalised here is everything this method controls: a token is minted and digested on both paths, and the branch that has no account throws its token away. What is not controlled here is Notifier#deliver, which is why its contract says it must return promptly -- an implementation that waits on an SMTP server puts a network round trip on one path and not the other, and hands the oracle back.

Rate limited per address, so the endpoint cannot be turned into a way to flood somebody's inbox. A denial is silent, for the same reason everything else here is.

Source
reset_password(raw_token : String, new_password : String) : ActionOutcome

Spends a reset link and sets a new password.

The token is consumed before the password is checked against the policy, and stays consumed even when the policy then rejects it -- docs/02-security-model.md, token rule five. Leaving it spendable would turn one emailed link into unlimited attempts; the user simply asks for another link, which costs them one email and costs an attacker a foothold.

Source