> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/administration-guide/federation/saml-identity-provider.md).

# OpenIAM as a SAML identity provider

Configure OpenIAM as a SAML 2.0 identity provider for one service provider, test both initiation paths, and diagnose a missing assertion attribute.

## What this is, and when you need it

Create a `SAML_PROVIDER` when OpenIAM authenticates a user and issues a signed SAML assertion to an external service provider — a SaaS application, an internal web application, or a Microsoft 365 verified domain.

Do **not** pick **SAML Service Provider**. That type (`SAML_SP_PROVIDER`) accepts assertions *from* someone else's IdP and is [the other page](/administration-guide/federation/saml-service-provider.md). The two look almost identical on screen and point in opposite directions. Federating a whole Microsoft Entra domain is a routing change rather than an application registration and has its own page: [Federate a Microsoft Entra domain](/administration-guide/federation/federate-a-microsoft-entra-domain.md).

## At a glance

|                          |                                                                                                                                                                                                                         |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | One service provider whose users sign in through OpenIAM, with a `SAML_IDP_PROCESS_SAML_REQUEST` audit event per SP-initiated sign-in and an unassigned user refused with `SAML_USER_NOT_AUTHORIZED_TO_SSO`             |
| **Where**                | **Access Control → Authentication Providers → New** (`/webconsole/newAuthProvider`), then the entitlement on a role                                                                                                     |
| **You need first**       | The SP's entity ID / issuer, the exact **audience** it validates, its ACS URL and binding, its required NameID format, which signature and digest algorithms it accepts, and one assigned plus one unassigned test user |
| **Takes**                | Half a day for a first trust, most of it agreeing values with the application owner                                                                                                                                     |
| **Reversible**           | Yes — removing the launch entitlement stops new sessions without breaking the protocol. Delete the provider only after every dependency is gone                                                                         |

{% hint style="warning" %}
**The signature and digest defaults are SHA-384, and a large number of service providers reject that.** `rsa-sha384` and `#sha384` have been the shipped defaults deliberately since 4.0.0.0, but `rsa-sha256` / `#sha256` is what most partners publish support for. A mismatch presents as an invalid signature at the SP with no useful error, and it is the single most common first-integration failure. Read the SP's metadata `SigningMethod` and `DigestMethod` elements and set both attributes explicitly in step 4.
{% endhint %}

## Before you begin

Beyond the values named above, get these from the application owner or its metadata, because the form will not let you guess them: the required attribute names, data types and source values; whether the assertion must be encrypted and its encryption certificate; the single logout URL and binding; and which initiation flows are in scope — SP-initiated, IdP-initiated, or both.

Decide now who owns the signing certificate, when it expires, and what the rotation overlap will be. Certificate expiry is a scheduled outage in SAML, and the plan is cheaper to make before the trust exists.

HTTPS and synchronized clocks on both sides. OpenIAM's clock tolerance on an inbound message is `org.openiam.saml.time.threshold.minutes`, shipped `10`.

## Configure the trust

{% stepper %}
{% step %}

### Create the provider

**Access Control → Authentication Providers → New**, type **SAML Identity Provider**. If the SP publishes metadata, use **Import from Service Provider Metadata** on the unsaved form — it populates fields, and it validates nothing about ownership, access, NameID or attribute mapping, so review every imported value.

Name the provider for the application and the environment. Put the application, owner, trust direction, access population and certificate expiry in the description; never a key or a secret. Select the managed system representing the application; the form requires it.
{% endstep %}

{% step %}

### Fill the trust identity

| Field                  | Attribute ID | Value                                                                      |
| ---------------------- | ------------ | -------------------------------------------------------------------------- |
| Request Issuer         | `1`          | The issuer this OpenIAM IdP asserts, exactly as registered at the SP       |
| Response Issuer        | `2`          | Leave blank unless the SP needs a different issuer in the response         |
| Assertion Consumer URL | `3`          | The SP's ACS, copied exactly — scheme, host, port, path, case              |
| Audiences              | `4`          | The SP's required audience or entity ID. Do not derive it from the ACS URL |

