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 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 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.
- Open EZGH SSO > Applications and select Create application.
- Choose SAML application.
- Under Start from, choose From the catalog, From the app’s SAML metadata or Enter the details.
- Fill in the details, review, and select Create application.
The application opens on its Sign-in tab, with what to give the service provider.
# 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.
Call CreateApplication with kind: "saml":
{
"name": "Grafana",
"kind": "saml",
"saml": { "metadataUrl": "https://grafana.acme.com/saml/metadata" }
}Or give the settings themselves:
{
"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 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. |
Service providers that import IdP metadata take the metadata URL. Others take the entity ID, the SSO URL and the certificate:
ezgh sso applications idp Grafana
ezgh sso applications idp-certificate Grafana > ezgh-sso.pemIn the console, select Download certificate on the Sign-in tab.
If you change your directory’s alias, 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. |
email |
attributes |
Up to 50 attributes. See 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 |
${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.
ezgh sso applications update Tool --attribute 'email=${user.email}' --attribute 'department=${user.department}' \
--attribute 'groups=${user.groups}' --groups-filter allSigning 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.
ezgh sso applications update Slack --encrypt-assertions --encryption-certificate slack-encryption.pemCertificates
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:
ezgh sso applications idp-certificate Slack --next > ezgh-sso-next.pemYou 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
Issueris the application’sentityId; - 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.
Either way, the person must be assigned, 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 |
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
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
AuthnRequestandLogoutRequestmessages are ignored, andspCertificateswithuse: "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 (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 asslackDomain, 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 innotes.oidc: OIDC client settings, where the vendor documents a generic OIDC identity provider (Salesforce, Zendesk and Tailscale), for an OIDC application.scim: a SCIM base URL and attribute mappings for outbound provisioning, where the vendor supports it.sources: the vendor documentation each part was checked against.
ezgh sso catalog get slackThe console and the CLI create the application from an entry’s SAML settings; the API takes the filled-in settings in CreateApplication.
Delete a SAML application
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.