Class IdempotencyKey

java.lang.Object
org.apache.juneau.console.views.IdempotencyKey

public final class IdempotencyKey extends Object
A server-minted idempotency key for a row-action submit, bound at mint time to the (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):

// At modal-open: mint a key bound to this action + this row. IdempotencyKey key = IdempotencyKey.mint("ack", incidentId); modal.idempotencyKey(key.value()); // At submit: rehydrate the recorded binding and check it against the submitted (action, targetId). if (! recorded.matches(submittedAction, submittedTargetId)) return ActionResult.refusal("write-guard:idempotency-binding-mismatch");

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 its selfTargeted flag — 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 with selfTargeted — that pairing makes the client stop sending the real target the key was bound to, silently discarding the artifact identity and making matches(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:

// At modal-open on a ribbon-hosted dialog: there is no row, so the key targets itself. IdempotencyKey key = IdempotencyKey.mintSelfTargeted("create"); modal.idempotencyKey(key.value()).selfTargeted(true); // At submit: the submitted target IS the key value the client echoed back. if (! recorded.matches(submittedAction, submittedTargetId)) return ActionResult.refusal("write-guard:idempotency-binding-mismatch");

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 Details

    • mint

      public static IdempotencyKey mint(String action, String targetId)
      Mints a fresh key bound to the specified (action, targetId) pair.
      Parameters:
      action - The stable action id this key is issued for. Must not be null or blank.
      targetId - The id of the row/target this key is issued for. Must not be null or blank.
      Returns:
      A new key holding a fresh 256-bit secret, hex-encoded, bound to (action, targetId).
      Throws:
      IllegalArgumentException - If action or targetId is null or blank.
    • mintSelfTargeted

      public static IdempotencyKey mintSelfTargeted(String action)
      Mints a fresh key that is bound to itself: its targetId() is its own value().

      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 selfTargeted flag, or the client will not send the key's value back as the submitted target and the binding can never match. Conversely, selfTargeted must never be set on a modal carrying a mint(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) and matches(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 be null or blank.
      Returns:
      A new key holding a fresh 256-bit secret, hex-encoded, bound to that same value as its target.
      Throws:
      IllegalArgumentException - If action is null or blank.
    • of

      public static IdempotencyKey of(String value, String action, String targetId)
      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 to check it against the submitted pair.

      Parameters:
      value - The recorded key value. Must not be null or blank.
      action - The action id the key was bound to at mint time. Must not be null or blank.
      targetId - The target id the key was bound to at mint time. Must not be null or blank.
      Returns:
      A key holding the recorded value and binding.
      Throws:
      IllegalArgumentException - If any argument is null or blank.
    • value

      public String 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

      public String action()
      The action id this key is bound to.
      Returns:
      The bound action id.
    • targetId

      public String targetId()
      The target id this key is bound to.
      Returns:
      The bound target id.
    • matches

      public boolean matches(String submittedAction, String submittedTargetId)
      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 be null.
      submittedTargetId - The target id presented by the submit. Can be null.
      Returns:
      true only if both the action and the target id match this key's binding exactly.
    • toString

      public String 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.

      Overrides:
      toString in class Object
      Returns:
      A value-free description.