If the provider refuses to hold a second audience, `Audiences` is still declared as a single value on a database migrated before 4.1.0.0 — two later migrations correct it, and the value has to be re-entered as a list afterwards.
{% endstep %}

{% step %}

### Save once, then publish your metadata URL

{% code overflow="wrap" %}

```
https://<OPENIAM_HOST>/idp/saml2/idp/metadata/<PROVIDER_ID>
```

{% endcode %}

That URL is the deliverable: give it to the SP owner rather than retyping endpoints and certificates. Set `METADATA_EXPOSED` if the SP requires it reachable unauthenticated.

The document published there is an `IDPSSODescriptor` carrying `KeyDescriptor`, `NameIDFormat`, `SingleSignOnService` and `SingleLogoutService` elements — so the algorithms, certificate and NameID format you set on the form are exactly what the partner reads. Open it after every change to those fields; it is the cheapest confirmation that a value saved and swept.
{% endstep %}

{% step %}

### Configure signing, and change the algorithms

Upload a matching certificate and private key, or generate a test pair. Record subject, issuer, fingerprint, not-before, not-after and the owner.

Then set the three algorithm attributes to what the SP supports — **do not leave SHA-384 by default**: `IDPSignatureAlgorithm`, `IdpDigestAlgorithm` and `IDPCanonicalAlgorithm`. Set `IDPWantAssertionsSigned` and `IDPWantResponseSigned` to match the SP's contract, and `IDPWantAuthnRequestSigned` only if the SP signs its requests and you want that enforced.

Confirm what you are actually sending from your own metadata, not from the form: open the metadata URL above and read the algorithm URIs out of it. Note that as an **IdP**, the two "Want…Signed" fields control what OpenIAM *signs on the way out*, despite their names; only `IDPWantAuthnRequestSigned` genuinely means "want".

**IDP Sign Response** arrived only in 4.2.1.14; on an earlier release the field is absent and you cannot sign the response separately from the assertion.
{% endstep %}

{% step %}

### Configure NameID, logout and relay state

Set `NameIdFormat` to the SP's required format. It ships as the **SAML 1.1** email URN (`urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`), which is widely accepted — but if a partner's metadata specifies `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` you must change it. Set `NameQualifier` / `SPNameQualifier` only when the trust requires them; a spurious qualifier is a common cause of user-correlation failure at the SP.

`AuthnContextClassRef` ships `urn:oasis:names:tc:SAML:2.0:ac:classes:Password`, which is an accurate claim for a password sign-in and an inaccurate one once you add MFA. Some SPs make authorization decisions on it, so set it to the class that reflects what actually happened.

Set `SPLogoutURLForIDP` to the SP's SLO endpoint and `IDPLogoutMethodBinding` to a binding it supports, if single logout is in scope. Then decide relay state deliberately — pass-through, `IDPStaticRelayState`, or `IDPRelayStateGenerator`; see [What relay state resolves to, and what a script can change](#what-relay-state-resolves-to-and-what-a-script-can-change).
{% endstep %}

{% step %}

### Add the assertion attributes

The attribute table appears once the provider has an ID. One row per SAML attribute the SP expects: the exact **Name**, the compatible **Data Type**, and a **Source Type** of user property, static value or Groovy script.

