Class IdempotencyKey
(action, targetId) pair
it was issued for.
Why the key is server-minted and bound, not a client UUID
The key is minted when the modal opens — the server is already in the loop then, since the modal-open confirmation is server-rendered against the current record — so a double-click, an impatient re-submit and a browser retry all carry the same key and collapse to one effect. The modal owns the key's carry lifecycle; it does not mint it.
A client-generated key is not the security property. A globally-unique key looked up only by key would let
a recorded terminal outcome for incident A be replayed under a submit for incident B — returning
A's "success" for a write B never performed, and resurrecting the "resolve an incident that was re-triggered in the
interim" case. This key is therefore bound to (action, targetId) at mint time: a submit whose
(action, targetId) does not match the binding is a named refusal,
never a replayed success.
Unforgeability
The key value is 32 random bytes (256 bits) from SecureRandom,
hex-encoded — well beyond the ≥128-bit bar, matching SynchronizerToken's
256-bit reference. The binding is what makes it safe; the width is what makes it unguessable.
Example — the artifact-bound pattern (the default):
The row-less, self-targeted pattern (the opt-in)
A dialog opened from a view's ribbon has no row, so at mint time there is no artifact id to bind to.
mintSelfTargeted(String) mints a key whose targetId() is its own value(), and
the client sends that value back as the submitted target — so matches(String, String) still has a
real, agreed binding to check rather than a null target.
Two call sites have to honour one pairing contract, and it is not enforceable by either alone:
- A
mintSelfTargeted(String)key MUST be carried by a modal that also sets itsselfTargetedflag — that flag is what tells the client to send the key's own value as the target. Without it the client sends the row's id (or nothing at all, row-less), and the key's binding can never match. - A
mint(String, String)key must NEVER be paired withselfTargeted— that pairing makes the client stop sending the real target the key was bound to, silently discarding the artifact identity and makingmatches(String, String)vacuous. This is exactly why the flag is an explicit per-dialog opt-in rather than a blanket client-side precedence rule.
Example — the self-targeted pattern:
What a self-targeted key does and does not guarantee
A self-targeted key binds to (action, one-opened-dialog) — not (action, artifact).
It is minted per modal-open, and it is single-execution, replay-on-duplicate: it stops one opened dialog's
submission from being re-executed, and a duplicate submit replays the recorded outcome rather than
failing. It is not "single-use," and it does not answer "is this submission for the artifact the key
was minted for?" — at mint time there is no artifact.
A self-targeted key must therefore never be described as equivalent to a row-scoped one. A key minted via
the unchanged mint(String, String) binds to a real artifact identity, and that guarantee is strictly
stronger; it stays available to any consumer that has an artifact to bind to.
See Also:
- Since:
- 10.0.0
-
Method Summary
Modifier and TypeMethodDescriptionaction()The action id this key is bound to.booleanWhether a submitted(action, targetId)matches this key's binding.static IdempotencyKeyMints a fresh key bound to the specified(action, targetId)pair.static IdempotencyKeymintSelfTargeted(String action) Mints a fresh key that is bound to itself: itstargetId()is its ownvalue().static IdempotencyKeyRehydrates a previously-minted key from its recorded value and binding.targetId()The target id this key is bound to.toString()Returns a description that does not include the key value.value()The opaque key value, for carrying in the modal and echoing back on the submit.
-
Method Details
-
mint
Mints a fresh key bound to the specified(action, targetId)pair.- Parameters:
action- The stable action id this key is issued for. Must not benull or blank.targetId- The id of the row/target this key is issued for. Must not benull or blank.- Returns:
- A new key holding a fresh 256-bit secret, hex-encoded, bound to
(action, targetId). - Throws:
IllegalArgumentException- IfactionortargetIdisnull or blank.
-
mintSelfTargeted
Mints a fresh key that is bound to itself: itstargetId()is its ownvalue().For a dialog opened where there is no artifact to bind to — a ribbon-hosted, row-less dialog whose submit creates the thing it is about.
mint(String, String)cannot express this, since it requires a non-blank target that does not exist yet, and there is nothing gained by minting a second random string to serve as the target: one opaque, unguessable value plays both roles, and the client already has it as the modal's carried key.The pairing contract is not optional — a key from this factory must be carried by a modal that also sets its
selfTargetedflag, or the client will not send the key's value back as the submitted target and the binding can never match. Conversely,selfTargetedmust never be set on a modal carrying amint(String, String)key. See the class Javadoc, which also states what this key does and does not guarantee (single-execution replay protection for one opened dialog — not artifact identity).mint(String, String),of(String, String, String)andmatches(String, String)are unchanged by this capability: it arrives entirely as this additional factory.- Parameters:
action- The stable action id this key is issued for. Must not benull or blank.- Returns:
- A new key holding a fresh 256-bit secret, hex-encoded, bound to that same value as its target.
- Throws:
IllegalArgumentException- Ifactionisnull or blank.
-
of
Rehydrates a previously-minted key from its recorded value and binding.Intended for the submit path: the consumer looks up the recorded
(value, action, targetId)it minted at modal-open and reconstructs this tocheckit against the submitted pair.- Parameters:
value- The recorded key value. Must not benull or blank.action- The action id the key was bound to at mint time. Must not benull or blank.targetId- The target id the key was bound to at mint time. Must not benull or blank.- Returns:
- A key holding the recorded value and binding.
- Throws:
IllegalArgumentException- If any argument isnull or blank.
-
value
The opaque key value, for carrying in the modal and echoing back on the submit.- Returns:
- The key value. Never
null or blank.
-
action
The action id this key is bound to.- Returns:
- The bound action id.
-
targetId
The target id this key is bound to.- Returns:
- The bound target id.
-
matches
Whether a submitted(action, targetId)matches this key's binding.A mismatch must be answered with a named refusal, never a replayed success: it is a key being submitted against a different action or a different target than it was minted for. Compares the target id in constant time so a mismatch cannot be probed character-by-character.
- Parameters:
submittedAction- The action id presented by the submit. Can benull .submittedTargetId- The target id presented by the submit. Can benull .- Returns:
true only if both the action and the target id match this key's binding exactly.
-
toString
Returns a description that does not include the key value.The value is a secret; a bean-dumping logger or a debug view that stringifies it would otherwise write it somewhere it can be read back and replayed. The binding is safe to show.
-