# Set up a SAML app

> Connect a SAML 2.0 service provider to Trustaige. The values it needs, the metadata, the attributes it receives, and certificate rollover.

Source: https://docs.trustaige.com/sign-in/saml

Replace these placeholders with your organization's own values:

- `<your-org-id>`: Organization short ID, for example `a1B2c3D4e5F6`. The part after /o/ in your hub’s address.
- `<your-app-url>`: Your app’s address, for example `https://app.example.com`. Scheme and host, no path: where your app runs.

Trustaige is a SAML 2.0 identity provider. A service provider that speaks SAML
sends people to Trustaige, they sign in with their passkey, and Trustaige posts
a signed assertion back saying who they are. Use SAML for a service that
doesn't offer OpenID Connect; for anything that does, [set up an OpenID Connect
app](/sign-in/oidc) instead.

This page is for whoever connects the two sides: an administrator in the hub,
and whoever configures single sign-on in the service provider. You need the
permission to manage apps in your organization.

1. Person → Service provider: opens the app
2. Service provider → Trustaige: AuthnRequest to the SSO URL
3. Person ↔ Trustaige: passkey
4. Trustaige → Service provider: signed Response, posted to the ACS URL
5. Service provider → Person: signed in

Sign-in starts at the service provider: Trustaige answers an `AuthnRequest`,
and doesn't send unsolicited responses. The app's **Launch URL** in the hub
should therefore be an address at the service provider that starts its own
SAML sign-in.

## The values a service provider needs

Every value is on the app's page in the hub, in the **Identity Provider**
panel, ready to copy. Most of them carry your organization's short ID, the part
after `/o/` in your hub's address.

