skip to content

Which checks on an inbound SAML response must you configure yourself, and which does an integration library leave off by default?

level: middleimportance: must knowfreq 50%

answer

  1. the library checks maths, not meaning
  2. what is this message bound to
  3. an empty expected audience accepts everyone
  4. correlate only what you stored
  5. read the element verification returned

basics

~20 s

A library verifies the signature; the bindings to you are yours to configure. Set the expected audience to your own entityID, require the recipient and destination to be your endpoint, bound the validity window with an explicit skew, correlate InResponseTo against a stored request id, and process only the element verification returned.

solid answer

~50 s

An integration library will check a signature against a key I give it. What it cannot decide for me is what the message is supposed to be bound to, and those settings are the ones that are commonly empty. The expected audience has to be my own `entityID`; an unset expected-audience list means an assertion minted for someone else verifies fine and is accepted. The `Recipient` on `SubjectConfirmationData` and `Destination` on the response have to equal the endpoint that actually received the post. The `Conditions` window needs a skew allowance I chose rather than one I inherited. `InResponseTo` can only be checked if I stored the request id I generated, in storage every replica can read — otherwise the check silently passes as not-applicable. And I process the element the verification step handed back, never one I re-select from the parsed document afterwards.

code

pseudocode · 18 lines
pseudocode
validate(doc, connection, receiving_url, now):
    require(doc.status == "Success")

    covered = verify_signature(doc, connection.accepted_signing_certs)
    require(covered is not none)          # covered is the ELEMENT, not a boolean

    require(covered.issuer == connection.issuer_entity_id)
    require(connection.expected_audience in covered.audiences)   # our own entityID
    require(covered.subject_confirmation.recipient == receiving_url)
    require(doc.destination == receiving_url)

    require(now >= covered.not_before - SKEW)
    require(now <  covered.window_end  + SKEW)

    if doc.in_response_to is not none:
        require(request_store.consume(doc.in_response_to) is not none)

    return covered        # every later read comes from THIS object

go deeper

for a junior

Know that verifying a signature only proves who wrote the message, not who it was written for. A separate set of checks ties it to your service and your endpoint, and those come from your configuration.

for a middle

Name the bindings and what each one prevents: audience ties the message to your service, recipient and destination to this endpoint, the window to a moment, correlation to a sign-in you started. Explain why an unset expected audience is silent.

for a senior

Demonstrate the code-level rule that every later read comes from the element verification returned, and show that the correlation check implies a shared, expiring request-id store you had to build, size and monitor.

for a principal

Decide which of these are non-negotiable across every counterparty and which may be relaxed per connection, and put the machinery in place — ownership, expiry, review — so a relaxation granted during onboarding does not become a permanent default.

## The line between what the library does and what you decide An integration library takes bytes, finds a signature, and tells you whether it verifies against a key you supplied. That is genuinely hard cryptography and you should not write it. Everything past it is policy about **your** deployment, and a library cannot know it: which counterparty this connection is, what your service is called, which URL the message was supposed to land on, how much clock difference you will tolerate, and whether you asked for this sign-in at all. Those arrive as configuration, and the failure mode of this leaf is that several of them have a permissive empty value that looks like a working integration on day one. In the water authority's permit system, each contractor firm is a separate connection with a separate key and a separate set of these values. A check that is off is off for every firm at once. ## The checks, and what each one binds | Check | What it binds the message to | What its absence permits | |---|---|---| | Signature against **this connection's** configured certificate | the firm that is supposed to have issued it | any key your trust material happens to contain is accepted | | Expected audience equals your own `entityID` | your service | a message minted for a different service provider verifies and is accepted | | `Recipient` on `SubjectConfirmationData` equals the receiving URL | this endpoint | a message captured against another endpoint can be posted to this one | | `Destination` on the response equals the receiving URL | this endpoint | the same, by the other attribute | | `Conditions` window, with a skew you chose | a bounded moment | an old message stays usable for longer than anyone intended | | `InResponseTo` against a request id you stored | a sign-in this browser actually started | you cannot say which of your own requests, if any, this answers | | Status is success before anything is read as a subject | a completed authentication | a failure message is mined for attributes as though it were a success | The audience check is the one that names you, and the one nothing else substitutes for. Recipient and destination are about *which of your endpoints*; audience is about *whether it is you at all*. ## Consume what verification returned, not what you re-found The most expensive mistake in this area is structural rather than configurational. Verification proves that a signature covers a particular element in a particular document. If your code then walks the parsed document again — by element name, by an id lookup, by taking the first assertion it finds — you may end up reading a **different** element from the one that was proved. The rule is mechanical and worth stating as a code review item: 1. Verification returns a handle to the covered element. 2. Every attribute, subject and condition you use comes from that handle. 3. No later step re-parses the raw bytes or re-selects from the tree. A library that returns a boolean rather than the verified element is a library that makes this mistake easy, and that is a legitimate reason to prefer a different one. ## InResponseTo needs state, which is why it gets skipped This check is not free. To compare `InResponseTo` against something, you had to generate a request id, store it, and still be able to read it minutes later on a different replica than the one that issued it. That means a short-lived record in storage the whole fleet shares, with a time-to-live, marked used when consumed. Teams that never build that store end up with the check configured as optional, and an optional correlation check is not a check. Treat the request-id store as part of the integration, sized and monitored, not as an afterthought. ## Where the defaults actually bite - **Expected audience unset.** The commonest one. Nothing fails; everything is accepted. - **Destination treated as advisory.** Often tolerated because a firm's identity provider sets it to something stale and turning the check off makes the ticket go away. - **Skew inherited.** A default of several minutes is usually fine, but you should be able to say what yours is and why, because it has a cost elsewhere: it widens the period during which a message stays acceptable. - **Encryption and signature requirements left at whatever the counterparty sends.** Decide what you require per connection and assert it, rather than accepting whatever arrives. ## Order Check the status first, then the signature, then the bindings, then the window, then single use. Reading a subject out of a message before its signature verifies is the same defect as trusting a request body; doing the window check before the signature merely wastes the chance to reject cheaply.

  • Your connection holds several accepted signing certificates. Does that weaken the signature check?
    No, provided the set is scoped to one connection and every member was put there deliberately. What weakens it is verifying against a general trust store, where any key present is accepted and a message from one counterparty passes as another. Per-connection sets with a known reason and a removal date keep the property you want.
  • A firm's identity provider sets a destination value that does not match your endpoint. What do you do?
    Treat it as a connection-scoped decision, not a global switch. Ask the firm to correct it first; if it cannot, record a relaxation on that one connection with an owner and a review date, and keep the recipient check enforced. Turning the check off for everyone to fix one firm is how the estate rots.
  • Why is processing the element returned by verification different from re-reading the assertion from the document?
    Verification proves a signature covers one specific element. Re-selecting from the tree afterwards can hand you a different element that nothing proved, so the signature you checked and the attributes you trusted are no longer about the same thing. Binding every later read to the returned handle removes that gap entirely.

saying these in an interview costs you the question

  • The signature verified, so the message is fine.
  • The library validates everything out of the box.
  • Audience is optional; the signature already proves it was meant for us.
  • Skip the correlation check — we never built anywhere to store the request id.
  • Verify the signature, then re-read the assertion from the document by name.