KemalIdentity::ApiTokens::Service
Inherits KemalIdentity::RequestAuthenticator < Reference < Object
Issues, resolves and revokes personal access tokens.
A RequestAuthenticator: it answers "who is making this request?" from the value after the
Bearer scheme, and returns the same Outcome union a session cookie does. Everything
downstream — require!, PathGuard, env.auth — works unchanged, because the whole point
of that union is that the credential type stops mattering once it has been resolved.
Opaque, and therefore revocable
docs/06-roadmap.md puts opaque tokens ahead of JWT deliberately. Validity is read from
storage on every request, so revoking a token takes effect on the next one. A stateless JWT
cannot make that promise without server-side state, at which point it is not stateless.
Constants
Prepended to every token, before the random part.
Not decoration. A fixed, searchable prefix is what lets secret scanners — GitHub's, a pre-commit hook, a log scrubber — recognise one of these in a commit or a paste and report it. A bare base64 blob is indistinguishable from any other base64 blob.
An application should change this to something identifying itself, so a scanner can tell whose token it found.
How stale last_used_at may get before a read is allowed to write.
The same throttle sessions use, for the same reason: without it every authenticated API request becomes a write, and an API's request rate is exactly where that hurts most. The cost is that "last used" is accurate only to within one interval, which is fine for the management screen it exists to feed.
Constructors
lifetime_policy is the deployment's rule about how long a token may live, and is
nil by default: an application that wants non-expiring deploy keys is not wrong, so
nothing is imposed. See LifetimePolicy, and note that it is checked at issuance only.
Instance methods
Resolves the value after the Bearer scheme.
The order of the checks below mirrors Sessions::Service#resolve, and for the same
reasons: shape before any I/O, then revocation, then expiry, then account status.
Returns Anonymous when nothing was presented and Failed when something was — a caller
needs that difference to decide between "this is a public endpoint" and "answer 401".
Brings a token's expiry forward only if it belongs to account_id.
Answers false for somebody else's token and for one that does not exist — the same
answer, for the reason the two-argument revoke gives: a token id is not secret material,
so the difference would tell a caller whether an id is real.
Brings a token's expiry forward, for a rotation that wants a bounded overlap.
Issue the replacement, then give the old credential a deadline:
replacement = api.issue(account, "deploy-key (rotated)", scopes: old.scopes)
api.expire(old.id, account.id, at: clock.now + 15.minutes)
Both credentials work until the deadline and each is separately auditable — two ids, two
last_used_at stamps, so "has the fleet picked up the new key" is a question the
management listing answers. After it, the old one fails as Expired on the
authentication path itself: the window closes whether or not a sweeper ran.
Never lengthens. Answers false for a token that already expires at or before at,
for a revoked one, and for one that does not exist. See Repository#expire.
By id alone, so it is an administrative call — the three-argument form below is what a route may hand a client-supplied id to.
Mints a token for an account. The raw value is returned exactly once and never again.
expires_at of nil means it never expires — a real choice for a deploy key and a poor
one for a laptop, so it is the caller's to make.
scopes of nil means the token is not attenuated: it carries whatever its owner holds,
which is what every token issued before v0.8 does. An empty array means it permits
nothing — valid, and not the same answer. There is no wildcard; unrestricted is nil.
Every token for an account, newest first, for a management screen. Revoked ones included: "when did I revoke that?" is a question such a screen exists to answer.
Ends one token only if it belongs to account_id, which is what a "revoke this token"
button in a user's own settings needs.
Answers false for a token that exists and belongs to somebody else, and for one that does
not exist at all — the same answer, so a caller cannot use it to discover whether an id is
real. Costs one extra read; this is a management action, not a request-path one.
Ends one token. Takes effect on the very next request, because validity is read from storage rather than asserted by a signature.
Revokes by id alone, so it is an administrative call. A route that lets a client name
the token to revoke — DELETE /tokens/:id — must use the two-argument form below, or it
will happily end a token belonging to somebody else. A token id is not secret material:
it appears in api_token.revoked and api_token.issued audit lines, in a management
listing, and in whatever an operator exports from either.