# Single sign-on with SAML

> Let the people in an Enterprise workspace sign in with your company identity provider over SAML 2.0. Which values go where for Microsoft Entra ID and Okta, how to prove your domain with a DNS record, how to rotate the signing certificate, how SSO-only mode keeps an owner break-glass, and what each refused sign-in means.

Single sign-on (SSO) lets the people in your workspace sign in to Badge with the account your company already uses. You connect your identity provider, the system that already holds your staff accounts, to Badge over SAML 2.0. Sign-in starts at Badge. Your identity provider checks the person, and Badge signs them in. If they are not yet members of the workspace, Badge adds them with the default role you chose.

SSO is available only when it is switched on for your Badge deployment. While it is off, the SSO settings and the sign-in endpoints are not available. If your company uses Google Workspace, see [Google Workspace sign-in](/docs/guides/sso-google). The two can be used in the same workspace.

Before you start [#before-you-start]

* **You must be the workspace owner.** Only owners can set up, change, or turn off SSO.
* **Your workspace must be on the Enterprise plan.** SSO is an Enterprise feature, as shown on [Pricing](/docs/guides/pricing). Both the workspace and your own subscription must be on Enterprise. On any other plan, the Single sign-on section says the plan does not include SSO, and saving settings or verifying a domain is refused with `plan_sso_not_included`. If your plan changes after you set SSO up, see [If your plan changes](#if-your-plan-changes).
* **You need administrator access to your identity provider** (Microsoft Entra ID, Okta, or another SAML 2.0 provider). You will create an application there and copy values from it.
* **Decide your default role.** People who sign in for the first time are added with this role, which is `Member` or `Viewer`. Owners and Admins are never granted by SSO.
* **You need access to the DNS for your company domain.** You will add one TXT record to prove that you control the domain. See [Prove your domain with DNS](#prove-your-domain-with-dns).

How the pieces fit together [#how-the-pieces-fit-together]

1. In Badge, open the **Single sign-on** section on the **Members** tab of the [Team page](/team), and choose SAML. Badge shows the values for your identity provider, with copy buttons, and the domain it will claim.
2. In your identity provider, create a SAML application. Give it the values Badge shows: the identifier, the sign-in response (ACS) URL, and the email name ID format.
3. Back in Badge, paste the values your identity provider gives you: its entity ID, its sign-in URL, and its signing certificate. Save.
4. In Badge, add the DNS record for each domain, then select **Verify now**.
5. In your identity provider, assign the people who should have access.
6. Members sign in from the Badge sign-in page. They choose **Sign in with SSO**, enter their work email, and continue to your identity provider. Badge uses only the domain part of the address to find your workspace.

Prove your domain with DNS [#prove-your-domain-with-dns]

A workspace can sign people in only on domains it has proved it controls.

* **The domain you claim.** On your first save, Badge claims the domain of your own email address. Each domain needs its own DNS record.
* **If your account uses a personal address** (such as `gmail.com` or `outlook.com`), or your email address is not verified yet, Badge cannot claim that domain. The Single sign-on section says so, names the domain, and asks for a **Company domain**: enter the domain your members sign in with, such as `example.com`. Save stays unavailable until you enter one. Badge claims that domain instead, and you prove it with its DNS record like any other.
* **The record.** After you save, Badge shows a TXT record for each domain:

  | Field | Value                                                                               |
  | ----- | ----------------------------------------------------------------------------------- |
  | Name  | `_badge-sso.example.com` (the domain you claimed)                                   |
  | Type  | `TXT`                                                                               |
  | Value | `badge-sso-verify=` followed by a token derived from your workspace and that domain |

  Copy the values exactly as Badge shows them. The token is unique to your workspace and that domain. Claiming the domain again shows the same record, so a record you have already published stays valid.
* **Verify.** Add the record where you manage the domain's DNS, then select **Verify now** on the Team page. DNS changes can take up to an hour to show. If the record is not visible yet, Badge says so and you can try again. If Badge cannot look up DNS at that moment, it says so too, and you can try again in a minute.
* **Until it is verified,** single sign-on stays off for that domain. People with addresses on it cannot sign in through your identity provider.
* **A domain another workspace has verified cannot be claimed.** A claim by another workspace that has not been verified can be displaced: when you claim the domain, that workspace's pending claim is removed, and its owners see the displacement in their audit log. Whichever workspace proves the domain with DNS keeps it. If the displaced workspace publishes its record and verifies later, it takes the domain back.
* **Consumer mailbox domains are refused.** Domains such as `gmail.com` and `outlook.com` cannot be claimed at all. That is an extra guard. The DNS record is what proves ownership.
* **Limits.** A workspace can make five new domain claims an hour. A sixth attempt is refused with a message to try again in an hour. A domain whose record name would be longer than 253 characters cannot be saved.

The values Badge needs [#the-values-badge-needs]

Replace `{team_id}` with your workspace id, or copy the full URLs from the Single sign-on section.

| Value                                | What it is                                                                                                                                      |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Identifier (SP entity ID)            | `https://api.badgeia.com/api/v1/auth/saml/{team_id}/metadata`. The same value as the metadata URL. Badge identifies itself by it.               |
| Metadata URL                         | `https://api.badgeia.com/api/v1/auth/saml/{team_id}/metadata`. An XML document that describes Badge as a SAML service provider.                 |
| Assertion Consumer Service (ACS) URL | `https://api.badgeia.com/api/v1/auth/saml/{team_id}/acs`. Where your identity provider sends its sign-in response, using the HTTP POST binding. |
| Sign-in URL                          | `https://api.badgeia.com/api/v1/auth/saml/{team_id}/login`. Where your people start a sign-in.                                                  |

Three settings matter beyond the URLs:

* **Name ID format:** `emailAddress`. Badge identifies the person by their email address, so the name ID must carry it.
* **Signed assertions:** required. Badge refuses an unsigned assertion, or one whose signed content has been moved or altered.
* **Signature algorithm:** SHA-256. Badge refuses a SHA-1 signature. See [Weak signature algorithms](#weak-signature-algorithms).
* **Badge's own signing certificate:** none. Badge does not sign its requests, so there is nothing to upload to your identity provider.

Microsoft Entra ID [#microsoft-entra-id]

Entra ID labels change from time to time. The fields below are what matters.

1. In the Microsoft Entra admin center, go to **Enterprise applications**, choose **New application**, then **Create your own application**. Choose the non-gallery option and name it for your workspace, for example "Badge".
2. Open the application's **Single sign-on** page and choose **SAML**.
3. In **Basic SAML Configuration**, set:
   * **Identifier (Entity ID)** to the metadata URL above.
   * **Reply URL (Assertion Consumer Service URL)** to the ACS URL above.
   * **Sign on URL** to the sign-in URL above. Members who open Badge from the My Apps tile then start at Badge, which is the sign-in Badge accepts.
4. In **Attributes & Claims**, set the &#x2A;*Unique User Identifier (Name ID)** to the user's email address (`user.mail`), with the format set to email address.
5. In **SAML Certificates**, choose a signing option that signs the assertion, and make sure the signature algorithm is SHA-256. Then download the &#x2A;*Certificate (Base64)**.
6. From the **Set up** section, copy the **Login URL** into Badge as the identity provider's sign-in URL, and the **Microsoft Entra Identifier** as its entity ID. Paste the certificate you downloaded into Badge's signing certificate field.
7. Under **Users and groups**, assign the people or groups who should have access. Entra blocks sign-in for anyone who is not assigned.

Okta [#okta]

Okta labels change from time to time. The fields below are what matters.

1. In the Okta admin console, go to **Applications**, choose **Create App Integration**, and select **SAML 2.0**.
2. Name the application for your workspace, for example "Badge".
3. In the SAML settings, set:
   * **Single sign-on URL** to the ACS URL above. Use it for the recipient and destination URLs if Okta asks.
   * **Audience URI (SP Entity ID)** to the metadata URL above.
   * **Name ID format** to `EmailAddress`, and **Application username** to `Email`.
   * **Assertion Signature** to **Signed**.
4. Finish the wizard. Open the application's **Sign On** tab and choose **View SAML setup instructions**. Copy the **Identity Provider Single Sign-On URL** into Badge as the sign-in URL, the **Identity Provider Issuer** as the entity ID, and the **X.509 Certificate** as the signing certificate.
5. Under **Assignments**, assign the people or groups who should have access.

Start sign-in from Badge [#start-sign-in-from-badge]

Badge accepts only sign-ins it started. Tell your people to sign in from Badge's sign-in page with **Sign in with SSO**.

Do not point an identity-provider dashboard tile at the SAML sign-in URL. A sign-in that starts from another site is refused. The person lands on the sign-in page with a generic error, and the audit log records the reason as `cross_site_start`. Point the tile at Badge's sign-in page instead. Setting Entra's sign-on URL as above does that for the My Apps tile.

A sign-in link that someone opens from outside any web page, such as a link in a mail or chat app, a bookmark, or a typed address, first shows a Badge page that says it is a company sign-in link for Badge and that nothing happens until the person continues. Nothing goes to the identity provider until they choose **Continue to company sign-in**. And if the browser is already signed in to Badge as someone else when an SSO sign-in finishes, Badge asks "You are signed in as … Continue as …?" and keeps the current account unless the person chooses to continue. Together these stop a stranger's identity provider from signing a browser in to the stranger's account behind the person's back.

Weak signature algorithms [#weak-signature-algorithms]

If your identity provider signs with SHA-1, Badge refuses the sign-in. The person sees `sso_failed`. The owner finds the reason `deprecated_algorithm` in the details of the `sso.login_failed` entry in the audit log. Set your identity provider to sign with SHA-256. Microsoft Entra ID and Okta both sign with SHA-256 by default. For any other provider, check its signature algorithm setting before you test.

Rotate the signing certificate [#rotate-the-signing-certificate]

Badge stores up to two signing certificates for your workspace, a current one and a next one. While both are stored, Badge accepts a signature made by either, so you can change certificates without locking anyone out. Badge also refuses a certificate that has expired, or that uses a key too weak to sign assertions.

1. In your identity provider, generate a new signing certificate, but do not activate it yet. Entra calls this **New Certificate**. Okta calls it **Generate new certificate**.
2. Copy the new certificate's text.
3. In Badge, open the Single sign-on section on the Members tab of the Team page, paste the new certificate into the **next** certificate slot, and save.
4. In your identity provider, activate the new certificate.
5. Sign in as a member, and confirm that SSO works.
6. Back in Badge, remove the old certificate, so that only the new one remains.

Do not remove the old certificate from Badge before your identity provider has switched to the new one. Each save is recorded in the audit log as `sso.config_updated`.

SSO-only mode and owner break-glass [#sso-only-mode-and-owner-break-glass]

SSO-only mode requires admins, members, and viewers to sign in through SSO. Their password and GitHub sign-in is refused. Owners are exempt and keep password and GitHub sign-in, so if your identity provider is down, an owner can get back in.

Before you turn SSO-only on:

1. Confirm that at least one owner can sign in with a password or GitHub. A person who joined through SSO may not have a password.
2. Sign in as a member through SSO, with SSO-only still off, and confirm that it works.
3. Turn SSO-only on.

When you turn SSO-only on, Badge ends the existing sessions of admins, members, and viewers. They sign straight back in through SSO. Owners keep their sessions. Until SSO-only is on, members can still sign in with a password.

Removing people [#removing-people]

When you remove someone from the workspace, Badge removes the link between their account and your identity provider. Their next SSO sign-in is then refused with `sso_account_unavailable` until you invite them again. If they have a pending invitation, signing in through SSO accepts it, and they join with the role on that invitation. A pending invitation that someone else has already used does not admit a person through SSO. To stop access altogether, remove them in your identity provider as well.

A person whose Badge account signs in with a password or GitHub, and who was removed from the workspace, is refused with `sso_account_conflict`. Badge never takes over an existing account.

Connecting an existing account [#connecting-an-existing-account]

When someone who already has a Badge account (with a password or a GitHub sign-in) signs in through your identity provider for the first time, Badge does not connect the two on its own. After the identity provider, Badge shows a page that names the workspace, the identity provider and the account, and asks the person to confirm it is them by entering their current Badge password, or by signing in with GitHub. Only then does Badge connect the account to your identity provider and sign them in. Owners confirm the same way. From then on, signing in through your identity provider is enough.

The page tells the person exactly what connecting does: your company's identity provider will be able to sign in to this Badge account. In a workspace that is not SSO-only, it adds that their Badge password keeps working. In an SSO-only workspace, it says instead that after they confirm, their Badge password and GitHub sign-in will no longer work for that workspace, and that owners keep theirs as a break-glass. If they have been invited but have not joined yet, it also says that confirming joins them to the workspace, with the role from the invitation. For an owner, the page says that they can turn SSO off in Team settings; for everyone else it says to ask an owner or admin to remove them.

The confirmation page is valid for 10 minutes and allows one attempt, and it says so before the person types. A wrong password ends it and counts toward that account's sign-in lockout, the same one the sign-in page uses: after five failed attempts for the account, from any network, its password sign-in is paused for 15 minutes, on this page and on the sign-in page alike. Resetting the password with &#x2A;*Forgot your password?** lets you set a new password and lifts the pause for that account, but if someone keeps trying wrong passwords against your email address, the pause can return. If that keeps happening, contact support. Colleagues who share the office network are not affected by someone else's failed attempts, unless that network fails across many accounts: Badge separately pauses sign-in from a network address that fails across many accounts in a short time. To try again, the person opens the company sign-in link again, or chooses **Sign in with SSO** on Badge's sign-in page. The page has a &#x2A;*Forgot your password?** link that opens in a new tab.

There is no button for a member to disconnect their own account later. When an owner or admin removes the person from the workspace, Badge removes the connection too. Turning SSO off does not remove it.

Text you can send to your people before you turn SSO on. Fill in the name of the person to ask:

> The first time you sign in to Badge through our company sign-in, Badge asks you to confirm that the Badge account is yours, using the password you already use for Badge (or GitHub, if that is how you sign in). You have one attempt and 10 minutes, so if you don't remember your password, reset it with "Forgot your password?" first. "Connect" means that, after you confirm, our company sign-in can sign you in to that Badge account. If it doesn't work, open the company sign-in link again, or choose "Sign in with SSO" on Badge's sign-in page. To disconnect later, ask NAME to remove you from the workspace. If you get stuck, ask NAME.

If you have turned SSO-only on, add this sentence, so nobody is surprised later:

> Once you have confirmed, you sign in to Badge only through our company sign-in; your Badge password and GitHub sign-in will no longer work for our workspace.

Accounts that your identity provider created have nothing to confirm with and are not asked. A refused confirmation is recorded in the audit log as `sso.login_failed` with the reason `consent_refused`, and a confirmed one as `sso.identity_linked`.

Turning SSO off [#turning-sso-off]

Turning SSO off removes the identity provider settings and the domains from the workspace. Members go back to signing in with a password or GitHub. Owners keep their sign-in methods throughout.

The confirm dialog shows how many members joined through SSO and have no password. Those members must use **Forgot password** on the sign-in page to set one before they can sign in again. Forgot password works for accounts created through SSO. It is also the way back after a rollback that removes SSO. An account that only ever signed in with GitHub cannot reset a password. Turning SSO back on with the same identity provider needs no re-linking.

If your plan changes [#if-your-plan-changes]

If your workspace leaves the Enterprise plan while SSO is set up, the people who are already members keep signing in as before, through SSO or, if you have not required SSO, with their password or GitHub. Nobody new can join the workspace while it is below Enterprise: SSO adds no one, and you cannot invite anyone, send an invitation again, or have a pending invitation accepted. This also covers people whose email address is not on your SSO domain. To invite people again, move back to Enterprise or turn SSO off. Pending invitations are kept, and work again if you do either before they expire.

You can still make changes that reduce what SSO allows: turn off **Require single sign-on for all members**, replace the signing certificate (see [Rotate the signing certificate](#rotate-the-signing-certificate)), remove a domain, or turn SSO off. Every other change, such as a new identity provider, a new sign-in URL, a new domain, or a different default role, needs the Enterprise plan. On the Team page, the Single sign-on section still shows every setting, but only the ones you can still change are editable; the others say they need the Enterprise plan. Each domain has its own **Remove** button, which asks you to confirm. Saving other changes keeps your domains as they are. Turning SSO off is one-way below Enterprise: turning it back on needs the Enterprise plan.

Replacing the signing certificate means Badge will trust whoever holds the new certificate's private key to sign in as any member who already signs in through SSO. Badge cannot check that the new certificate belongs to the same identity provider. It records the change in the audit log with the old and new certificate fingerprints. Only paste a certificate your identity provider issued.

What v1 does not support [#what-v1-does-not-support]

* **IdP-initiated sign-in.** Sign-in must start at Badge. See above.
* **SCIM.** Badge does not create or remove people from your identity provider's directory.
* **Role mapping from attributes.** Roles come from the default role, and from role changes you make in Badge's Members tab. A group or attribute in your identity provider does not set a role.
* **More than one SSO workspace per person.** A person can belong to one SSO workspace in v1.

If sign-in fails [#if-sign-in-fails]

When sign-in is refused, you return to the login page with an error code in the address, for example `sso_expired`. The codes are:

| Code                      | What it means                                                                                                                                                                                                                                  | What to do                                                                                                                                                                  |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sso_expired`             | The sign-in took too long to finish, the sign-in was started in another tab that has since been used, or the identity provider's response is outside a two-minute clock window.                                                                | Start again from Badge's sign-in page. If it happens to everyone, check your identity provider's clock.                                                                     |
| `sso_failed`              | Badge refused the identity provider's response. Causes include an unsigned or altered response, a certificate Badge does not hold, a repeated response, a response addressed to another service, and a weak signature algorithm such as SHA-1. | An owner checks the `sso.login_failed` entry in the audit log, and the identity provider's signing and audience settings.                                                   |
| `sso_unavailable`         | Badge could not finish the sign-in because a short-lived store it depends on is down. The sign-in is refused, not allowed through.                                                                                                             | Try again shortly. If it keeps happening, contact support.                                                                                                                  |
| `sso_not_enabled`         | SSO is not set up for this workspace, or the workspace is not on the Enterprise plan, so SSO cannot add a new member.                                                                                                                          | An owner completes the Single sign-on settings on the Members tab of the Team page.                                                                                         |
| `sso_required`            | The workspace is SSO-only and this person is not an owner. Sign-in with GitHub, or with a password, ends with the same message.                                                                                                                | Sign in through SSO.                                                                                                                                                        |
| `sso_domain_not_allowed`  | The address is not on a domain the workspace has verified with DNS.                                                                                                                                                                            | Use your work address. An owner checks the domain and its DNS record on the Team page.                                                                                      |
| `sso_account_conflict`    | An existing Badge account has this email, but it is not already an accepted member of this workspace with a verified email, or it is linked to another SSO workspace. Badge never takes over an existing account.                              | If the person should be in this workspace, an owner invites that account. Once it is an accepted member, SSO links to it.                                                   |
| `sso_account_unavailable` | The Badge account is deactivated, or the person was removed from this workspace and has not been invited again.                                                                                                                                | For a removed member, an owner invites them again. For a deactivated account, contact support.                                                                              |
| `sso_consent_refused`     | The person did not confirm the connection to an existing account: the password was wrong, or they signed in with a different GitHub account. Nothing was connected.                                                                            | Open the company sign-in link again, or choose **Sign in with SSO** on Badge's sign-in page, and confirm with the current password or GitHub account of that Badge account. |
| `sso_seat_limit`          | The workspace has no free seat for a new member. Pending invitations hold seats too.                                                                                                                                                           | An owner frees a seat, by removing a member or cancelling an invitation.                                                                                                    |

Badge records the specific reason for a refused sign-in in the audit log, from a fixed list. The reason is never shown to the person, so the login page cannot be used to probe your configuration. If many people hit the same problem at once, the audit log keeps one entry a minute for each reason, with a `suppressed` count for the rest.

What gets recorded [#what-gets-recorded]

These entries appear in the [audit log](/docs/guides/audit-log) for the workspace:

* `sso.config_updated`: an owner changed the SSO settings, or turned SSO off.
* `sso.domain_verified`: a claimed domain was verified with its DNS record.
* `sso.domain_claim_displaced`: a pending claim on a domain was replaced by another workspace's claim.
* `sso.login_succeeded`: a person signed in through SSO.
* `sso.login_failed`: a sign-in through SSO was refused. The entry holds the reason only.
* `sso.user_provisioned`: a person's first SSO sign-in created their account or added them to the workspace.
* `sso.identity_linked`: an existing Badge account was connected to the workspace's SSO. The entry records whether the person confirmed it (`consent`) and how (`method`: `password` or `github`). When an existing connection moved to a new identity from the same workspace, for example after you switched identity providers, it records `replaced` and the previous provider.