| The service provider calls it | Hub label | Value |
|---|---|---|
| IdP entity ID, Issuer | **IdP Entity ID (Issuer)** | `https://id.trustaige.com` |
| Single sign-on URL | **SSO URL** | `https://id.trustaige.com/o/<your-org-id>/saml/sso` |
| Single logout URL | **SLO URL** | `https://id.trustaige.com/o/<your-org-id>/saml/slo` |
| Metadata URL | **Metadata URL** | `https://id.trustaige.com/o/<your-org-id>/saml/metadata` |
| Signing certificate, X.509 certificate | **Signing Certificate** | Shown in the hub; see [The signing certificate](#the-signing-certificate) |

The entity ID is the same for every organization. The SSO and SLO URLs are your
organization's own: Trustaige finds the app by the service provider's entity ID
*within* the organization the URL names, so a service provider pointed at
another organization's URL is refused.

Both URLs accept the HTTP-Redirect and HTTP-POST bindings.

> **Tip:** If the service provider can read metadata, give it the **Metadata URL** and
> nothing else. It then has every value above, and it picks up a new signing
> certificate by itself when one is published.

## The metadata

The **Metadata URL** serves a standard SAML 2.0 metadata document, with no
sign-in needed. Opening it in a browser downloads it as
`trustaige-<your-org-id>-idp-metadata.xml`, which is what the **Download XML**
button in the hub does. It contains:

- An `EntityDescriptor` with `entityID="https://id.trustaige.com"`.
- An `IDPSSODescriptor` with one `KeyDescriptor use="signing"` for each
  certificate the service provider should trust, the one signing today first.
- The NameID formats Trustaige supports: `unspecified`, `emailAddress`,
  `persistent` and `transient`.
- A `SingleSignOnService` and a `SingleLogoutService`, each for the
  HTTP-Redirect and HTTP-POST bindings, at your organization's SSO and SLO URLs.

```xml title="IdP metadata (shape)" {5-7,12-15}
<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://id.trustaige.com">
  <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <md:KeyDescriptor use="signing">
      <ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIIC…</ds:X509Certificate></ds:X509Data></ds:KeyInfo>
    </md:KeyDescriptor>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://id.trustaige.com/o/<your-org-id>/saml/sso"/>
    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://id.trustaige.com/o/<your-org-id>/saml/sso"/>
    <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://id.trustaige.com/o/<your-org-id>/saml/slo"/>
    <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://id.trustaige.com/o/<your-org-id>/saml/slo"/>
  </md:IDPSSODescriptor>
  <md:Organization>…</md:Organization>
  <md:ContactPerson contactType="technical">…</md:ContactPerson>
</md:EntityDescriptor>
```

While a new certificate is waiting to take over, there are two
`KeyDescriptor` elements; see [Certificate rollover](#certificate-rollover).

## Create the app

You need two values from the service provider first: its **entity ID** (the
`Issuer` of the requests it sends) and its **ACS URL** (Assertion Consumer
Service: where it receives responses). Its own SAML settings page or its
metadata lists both.

1. In the hub, go to **Apps**. Under **In-house applications**, click
   **Create in-house app**.
2. Give it a name, and under **Protocol** choose **SAML 2.0**. Continue.
3. Fill in **SP Entity ID** exactly as the service provider sends it.
4. Fill in the **ACS URL**, for example `<your-app-url>/saml/acs`. The hub
   marks it optional, but sign-in can't complete without one: Trustaige has
   nowhere to send the response.
5. Choose a **Name ID Format** (see [Who the assertion names](#who-the-assertion-names)).
   **Email Address** suits most services.
6. Check the summary and click **Create application**.
7. On the app's page, copy the values from the **Identity Provider** panel into
   the service provider's SAML settings, or give it the **Metadata URL**.

**Choose who can use it.** In the app's **Access** section, assign the groups
whose people may sign in. With none assigned, the section reads **Open to
everyone**.

**If the service provider signs its requests**, open the app's menu, choose
**Edit app**, turn on **Want AuthnRequests Signed**, and paste its certificate
under **SP Signing Certificate** (PEM or Base64 DER). Trustaige checks the
signature of every request that carries one, so a service provider that signs
needs its certificate here even if you don't require signing.

## What Trustaige sends

The response is posted to the ACS URL with the HTTP-POST binding.

| | |
|---|---|
| Issuer | `https://id.trustaige.com` on the response and the assertion |
| Signatures | The response and the assertion are both signed, always |
| Signature algorithm | RSA-SHA256 (`http://www.w3.org/2001/04/xmldsig-more#rsa-sha256`), SHA-256 digest, exclusive canonicalization |
| Encryption | Assertions aren't encrypted |
| Audience | The service provider's entity ID |
| Recipient, Destination | The ACS URL the response is posted to |
| Validity | `NotBefore` 2 minutes before issue (for clock skew), `NotOnOrAfter` 5 minutes after |
| Subject confirmation | `bearer`, with `InResponseTo` the request's ID |
| `SessionIndex` | Present; send it back in a `LogoutRequest` to end that session |
| `AuthnContextClassRef` | `urn:oasis:names:tc:SAML:2.0:ac:classes:FIDO`, or `urn:oasis:names:tc:SAML:2.0:ac:classes:HardwareToken` when the person signed in with a security key proven to be hardware |

A requested authentication context isn't enforced: Trustaige asserts the one
the person actually achieved. `ForceAuthn="true"` makes the person sign in
again, and `IsPassive="true"` gets a `NoPassive` status when they aren't
signed in already.

### Who the assertion names

The `NameID` follows the app's **Name ID Format**. When the request's
`NameIDPolicy` asks for a format Trustaige supports, that one wins.

| Format | Hub label | The `NameID` is |
|---|---|---|
| `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` | **Email Address** | The person's email in your organization |
| `urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified` | **Unspecified** | The person's email in your organization |
| `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` | **Persistent** | An opaque value, the same every time for this person and this service provider, and different at every other service provider |
| `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` | **Transient** | A new random value at every sign-in |

Choose **Persistent** when the service provider keys accounts on the `NameID`
and you don't want an email change to orphan an account. `NameQualifier` is
`https://id.trustaige.com` and `SPNameQualifier` is the service provider's
entity ID.

### Attributes

Each attribute has the name format
`urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified`. An attribute with no
value is left out.

| Attribute | Value |
|---|---|
| `email` | The person's email in your organization |
| `uid` | The person's ID in your organization. It's stable for as long as they're a member, and different in every organization, so two organizations' apps can't use it to recognise the same person |
| `givenName` | First name from their profile |
| `surname` | Last name from their profile |
| `displayName` | Their full name |
| `groups` | The groups they belong to, directly or through a parent group, that are assigned to this app. Each is sent as the group's slug, or its name when it has none |
| `roles` | The roles they hold in this app (its own app roles, never their roles in the hub) |
| `deviceId` | The device they signed in from, when Trustaige knows it |
| `collaborator.id`, `collaborator.name`, `collaborator.logo` | For a person from a collaborating organization: that organization's short ID, name and logo. Such a person gets no `groups` or `roles` |

When a profile has no first and last name, Trustaige splits the display name at
its first space, because some service providers refuse a sign-in without both.

**Rename attributes** to what the service provider expects under **Edit app →
Attribute Mappings**: the left side is Trustaige's name, the right side the name
sent. A new app starts with `email`, `uid`, `givenName`, `surname` and
`displayName` mapped to themselves.

## The signing certificate

One certificate signs SAML for every organization on Trustaige. It is
self-signed, so a service provider must trust the certificate itself rather
than look for a certificate authority. The key is RSA 2048-bit, and each
certificate is valid for a year.

The hub shows it under **Signing Certificate** in three forms; pick the one
the service provider asks for:

| Tab | What you get |
|---|---|
| **PEM** | The certificate between `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` lines. **Download .pem** saves it as `certificate.pem` |
| **Base64 DER** | The same certificate as one Base64 string with no header or footer. **Download .der** saves the binary `certificate.der` |
| **SHA-256** | The certificate's fingerprint, as colon-separated hex pairs. Use it to confirm a service provider holds the right one |

The **Expires** date shown with it is when the current certificate stops
signing.

## Certificate rollover

Trustaige replaces the signing certificate before it expires, and publishes
the new one well ahead of the switch so every service provider can trust both.

1. **About 60 days before** the current certificate expires, Trustaige creates
   the next one and adds it to the metadata, after the current one. It signs
   nothing yet.
2. **At the moment the current certificate expires**, the new one starts
   signing every response, and moves to the top of the metadata.
3. **For a day after the switch**, the retired certificate stays in the
   metadata, then it's removed.

What the service provider has to do depends on how it got the certificate:

| The service provider… | What to do |
|---|---|
| Reads the **Metadata URL** | Nothing, as long as it refreshes the metadata before the switch |
| Had the certificate pasted in, and can hold several | Add the new certificate any time before the switch. Both are trusted until then, so adding it early is safe |
| Had the certificate pasted in, and holds only one | Replace it at the switch. Until then, responses are still signed with the current certificate |

**In the hub**, while a new certificate is waiting, the app's **Identity
Provider** panel shows **A new signing certificate takes over on** *day*, with
the new certificate below it to copy. For apps that Trustaige configures in
Microsoft Entra, Google Workspace or Cloudflare Access through an integration,
Trustaige delivers the new certificate itself, retrying every hour, and the
panel says when the provider already trusts it.

**By email**, your organization's administrators hear about it about 60, 30
and 7 days before the switch, when at least one SAML app might need the
certificate added by hand. The subject reads **Action needed: update** *n*
**SAML app(s) within** *n* **day(s)**, and the email gives the exact time of
the switch in UTC, the new certificate's SHA-256 fingerprint, and a **Get the
new certificate** link.

## Single logout

A service provider can end the person's Trustaige session by sending a
`LogoutRequest` to the **SLO URL**, by HTTP-Redirect or HTTP-POST. Trustaige
ends the session the request's `SessionIndex` names, and only when its `NameID`
names the same person. It replies with a signed `LogoutResponse` to the
service provider's single logout address, which Trustaige knows only from the
service provider's metadata; without one, the person lands on your
organization's sign-in page instead.

## Check it works

Open the service provider's sign-in and choose single sign-on. You should land
on your organization's Trustaige sign-in, prove it's you with your passkey, and
come back signed in. Each sign-in appears in the hub's **Audit log**.

## Turn it off or remove it

- **Pause sign-in:** switch the app to **Disabled** at the top of its page.
  The service provider then receives a `Requester` status with the message
  `Application is disabled`.
- **Remove it:** in the app's menu, choose **Delete app**. Remove Trustaige
  from the service provider's settings too, so nobody is sent to an app that
  no longer exists.

## Troubleshooting

**“Access denied: you are not assigned to this application”**

The app has groups assigned under **Access**, and the person isn't in any of
them. Add them to one of those groups, or assign a group they're in.

**“no application is configured for this service provider”**

The request's `Issuer` doesn't match the app's **SP Entity ID** in this
organization. Compare them character for character, including any trailing
slash, under **Edit app → SAML Configuration → Entity ID**. Also check the
service provider uses your organization's SSO URL, with your short ID.

**“failed to resolve ACS endpoint: requested ACS URL not registered”**

The request asks for a response at an address the app doesn't have. The ACS URL
the service provider sends must match the app's **ACS URL** exactly.

**“failed to resolve ACS endpoint: no ACS endpoint configured for SP”**

The app was created without an **ACS URL**, so Trustaige has nowhere to post
the response.

**“signature validation failed”**

The service provider signed its request, and Trustaige couldn't verify it.
Turn on **Want AuthnRequests Signed** under **Edit app** and paste the service
provider's current certificate under **SP Signing Certificate**.

**“unsupported NameID format”**

The request's `NameIDPolicy` asks for a format other than `unspecified`,
`emailAddress`, `persistent` or `transient`. Change the format the service
provider requests.

**“This SAML endpoint requires an organization context.”**

The organization short ID in the URL is wrong. Copy the URL again from the
app's **Identity Provider** panel.

**The service provider rejects the signature after a certificate switch**

It still trusts only the retired certificate. Give it the current one from
**Signing Certificate**, or point it at the **Metadata URL** so the next
switch takes care of itself.

**The service provider says the response is expired or not yet valid**

A response is valid from 2 minutes before it was issued until 5 minutes after.
Check the clock on the service provider's server.
