# SAML single sign-on for your app (Okta, Entra ID, Google Workspace)
Source: https://sovaria.in/docs/saml-sso

Let your business customers' staff sign in to your app with their company identity provider over SAML 2.0: one connection per IdP, bound to the email domains it may sign in.

When you sell your app to companies, their IT team will ask for single sign-on. Add a **SAML connection** for each customer's identity provider and name the email domains it signs in. A user who types `ada@acme.com` goes to Acme's Okta, signs in there, and comes back to your app with a normal Sovaria session (MFA rules, session limits and your blocking hooks still apply).

(Diagram: A SAML sign-in to your app)

## Add a connection

- Console › **Authentication › SAML SSO › Add connection**: a name (“Acme (Okta)”), the email domains (`acme.com`), and the IdP's **metadata XML** (or its entity ID, sign-in URL and signing certificate).
- Give the customer's IdP admin the **ACS URL** and **Entity ID** shown for the connection. Name ID format: email address. Sign the response or the assertion (unsigned or encrypted assertions are refused).
- Okta: Applications › Create App Integration › SAML 2.0: “Single sign-on URL” = ACS URL, “Audience URI” = Entity ID. Entra ID: Enterprise application › Single sign-on › SAML: Identifier = Entity ID, Reply URL = ACS URL. Google Workspace: Apps › Web and mobile apps › Add custom SAML app.

JavaScript:
```js
// Sign-in screen: "Continue with SSO"
await sov.auth.signInWithSSO({ email: 'ada@acme.com', redirectTo: 'https://app.example.com/auth/callback' })

// On /auth/callback
await sov.auth.handleEmailLink(window.location.href)   // or exchangeCodeForSession(code)
```

Swift:
```swift
let start = try await sovaria.auth.getSSOSignInURL(email: "ada@acme.com", redirectTo: "com.example.app://auth")
// open start.url in ASWebAuthenticationSession, then:
try await sovaria.auth.exchangeCodeForSession(code)
```

Kotlin:
```kotlin
val start = sovaria.auth.getSSOSignInUrl(email = "ada@acme.com", redirectTo = "com.example.app://auth")
// open start.url in a Custom Tab, then:
sovaria.auth.exchangeCodeForSession(code)
```

Dart:
```dart
final start = await sovaria.auth.getSSOSignInUrl(email: 'ada@acme.com', redirectTo: 'com.example.app://auth');
// open start.url with flutter_web_auth_2, then:
await sovaria.auth.handleDeepLink(Uri.parse(result));
```

HTTP:
```bash
curl "$API/api/v1/auth/sso/saml/lookup?project_id=$PROJECT&email=ada@acme.com"     # → { connection_id }
curl "$API/api/v1/auth/sso/saml/$CONNECTION/authorize?project_id=$PROJECT&mode=json&code_challenge=…"
curl -X POST $API/api/v1/auth/sso/exchange -d '{"code":"…","code_verifier":"…"}'
```

## Users

The first sign-in creates the app user (email verified, no password; your before-create hook runs and can refuse it). If an app user with that email already exists, the SAML identity is linked to it. Pair it with [SCIM provisioning (Okta, Entra ID, Google Workspace)](https://sovaria.in/docs/scim-provisioning) so leavers are switched off the moment the company deactivates them.

## Security

- The response must be signed with the connection's certificate and match the IdP's issuer, our audience and ACS URL, the time window and the sign-in request we sent (InResponseTo). Each assertion works once.
- Only addresses on the connection's domains are signed in, and a domain belongs to one connection per project, so one customer's IdP can't sign in or take over another domain's users.
- Metadata is parsed without entities or network access; certificates are stored encrypted and never returned.
- Only the project's owners and admins can add, change or remove connections.

## Errors back to your app

| `error` on the redirect | Meaning |
|---|---|
| `saml_invalid` | The response failed verification (signature, audience, time, request). Check the certificate and URLs in the IdP. |
| `saml_replayed` | The same assertion was posted twice. |
| `domain_not_allowed` | The signed-in email isn't on the connection's domains. |
| `signup_refused` | Your before-create hook refused the new user. |
| `plan_required` | The organization's plan doesn't include SSO. |

## Your own team (console SSO)

Separately, an organization on Team or Enterprise can make its members sign in to the Sovaria console with SAML: Organization › Settings › Enterprise SAML SSO (paste the IdP metadata or the three fields). That is for your team, not your app's users.

Like app-user SAML, console SSO is bound to the email domains you name, and a sign-in for an address outside them is refused — so your IdP can only vouch for your own people. Only an **organization owner** can set or change the IdP certificate and turn SSO on; admins can view the settings. A member's MFA still applies on top of SSO.

## Plans

|  | Free | Pro | Ultra | Team | Enterprise |
|---|---|---|---|---|---|
| Enterprise SSO (SAML/OIDC) | — | — | — | ✓ | ✓ |
