---
title: "SAML applications"
description: "Sign people in to SAML 2.0 applications from the catalog or from their metadata, with your directory as the identity provider."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.ezghcloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SAML applications

A `saml` application is a SAML 2.0 service provider, such as Slack, Zoom or your own app, that
signs your directory's people in with your directory as its identity provider (IdP). Only users
you [assign](/sso/assignments) to the application, directly or through a group, can sign in to it.

SAML applications count against the same limit as other applications: up to 200 per directory.
You need `sso.applications.create`.

## Create a SAML application

Start from one of three places:

- **The catalog:** a common SaaS application with its settings filled in. You enter the values
  that are yours, such as your Slack domain. See [The catalog](#the-catalog).
- **The service provider's metadata:** a metadata URL or the XML document. EZGH SSO imports the
  entity ID, the HTTP-POST ACS URLs, the single logout URL and the certificates from it, once. It
  doesn't refresh them later.
- **The details:** the service provider's entity ID and ACS URLs.

### Console

1. Open **EZGH SSO** > **Applications** and select **Create application**.
2. Choose **SAML application**.
3. Under **Start from**, choose **From the catalog**, **From the app's SAML metadata** or
**Enter the details**.
4. Fill in the details, review, and select **Create application**.

The application opens on its **Sign-in** tab, with what to give the service provider.
### CLI

```sh
# From the catalog
ezgh sso applications create --catalog slack --param slackDomain=acme.slack.com

# From the service provider's metadata
ezgh sso applications create --name Grafana --kind saml --metadata-url https://grafana.acme.com/saml/metadata

# From the details
ezgh sso applications create --name Tool --entity-id https://tool.acme.com --acs-url https://tool.acme.com/saml/acs \
  --attribute 'email=${user.email}' --attribute 'groups=${user.groups}'
```

Flags given with `--catalog` or metadata win over what they fill in.
### API

Call [CreateApplication](/sso-api/CreateApplication/) with `kind: "saml"`:

```json
{
  "name": "Grafana",
  "kind": "saml",
  "saml": { "metadataUrl": "https://grafana.acme.com/saml/metadata" }
}
```

Or give the settings themselves:

```json
{
  "name": "Tool",
  "kind": "saml",
  "saml": {
    "entityId": "https://tool.acme.com",
    "acsUrls": ["https://tool.acme.com/saml/acs"],
    "attributes": [{ "name": "email", "value": "${user.email}" }]
  }
}
```

`metadataUrl` must be a public https URL; it's fetched at once, and a failed fetch is `400`.
`metadataXml` takes the document itself, up to 256 KiB. Fields sent alongside either win over
what they import.

Then [assign](/sso/assignments) users or groups, and register the IdP at the service provider.

## Register the IdP at the service provider

Each SAML application has its own IdP: its own entity ID and metadata, with your directory's
signing certificate. So you can add the same service provider twice. `saml.idp` in the
application, the console's **Sign-in** tab and `ezgh sso applications idp` show:

| Value | Form |
| --- | --- |
| IdP entity ID, metadata URL | `https://<alias>.sso.ezgh.app/saml/metadata/<application ID>` |
| SSO URL (HTTP-Redirect and HTTP-POST) | `https://<alias>.sso.ezgh.app/saml/sso/<application ID>` |
| SLO URL (HTTP-Redirect and HTTP-POST) | `https://<alias>.sso.ezgh.app/saml/slo/<application ID>` |
| IdP-initiated sign-in | `https://<alias>.sso.ezgh.app/saml/sso/<application ID>`, opened without a request |
| Signing certificates | The directory's certificates. See [Certificates](#certificates). |

Service providers that import IdP metadata take the metadata URL. Others take the entity ID, the
SSO URL and the certificate:

```sh
ezgh sso applications idp Grafana
ezgh sso applications idp-certificate Grafana > ezgh-sso.pem
```

In the console, select **Download certificate** on the **Sign-in** tab.

If you change your directory's [alias](/sso/directory#aliases), these URLs change: update the
service provider.

## Settings

`saml` holds the service provider's settings. On update, only the fields you send change;
`metadataUrl` or `metadataXml` imports metadata again.

| Field | Description | Default |
| --- | --- | --- |
| `entityId` | The service provider's entity ID, 1 to 1,024 characters: the assertion's audience, and the `Issuer` its requests must carry. | Required |
| `acsUrls` | 1 to 10 exact ACS URLs for the HTTP-POST binding: https, or http on a loopback address. The first is the default. | Required |
| `sloUrl`, `sloBinding` | The service provider's single logout URL, and its binding: `HTTP-Redirect` or `HTTP-POST`. | `null`, `HTTP-Redirect` |
| `nameIdFormat` | `email`, `persistent` or `transient`. See [NameID formats](#nameid-formats). | `email` |
| `attributes` | Up to 50 attributes. See [Attributes](#attributes). | None |
| `groupsFilter` | Which groups `${user.groups}` lists: `assigned` (the user's groups assigned to this application) or `all`. | `assigned` |
| `signResponse`, `signAssertion` | Sign the Response, the Assertion, or both. At least one. | Both `true` |
| `encryptAssertions`, `encryptionAlgorithm` | Encrypt the assertion to the service provider's encryption certificate, with `aes128-gcm` or `aes256-cbc`. | `false`, `aes128-gcm` |
| `spCertificates` | Up to 10 of the service provider's certificates, `{ "use": "signing" \| "encryption", "certificate" }`: base64 DER or PEM, RSA (2048 bits or more) or ECDSA. Encryption needs RSA. | None |
| `idpInitiated` | Allow IdP-initiated sign-in, and the portal tile. | `true` |
| `defaultRelayState` | The RelayState sent with IdP-initiated sign-in: where the service provider lands. Up to 1,024 characters. | `null` |

The answer also shows `metadataUrl` and `metadataImportedAt` (where and when metadata was last
imported) and `idp`.

### NameID formats

| `nameIdFormat` | `NameID` sent | Format URI |
| --- | --- | --- |
| `email` | The user's email address | `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` |
| `persistent` | The user's ID, which never changes | `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` |
| `transient` | A new value for every assertion | `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` |

A request whose `NameIDPolicy` asks for a format other than the application's (or `unspecified`)
gets an `InvalidNameIDPolicy` error response.

### Attributes

Each attribute is `{ "name", "nameFormat", "value" }`. `name` is what the service provider
expects, up to 256 characters, and each name appears once. `nameFormat` is `unspecified` (the
default), `uri` or `basic`. `value` is a template of up to 1,024 characters:

| Variable | Value |
| --- | --- |
| `${user.id}` | The user's ID |
| `${user.email}` | The email address |
| `${user.name}` | The display name |
| `${user.givenName}`, `${user.familyName}` | The given and family names |
| `${user.department}` | The department: set on the user, or sent by [inbound SCIM](/sso/scim) |
| `${user.groups}` | Exactly this and nothing else: one value per group name, filtered by `groupsFilter` |

Literal text and variables can be mixed, such as `${user.givenName} ${user.familyName}`. An
attribute whose value is a single variable the user has no value for is left out.

```sh
ezgh sso applications update Tool --attribute 'email=${user.email}' --attribute 'department=${user.department}' \
  --attribute 'groups=${user.groups}' --groups-filter all
```

### Signing and encryption

Responses and assertions are signed with RSA-SHA256 and exclusive canonicalization. By default
both the Response and the Assertion are signed, which most service providers accept; turn one off
only if the service provider needs it.

With `encryptAssertions`, the assertion is signed, then encrypted to the service provider's
encryption certificate: the content with AES-128-GCM (default) or AES-256-CBC, the key with
RSA-OAEP. Add the certificate to `spCertificates` with `use: "encryption"`, or import it with the
metadata.

```sh
ezgh sso applications update Slack --encrypt-assertions --encryption-certificate slack-encryption.pem
```

## Certificates

Each directory has one SAML signing key: RSA-3072, with a self-signed certificate valid for three
years. It's made when you create the directory's first SAML application, and every SAML
application uses it.

| `status` | Meaning |
| --- | --- |
| `active` | Signs responses and assertions now. |
| `next` | The successor, made 90 days before the active certificate expires and published at once in every application's metadata and `saml.idp.certificates`. It starts signing when the active certificate expires (`activatesAt`), and the old one is then removed. |

Each certificate shows its `fingerprintSha256`, `subject`, `notBefore`, `notAfter` and
`warnAt`, 60 days before it expires, from when the console warns you.

**Rollover:** service providers that read your IdP metadata URL pick up the next certificate on
their own. For the others, give them the next certificate once it's published and before the
active one expires:

```sh
ezgh sso applications idp-certificate Slack --next > ezgh-sso-next.pem
```

You can't roll the certificate early.

## How sign-in works

**Service provider-initiated:** the service provider sends an `AuthnRequest` to the SSO URL, with
the HTTP-Redirect or HTTP-POST binding. EZGH SSO checks that:

- its `Issuer` is the application's `entityId`;
- its `Destination`, when sent, is the SSO URL, and it was issued within the last 90 seconds;
- the ACS it names, by URL or index, is one of `acsUrls` (without one, the default is used);
- its RelayState is at most 1,024 bytes, and the request is at most 64 KiB.

A request that fails these checks gets an error page; nothing is sent to an ACS URL. A valid
request continues with the person's portal session, or they sign in first. With `ForceAuthn`,
they sign in again unless they signed in after the request was made. With `IsPassive`, someone
who would have to sign in gets a `NoPassive` error response instead. A `ProtocolBinding` other
than HTTP-POST gets `UnsupportedBinding`.

**IdP-initiated:** opening the SSO URL without a request (the portal tile does this) signs the
person in to the default ACS URL, with `defaultRelayState`. It's refused when `idpInitiated` is
off. A RelayState in the URL is never used.

**Portal tiles:** assigned users see a tile for each SAML application that allows IdP-initiated
sign-in. The tile can show the application's own [logo](/sso/applications#tile-logo).

Either way, the person must be [assigned](/sso/assignments), checked at every sign-in: someone
who isn't sees an error page (with `IsPassive`, a `RequestDenied` response), recorded in Trails as
a failed `AuthorizeApplication`.

### The assertion

| Part | Value |
| --- | --- |
| `Issuer` | The IdP entity ID |
| `Subject` | The `NameID` in the application's format, with a bearer confirmation for the ACS URL, valid for 5 minutes |
| `Conditions` | `AudienceRestriction` with the application's `entityId`; `NotBefore` one minute before issue |
| `AuthnStatement` | `AuthnInstant` (when the person signed in), `SessionIndex` (the portal session), `SessionNotOnOrAfter` (when the session ends) |
| `AuthnContextClassRef` | `https://refeds.org/profile/mfa` for a multi-factor sign-in, `urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport` for a password alone, otherwise `urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified` |
| `AttributeStatement` | The application's [attributes](#attributes) |

## Single logout

Single logout is best effort, for the browser the request comes from. A `LogoutRequest` to the SLO
URL (HTTP-Redirect or HTTP-POST), checked like an `AuthnRequest`, ends that browser's portal
session when it names the session (`SessionIndex`) or the user. That also ends the session's OIDC
grants without `offline_access`, with their [back-channel logout](/sso/applications#logout)
notices. The signed `LogoutResponse` goes to the application's `sloUrl`, or the person sees the
signed-out page without one.

Other service providers the person signed in to aren't told, and EZGH SSO never sends a
`LogoutRequest` itself.

## Not supported

- **Signed requests aren't verified.** Signatures on `AuthnRequest` and `LogoutRequest` messages
  are ignored, and `spCertificates` with `use: "signing"` are stored but not used. Nothing a
  request says is trusted beyond what you registered: responses go only to registered ACS URLs,
  and a forged logout request can at most sign out the browser it's sent from.
- **No single logout propagation** to other service providers, and no IdP-initiated logout.
- The HTTP-Artifact binding, `RequestedAuthnContext` (it's ignored), pairwise persistent
  NameIDs, and refreshing imported metadata.

## The catalog

[ListCatalogApplications](/sso-api/ListCatalogApplications/) (`ezgh sso catalog list`, needs
`sso.catalog.list`) returns 16 common SaaS applications: Slack, Zoom, GitHub Enterprise Cloud
(organization), Atlassian Cloud, Salesforce, Google Workspace, AWS IAM Identity Center, Dropbox,
Notion, Figma, Zendesk, PagerDuty, Asana, Miro, Sentry and Tailscale. Each entry has:

- `parameters`: the values you fill in, such as `slackDomain`, each with a label, a description,
  an example and a pattern. They replace `{{name}}` in the entry's URLs.
- `saml`: SAML settings in this API's terms (all but Tailscale), with the vendor's setup steps
  in `notes`.
- `oidc`: OIDC client settings, where the vendor documents a generic OIDC identity provider
  (Salesforce, Zendesk and Tailscale), for an [OIDC application](/sso/applications).
- `scim`: a SCIM base URL and attribute mappings for [outbound provisioning](/sso/provisioning),
  where the vendor supports it.
- `sources`: the vendor documentation each part was checked against.

```sh
ezgh sso catalog get slack
```

The console and the CLI create the application from an entry's SAML settings; the API takes the
filled-in settings in [CreateApplication](/sso-api/CreateApplication/).

## Delete a SAML application

[DeleteApplication](/sso-api/DeleteApplication/) (`ezgh sso applications delete`) removes it
with its assignments and provisioning. Its users can't sign in to it through EZGH SSO any more,
and its tile leaves the portal.

Source: https://docs.ezghcloud.com/sso/saml-applications/index.mdx