Map only the minimum. Test a missing value and a multi-valued value — do not assume the SP handles either. Only one of the three source forms is read per row, in a fixed order, so a stale literal value will never reach your script; see [An attribute the SP expects is absent or empty](#an-attribute-the-sp-expects-is-absent-or-empty-in-the-assertion).
{% endstep %}

{% step %}

### Grant access: this is a separate step

Creating the provider grants nothing. OpenIAM checks the user's entitlement to the provider's **resource** using the same entitlement rules as everywhere else, so group and role inheritance applies; a user who is not entitled is refused with `SAML_USER_NOT_AUTHORIZED_TO_SSO` — *"User with ID '%s' not entitled to SSO with SAML Provider '%s"*, misplaced closing quote and all.

Open the test role's entitlements, add a **Resource** entitlement of type **Authentication provider**, select this provider, and assign only the test user to the role.
{% endstep %}

{% step %}

### Decide whether this trust demands a second factor

`IdpProtectedBy2FA` is `REQUIRED='Y'`, and when it arrived in 4.1.8.0 every existing `SAML_PROVIDER` was given the value `false` rather than being left empty. **So the effective default is `false` — existing SAML applications were not retro-fitted with a second factor.** Ticking it also demands that the content provider serving the SSO endpoint actually offers one.

Open each SAML provider and read the checkbox; there is no list view that shows which are protected.
{% endstep %}

{% step %}

### Wait a sweep, then test

`org.openiam.auth.provider.sweep.time` is `60000` ms.
{% endstep %}
{% endstepper %}

## Test both initiation paths

**SP-initiated.** Start at the application, capture the `AuthnRequest`, authenticate through OpenIAM, and inspect the response in a SAML tracer in non-production: destination, issuer, audience, `InResponseTo`, `NotBefore` / `NotOnOrAfter`, the signature or signatures, encryption, NameID and every required attribute.

**The ACS OpenIAM posts to is not always the configured one.** When the `AuthnRequest` carries an `AssertionConsumerServiceURL`, OpenIAM uses that, falling back to the provider's **Assertion Consumer URL** attribute only when the request leaves it blank — what the SAML core specification requires, and surprising in practice. Compare the request's `AssertionConsumerServiceURL` with the response's `Destination`; if they match each other but not your configured value, this is why.

**IdP-initiated.** Start at `https://<OPENIAM_HOST>/idp/saml2/idp/initiate/<PROVIDER_ID>`, the URL your own portal links to. Audited `SAML_IDP_INITIATED_SSO`; confirm the same outcome.

**Then the negatives**, each of which must fail and leave evidence: an unassigned user; a disabled OpenIAM user; a missing required attribute; a wrong audience in a test copy of the provider; an expired certificate; a replayed response.

**Then logout.** Record what actually ends — the OpenIAM session, the SP session, or both. Partial logout is normal with many SPs and must be documented rather than assumed away: [What single logout actually reaches](/administration-guide/security/session-management/session-management-reference.md#what-single-logout-actually-reaches).

## Verification

| What to check                            | How                                                                                                                                                                                                                                                                                                                                                            |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The provider is live in the cache        | The metadata URL answers XML for your `PROVIDER_ID`. Changes sweep in 60 s: `org.openiam.auth.provider.sweep.time`                                                                                                                                                                                                                                             |
| Metadata is correct and reachable        | `/idp/saml2/idp/metadata/<PROVIDER_ID>` answers XML with your certificate, the NameID format and both service endpoints                                                                                                                                                                                                                                        |
| The algorithms are what the SP accepts   | The `SigningMethod` / `DigestMethod` in your metadata, against theirs. The shipped defaults are SHA-384                                                                                                                                                                                                                                                        |
| The attributes persisted                 | Re-open the provider and read the fields back. For the stored values rather than the rendered ones, the read-only break-glass query is `SELECT AUTH_ATTRIBUTE_ID, VALUE FROM AUTH_PROVIDER_ATTRIBUTE WHERE PROVIDER_ID='<PROVIDER_ID>'` — the only way to see an attribute the form does not render                                                            |
| `IdpProtectedBy2FA` is what you intended | Open the provider and read the checkbox; it is back-filled to `false` on every pre-existing provider                                                                                                                                                                                                                                                           |
| Entitlement is enforced                  | An unassigned user gets `SAML_USER_NOT_AUTHORIZED_TO_SSO`                                                                                                                                                                                                                                                                                                      |
| Audit records exist                      | `SAML_IDP_PROCESS_SAML_REQUEST` (an inbound `AuthnRequest`), `SAML_IDP_INITIATED_SSO`, `SAML_IDP_PROCESS_LOGOUT_REQUEST` and `SAML_IDP_GLOBAL_LOGOUT`. Edits to the provider itself carry no dedicated action constant — they are recorded with a free-text description such as *Delete auth provider* — so filter provider changes on the target type instead |
| The SP agrees                            | Correlate your `SAML_IDP_PROCESS_SAML_REQUEST` event with the SP's own sign-in event                                                                                                                                                                                                                                                                           |
| Debugging                                | `/idp/debugSAMLRequest` and `/idp/debugSAMLResponse` render the last request and response                                                                                                                                                                                                                                                                      |

## When it goes wrong

| Symptom                                                                                                       | Cause                                                                                                                             | Fix                                                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| SP reports an unknown issuer                                                                                  | Attribute `1` Request Issuer differs from what is registered                                                                      | Make them identical                                                                                                                                                                                                                              |
| Signature invalid, everything else looks right                                                                | Algorithm mismatch — OpenIAM defaults to `rsa-sha384` / `#sha384`                                                                 | Set `IDPSignatureAlgorithm` and `IdpDigestAlgorithm` to what the SP publishes, usually SHA-256                                                                                                                                                   |
| Signature invalid after a certificate change                                                                  | The metadata certificate no longer matches the private key                                                                        | Re-publish metadata; keep the old certificate during an overlap                                                                                                                                                                                  |
| Destination or recipient error                                                                                | Assertion Consumer URL, or the SP sent a different ACS in its `AuthnRequest` — OpenIAM honours the request's ACS when present     | Align both, and check the public URL through the proxy                                                                                                                                                                                           |
| Audience rejected                                                                                             | Attribute `4` Audiences is not the SP's registered audience                                                                       | Use the SP's value; never guess it from the ACS URL                                                                                                                                                                                              |
| Audiences will not accept a second value                                                                      | On a database migrated before 4.1.0.0 the attribute was single-valued; two migrations correct it                                  | Re-enter the value after migration                                                                                                                                                                                                               |
| Assertion cannot be decrypted                                                                                 | Wrong SP encryption certificate, or wrong data/key algorithm                                                                      | Disable [encryption](/administration-guide/federation/saml-identity-provider/saml-identity-provider-reference.md#encryption) in the test trust while you correct it                                                                              |
| User not found at the SP                                                                                      | NameID format or value                                                                                                            | Fix the identity contract; never broaden access                                                                                                                                                                                                  |
| A second factor is demanded unexpectedly                                                                      | `IdpProtectedBy2FA` is ticked **and** the content provider offers one                                                             | Untick it, or accept the requirement                                                                                                                                                                                                             |
| Users bypass the second factor                                                                                | `IdpProtectedBy2FA` was back-filled `false` on every pre-existing provider                                                        | Set it per provider                                                                                                                                                                                                                              |
| SSO works but users and passwords do not synchronize                                                          | SAML is only the authentication trust                                                                                             | Configure [account lifecycle](/administration-guide/lifecycle.md) separately                                                                                                                                                                     |
| Logout loops, or leaves a session open                                                                        | `/logout` and `/globalLogout` do different things; cookie domains and SLO binding matter                                          | Register `/logout` as the SLO endpoint ([Every endpoint](#every-endpoint))                                                                                                                                                                       |
| An unassigned user can launch the application                                                                 | A broad entitlement, or a universal role or group                                                                                 | Remove it; inspect inherited access                                                                                                                                                                                                              |
| Chaining loops                                                                                                | `IDPChainingEnabled` set on providers that reference each other                                                                   | Test the failure case before you rely on a chain                                                                                                                                                                                                 |
| An attribute the SP expects is absent or empty in the assertion                                               | OpenIAM omits an attribute whose value list is empty rather than sending it empty                                                 | [An attribute the SP expects is absent or empty](#an-attribute-the-sp-expects-is-absent-or-empty-in-the-assertion)                                                                                                                               |
| **Every** attribute is missing, not one                                                                       | `Can't get SSO Attibutes` (sic) — one throwing attribute clears the whole list                                                    | Search the ESB log for that exact misspelled string and fix the attribute that threw                                                                                                                                                             |
| The `SAMLRequest` is lost after the user authenticates, and no `SPRING_SECURITY_SAVED_REQUEST` cookie was set | The interstitial-screen case recognises `/idp/saml2/idp/login` only, so a `/login/{randomId}` URL is not covered                  | Confirm the missing cookie in the HAR, then protect the bare `/login` path for that SP or remove the interstitial screen — [How an interrupted `AuthnRequest` survives authentication](#how-an-interrupted-authnrequest-survives-authentication) |
| `Can't save AuthnRequest from SP` in the IdP log                                                              | The save into Redis failed                                                                                                        | Check Redis health and the IdP's Redis configuration; this is not a SAML problem                                                                                                                                                                 |
| `No attribute named 'lastSAMLMessageId' found for the current session`                                        | The cookie correlating a **sent** `AuthnRequest` with its response did not come back; its path is a literal string                | Check the IdP context path is still `/idp`, and that the round trip stays on one host and cookie domain                                                                                                                                          |
| Replaying a saved SAML flow works once and then fails                                                         | The saved request is single use — reading it deletes the Redis entry and expires the cookie                                       | Restart the flow from the SP rather than reloading the SSO endpoint                                                                                                                                                                              |
| Different service providers need different authentication levels but there is only one SSO URL                | The level lives on the URI pattern, and every SP posts to the same endpoint                                                       | Register `/idp/saml2/idp/login/<segment>` per SP — [One SSO endpoint per authentication level](#one-sso-endpoint-per-authentication-level-loginrandomid)                                                                                         |
| `IDPRelayStateGenerator` set but the script never runs                                                        | The lookup is by ID, and an ID that resolves to nothing falls back to the default behaviour silently                              | Verify the script ID; do not match by name                                                                                                                                                                                                       |
| A relay state script fails on `spSuccessURL`                                                                  | That field exists only on `DefaultServiceProviderRelayStateGenerator`, and the shipped sample script is SP-side                   | Extend `DefaultIdentityProviderRelayStateGenerator` for an IdP provider                                                                                                                                                                          |
| The SP's own `RelayState` is ignored                                                                          | `IDPStaticRelayState` is checked first and wins                                                                                   | Clear the static value, or make a script decide between them                                                                                                                                                                                     |
| Attribute row edited but the old value still arrives                                                          | A literal attribute value takes precedence over a script ID, which takes precedence over a user attribute; the first one set wins | Clear the literal value before expecting the script to run                                                                                                                                                                                       |

## Rollback and retirement

Remove the launch entitlement first — that stops new sessions without breaking the protocol — then disable the application-side trust. Keep the old signing certificate through an approved overlap when rotating. Delete the provider only after content-provider, URI-pattern, role, group, resource, managed-system, chain and active-session dependencies are gone. For a [federated Entra domain](/administration-guide/federation/federate-a-microsoft-entra-domain.md), remove the federation configuration **before** the provider.

Retain redacted metadata, certificate fingerprints, audit events and test outcomes — never a private key.

## Every endpoint

All SAML paths are built from `org.openiam.idp.provider.saml.base.url` (`${org.openiam.idp.context.path}/saml2`, so `/idp/saml2` by default). The IdP's own paths sit under `/idp/saml2/idp`.

These are the values you hand the SP's owner, or link from your own portal.

| Purpose                                     | Concrete URL                                                          |
| ------------------------------------------- | --------------------------------------------------------------------- |
| IdP metadata, published as XML              | `https://<OPENIAM_HOST>/idp/saml2/idp/metadata/{identityProviderId}`  |
| Where the SP sends its `AuthnRequest` (SSO) | `https://<OPENIAM_HOST>/idp/saml2/idp/login`, and `/login/{randomId}` |
| The same, unauthenticated entry             | `https://<OPENIAM_HOST>/idp/saml2/idp/public/login`                   |
| IdP-initiated SSO                           | `https://<OPENIAM_HOST>/idp/saml2/idp/initiate/{id}`                  |
| Single logout                               | `https://<OPENIAM_HOST>/idp/saml2/idp/logout`                         |
| Global logout                               | `https://<OPENIAM_HOST>/idp/saml2/idp/globalLogout`                   |

The metadata URL is the one to open first: if it answers XML for your `PROVIDER_ID`, the provider exists and is live in the cache. `{identityProviderId}` and `{id}` are the provider's own ID, not the issuer.

**Both logout endpoints exist and they are different.** `/logout` processes an inbound `LogoutRequest` from one SP and is audited `SAML_IDP_PROCESS_LOGOUT_REQUEST`. `/globalLogout` ends the OpenIAM session and is audited `SAML_IDP_GLOBAL_LOGOUT`. Register `/logout` with the partner as the IdP SLO endpoint; `/globalLogout` is the one your own portal links to.

The SSO login path is also the shipped value of `org.openiam.ui.url.pattenrs.always.require.auth` — the misspelling is shipped, and you must reproduce it to find the key. That property is what makes the SSO endpoint always demand an authenticated session, and so what makes SP-initiated SSO prompt for credentials.

### One SSO endpoint per authentication level: `/login/{randomId}`

An OpenIAM IdP holds **one** authentication level per federation, because the level is a property of the URI pattern protecting the SSO endpoint and every SP arrives at the same endpoint. The way out is that the SSO endpoint also answers on `/idp/saml2/idp/login/<anything>`: a trailing segment is accepted and then ignored, so `/idp/saml2/idp/login/finance` behaves exactly like `/idp/saml2/idp/login`.

Confirm that for yourself before you build on it — post the same `AuthnRequest` to the bare path and to a path with an invented segment, and the flow completes identically. The point of the segment is to give one IdP several **distinct URLs** for the same endpoint, so each can carry its own URI pattern, and therefore its own authentication level and its own entitlement.

To use it: pick a stable, opaque segment per service provider, register `https://<OPENIAM_HOST>/idp/saml2/idp/login/<segment>` with that SP as the IdP SSO endpoint, create a URI pattern for exactly that path on the content provider serving the IdP, and set the authentication level on that pattern. The generic `/idp/saml2/idp/login` pattern keeps whatever level it had, and OpenIAM already recognises the sub-paths as SSO endpoints — `org.openiam.idp.provider.saml.idp.login.extends` holds `/idp/saml2/idp/login/` and the matching `...without.basepath.extends` holds `/saml2/idp/login/**`, both shipped, neither needing an edit.

{% hint style="info" %}
The segment appears in metadata and in browser history, so treat it as an identifier, not a secret, and do not derive it from a tenant or customer name. Keep a record of which segment belongs to which SP: nothing in the Webconsole associates them, because as far as the product is concerned the segment is inert.
{% endhint %}

### How an interrupted `AuthnRequest` survives authentication

When an inbound `AuthnRequest` arrives at an SSO endpoint that demands authentication the request itself must be parked somewhere and replayed afterwards. Knowing which of two similarly-named mechanisms does that is the difference between a five-minute diagnosis and a long one.

**`SPRING_SECURITY_SAVED_REQUEST` is the cookie that parks an inbound `AuthnRequest`.** OpenIAM writes the whole saved request into Redis under a fresh UUID and hands the browser that UUID as an `HttpOnly`, session-scoped cookie of that name. It is **single use**: reading it deletes the Redis entry and expires the cookie. On the way back OpenIAM replays the saved request, either resubmitting the `SAMLRequest` and `RelayState` or returning the browser to the original URL, according to how the SP sent them. A save that fails logs `Can't save AuthnRequest from SP`, which points at Redis rather than at SAML.

Three things can park the request, and they do not agree about the `/login/{randomId}` variants:

| What interrupts the request                                                                          | Covers `/login/{randomId}`?                                      |
| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| The user is not authenticated yet, so OpenIAM sends them to sign in                                  | **Yes** — the bare login path and any sub-path both count        |
| An **interstitial** screen stands in the way — secret word, terms acceptance, forced password change | **No** — only the bare `/idp/saml2/idp/login` is recognised here |
| The sign-in came back needing a second factor, so the SSO endpoint re-requests itself                | Yes — whatever the URL                                           |

So an **interstitial** screen on a `/login/{randomId}` URL is the gap: nothing is saved, and the browser returns to the SSO endpoint with no `SAMLRequest` to process. The observable signature is the absence of a `SPRING_SECURITY_SAVED_REQUEST` cookie in the browser at the moment of the redirect. Confirm that in the HAR before theorising about the assertion — if the cookie was never set, no amount of SAML configuration will help.

**`lastSAMLMessageId` is a different cookie for a different job.** It carries the UUID of an `AuthnRequest` OpenIAM has **sent**, so the eventual response can be correlated with it. It is written with cookie path `/idp/saml2/sp/login` on the SP-facing path and `/idp/saml2/idp/login` on the IdP request-forwarding path, the Redis entry lives 24 hours, and the cookie is `HttpOnly` with no `Domain`. Its absence raises `No attribute named 'lastSAMLMessageId' found for the current session`.

{% hint style="warning" %}
Those two cookie paths are **fixed**, not derived from `org.openiam.idp.context.path`. If you change the IdP context path away from `/idp`, the cookie is written for a path the SSO and ACS endpoints no longer live under, the browser stops sending it back, and correlation fails with the message above. You can see this directly: look at the `Path` attribute of `lastSAMLMessageId` in the browser's cookie list and compare it with the path of the endpoint you are actually posting to. Treat a change to `org.openiam.idp.context.path` as a change that needs a full SAML round trip retested in both directions.
{% endhint %}

## What relay state resolves to, and what a script can change

Relay state decides where the user's browser goes once the assertion has been consumed. With no script configured, OpenIAM resolves it in a strict order of three: the value of `IDPStaticRelayState` if it is set; otherwise the `RelayState` parameter the SP sent; otherwise nothing at all.

So `IDPStaticRelayState` **wins over the SP's own `RelayState`** — worth knowing before you set a static value on a provider serving more than one entry point, because it silently overrides whatever the SP asked for. The check is to compare the `RelayState` in the SP's request with the one in OpenIAM's response, in a SAML tracer; if they differ, a static value or a script is in the way.

`IDPRelayStateGenerator` replaces that ordering with a Groovy script of your own. The lookup is **by ID**, and nothing is reported when the ID resolves to nothing: OpenIAM quietly uses `DefaultIdentityProviderRelayStateGenerator` instead. A wrong or stale script ID therefore presents as "my script is not running" with nothing in the log — check the ID against the Groovy editor, per [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md#how-a-caller-finds-your-script).

Your script inherits four fields, and they are the only input it gets:

| Field                   | On this IdP response path        | Notes                                                                                                                       |
| ----------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `request`               | The current `HttpServletRequest` | Use it for `request.getParameter("RelayState")`, the `issuer` parameter, or anything else the SP put on the URL             |
| `signableSAMLObject`    | The outbound SAML **Response**   | On the request-processing path the same field holds the inbound `AuthnRequest` — check which flow you are in before casting |
| `user`                  | The resolved OpenIAM user        | Populated from the session's user id                                                                                        |
| `staticRelayStateValue` | `IDPStaticRelayState`, if set    | Your script decides whether to honour it; the default class returns it first                                                |

{% hint style="warning" %}
**Extend the right base class.** The sample script that ships as `AM/sp/CustomServiceProviderRelayStateGenerator_1.0.groovy` extends `DefaultServiceProviderRelayStateGenerator` and returns `spSuccessURL` — a field that exists **only** on the service-provider base class, where it takes its value from `org.openiam.sp.login.redirect.success.url`. Copying that sample onto an IdP provider's `IDPRelayStateGenerator` gives you a script written against the wrong direction, and the failure is a script error on an unknown field. For an IdP, extend `DefaultIdentityProviderRelayStateGenerator` and return a URL.
{% endhint %}

Whatever the script returns is a redirect target, so validate it. Return a path, or a URL whose host you have checked against a list you control; never return a request parameter unfiltered. A relay state that is static must be an approved destination, and a generator script must not be able to emit an arbitrary external URL — this is the open-redirect surface of the whole integration, and a script is the easiest place to open it by accident.

## An attribute the SP expects is absent or empty in the assertion

The single most useful thing to know here is that **OpenIAM does not send empty attributes — it omits them entirely.** An attribute whose value resolves to an empty list is left out of the assertion rather than sent with no value. So "the SP says the attribute is missing" and "the attribute resolved to nothing" are the same fact, and the SP's complaint tells you nothing about *why*. You can see which is which only by reading the assertion yourself in a SAML tracer: the attribute's `Attribute` element is absent, not present-and-empty. Work it in this order.

{% stepper %}
{% step %}

### Confirm the attribute row is even in play

OpenIAM omits the whole `AttributeStatement` when the provider has no attribute rows at all, so start by confirming the row exists on the provider's attribute table. Then check which of the three mutually exclusive source forms that row uses, because OpenIAM tests them in a fixed order and stops at the first one set: a literal value (split on `||` for multiple values), then a Groovy script, then a user attribute. **A row with a stale literal value will never reach your script**, and the form shows both fields side by side without warning you which one wins.
{% endstep %}

{% step %}

### Open the Groovy source script **by ID**

If the row uses a script, the lookup is by ID only. Open that exact ID in the Groovy editor rather than searching by name; a script with the same name at a different path is not what runs. See [Create a Groovy script](/administration-guide/automation/manage-groovy-scripts.md#how-a-caller-finds-your-script).
{% endstep %}

{% step %}

### Read the ESB log for the resolution, not the assertion

The attribute resolution logs its decisions in the ESB log at `DEBUG`. Raising the ESB log level is a deployment-layer change — see the [Installation Guide](/installation/installation.md) — but once the level is up, three lines tell you almost everything, and they are worth searching for verbatim:

| Log line                                                                                        | What it tells you                                                                                         |
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `try to get attribute list by provider id:<PROVIDER_ID>`                                        | The resolution ran at all, and for the provider you think it did                                          |
| `Getting attribute with name:<NAME>` followed by `Attribute found` **or** `Attribute not found` | Whether the *source* user attribute existed. `Attribute not found` is a data answer, not a mapping answer |
| `Cannot create groovy script: <token>`                                                          | The script ID does not resolve, or the script is not a valid attribute mapper                             |

{% hint style="info" %}
**There is no log line that prints the resolved value count.** This release logs the found/not-found decision rather than the size of the result, and no console screen shows it either. Read the pair of lines above as your proxy — `Attribute not found` means zero — and if you need the number itself, log it from your own script before returning.
{% endhint %}
{% endstep %}

{% step %}

### Confirm the test user actually holds the data

This is the step that is skipped, and it is usually the answer. If the script derives the attribute from role or group membership, open the test user's access and confirm the memberships exist *now* — a campaign revocation, a birthright rule that did not fire, or a user who was never assigned the role all produce the same empty list. **A resolved count of zero is a data problem, not a mapping problem**, and no change to the attribute row, the name format or the data type will fix it.
{% endstep %}
{% endstepper %}

{% hint style="danger" %}
**One failing attribute silently removes them all.** OpenIAM resolves the whole attribute set in one pass, and any failure anywhere in it discards the entire set and logs `Can't get SSO Attibutes` — note the misspelling, which is what you must actually search for. So a script that throws on attribute three does not cost you attribute three; it costs you the entire `AttributeStatement`, and the SP sees an assertion with no attributes at all. If *every* attribute vanished at once, search the ESB log for `Can't get SSO Attibutes` before you look at any individual row.
{% endhint %}

## Related

* [Every SAML identity provider attribute, with its shipped default](/administration-guide/federation/saml-identity-provider/saml-identity-provider-reference.md) — the attribute keys, the encryption controls and the declared data types
* [OpenIAM as a SAML service provider](/administration-guide/federation/saml-service-provider.md) — the inbound direction
* [Federate a Microsoft Entra domain](/administration-guide/federation/federate-a-microsoft-entra-domain.md) — the tenant-wide special case

*Applies to OpenIAM 2026.8.1. Last reviewed 7 August 2026.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/administration-guide/federation/saml-identity-provider.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
