The ceremony of SPEC §6, in three acts: issue a challenge against an identifier, redeem it at most once, end a subject's access everywhere. The method — magic links by email — is implementation one and not the contract; a password, a passkey or a single-use code would arrive as another implementation of these same three calls.
Two properties of the code below are load-bearing and easy to lose in an innocent refactor:
issue! never asks whether the identifier is known. Not once. That is
not an optimisation of the anti-enumeration rule (SPEC §11), it is the whole
of it: there is no branch to time, because the question is only asked at
redemption, when the answer is already in the hands of whoever holds the
secret. A future edit that consults the store here to 'avoid pointless work'
reintroduces exactly the defect.
A delivery failure is not an authentication failure (SPEC §8). It cannot reach the caller, because reaching the caller means telling them something about the address; it goes to the operator instead.
The ceremony of SPEC §6, in three acts: issue a challenge against an identifier, redeem it at most once, end a subject's access everywhere. The method — magic links by email — is implementation one and not the contract; a password, a passkey or a single-use code would arrive as another implementation of these same three calls. Two properties of the code below are load-bearing and easy to lose in an innocent refactor: **`issue!` never asks whether the identifier is known.** Not once. That is not an optimisation of the anti-enumeration rule (SPEC §11), it is the whole of it: there is no branch to time, because the question is only asked at redemption, when the answer is already in the hands of whoever holds the secret. A future edit that consults the store here to 'avoid pointless work' reintroduces exactly the defect. **A delivery failure is not an authentication failure** (SPEC §8). It cannot reach the caller, because reaching the caller means telling them something about the address; it goes to the operator instead.
(ceremony {:keys [store deliver! link ttl-ms clock bootstrap normalise]
:as config})Validates the host's configuration and returns the value the three acts take. Every failure is raised here, at construction, naming the key — a malformed link discovered on the first login is a defect found by a user.
:store an implementation of store/Store (required)
:deliver! (fn [identifier link]) — gets it to the human (required)
:link {:base-url "https://host" :redeem-path "/entrar"} (required)
:ttl-ms how long a challenge lives (default 15 minutes)
:clock (fn []) → epoch milliseconds (default the system clock)
:bootstrap identifiers that hold no record and may still enter (SPEC §12)
:normalise (fn [identifier]) → the canonical form (default trim + lower-case)
Validates the host's configuration and returns the value the three acts
take. Every failure is raised here, at construction, naming the key — a
malformed link discovered on the first login is a defect found by a user.
:store an implementation of `store/Store` (required)
:deliver! (fn [identifier link]) — gets it to the human (required)
:link {:base-url "https://host" :redeem-path "/entrar"} (required)
:ttl-ms how long a challenge lives (default 15 minutes)
:clock (fn []) → epoch milliseconds (default the system clock)
:bootstrap identifiers that hold no record and may still enter (SPEC §12)
:normalise (fn [identifier]) → the canonical form (default trim + lower-case)(generation {:keys [store]} subject)The subject's current revocation generation, to be carried by the session it is about to establish.
The subject's current revocation generation, to be carried by the session it is about to establish.
(issue! {:keys [store deliver! link ttl-ms clock normalise]} identifier)Records a challenge for identifier and hands the link to deliver!.
Returns nil. Always, whatever the identifier is, and that is the point: a return value that distinguished a known address from an unknown one would put the enumeration back one layer up, in the handler that reads it.
Records a challenge for `identifier` and hands the link to `deliver!`. Returns nil. Always, whatever the identifier is, and that is the point: a return value that distinguished a known address from an unknown one would put the enumeration back one layer up, in the handler that reads it.
(redeem! {:keys [store clock] :as ceremony} token)Spends token and returns the subject it attests, or nil.
The challenge is consumed before it is judged: an expired token is spent by the attempt that found it expired, so there is no second look at it.
Spends `token` and returns the subject it attests, or nil. The challenge is consumed before it is judged: an expired token is spent by the attempt that found it expired, so there is no second look at it.
(revoke! {:keys [store]} subject)Ends the subject's access everywhere (SPEC §10). Every session of theirs carries the generation it was born with and dies at its next request; there is no enumeration of sessions to do, and none is possible over Ring's store protocol, which is why revocation lives on the subject.
Ends the subject's access everywhere (SPEC §10). Every session of theirs carries the generation it was born with and dies at its next request; there is no enumeration of sessions to do, and none is possible over Ring's store protocol, which is why revocation lives on the subject.
(subject-of {:keys [store bootstrap normalise]} identifier)The subject behind an identifier: the store's record, or — only when there is none — a bootstrap identity (SPEC §12). The absence of a record is the signal, not the emptiness of the table, which works once for the first administrator and never again. No row is created either way.
A bootstrap subject carries its identifier and says what it is, so that an audit trail has something to name when there is no local identity at all.
The subject behind an identifier: the store's record, or — **only when there is none** — a bootstrap identity (SPEC §12). The absence of a record is the signal, not the emptiness of the table, which works once for the first administrator and never again. No row is created either way. A bootstrap subject carries its identifier and says what it is, so that an audit trail has something to name when there is no local identity at all.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |