# Single Sign-On (SSO)

eforms.cloud unterstuetzt drei SSO-Protokolle: LDAP, SAML 2.0 und OIDC (OpenID Connect). SSO ist ab dem Enterprise-Plan verfuegbar.

## Uebersicht

```mermaid
graph TD
    Login["Login-Seite"] -->|Zeigt Provider| API["GET /sso/providers"]
    Login -->|LDAP| LDAP["POST /sso/{id}/login"]
    Login -->|SAML/OIDC| Redirect["GET /sso/{id}/redirect"]
    Redirect -->|Browser| IdP["Identity Provider"]
    IdP -->|Callback| CB["GET/POST /sso/{id}/callback"]
    CB -->|Token| FE["Frontend /admin/login?sso_token=..."]
    LDAP -->|Token| FE2["Response: {token, user}"]
```

## Provider-Verwaltung (Admin)

### CRUD-Endpunkte

| Method | Path | Beschreibung |
|--------|------|--------------|
| `GET` | `/admin/sso-providers` | Alle Provider auflisten |
| `POST` | `/admin/sso-providers` | Provider erstellen |
| `GET` | `/admin/sso-providers/{id}` | Provider-Details |
| `PUT` | `/admin/sso-providers/{id}` | Provider aktualisieren |
| `DELETE` | `/admin/sso-providers/{id}` | Provider loeschen |
| `POST` | `/admin/sso-providers/{id}/test` | Verbindung testen |

Erfordert: `auth:sanctum` + `admin`-Rolle + Plan-Feature `sso`.

### Provider-Typen

Jeder Provider hat einen `provider_type`: `ldap`, `saml` oder `oidc`.

---

## LDAP

### Konfiguration

| Feld | Beschreibung | Beispiel |
|------|--------------|---------|
| `host` | LDAP-Server | `ldap.example.com` |
| `port` | Port | `389` (LDAP), `636` (LDAPS) |
| `use_tls` | TLS verwenden | `true` fuer LDAPS |
| `base_dn` | Basis-DN fuer Suche | `dc=example,dc=com` |
| `bind_dn` | Service-Account DN (optional) | `cn=admin,dc=example,dc=com` |
| `bind_password` | Service-Account Passwort | |
| `search_filter` | LDAP-Suchfilter | `(objectClass=person)` |
| `username_attribute` | Username-Attribut | `uid` oder `sAMAccountName` |
| `email_attribute` | E-Mail-Attribut | `mail` |
| `name_attribute` | Name-Attribut | `cn` oder `displayName` |

### Auth-Flow

```mermaid
sequenceDiagram
    participant U as User
    participant API as POST /sso/{id}/login
    participant LDAP as LDAP Server

    U->>API: {username, password}
    API->>LDAP: Service-Account Bind (falls konfiguriert)
    API->>LDAP: Search: (&(objectClass=person)(uid=username))
    LDAP-->>API: DN des Users
    API->>LDAP: User Bind (DN + Password)
    LDAP-->>API: Bind erfolgreich
    API->>API: findOrCreateUser()
    API->>API: Sanctum Token erstellen
    API-->>U: {token, user}
```

1. Optionaler Service-Account-Bind fuer die Suche
2. User per Suchfilter + Username-Attribut finden
3. User-DN extrahieren
4. User-Bind mit eingegebenem Passwort verifizieren
5. User in lokaler DB finden oder erstellen

### Verbindungstest

`POST /admin/sso-providers/{id}/test` testet:
- LDAP-Verbindung zum Server
- Service-Account-Bind (falls konfiguriert) oder Anonymous Bind

---

## SAML 2.0

### Konfiguration

| Feld | Beschreibung | Beispiel |
|------|--------------|---------|
| `sso_url` | IdP SSO-URL | `https://idp.example.com/sso` |
| `email_attribute` | SAML-Attribut fuer E-Mail | `urn:oid:0.9.2342.19200300.100.1.3` |
| `name_attribute` | SAML-Attribut fuer Name | `urn:oid:2.5.4.3` |

### Auth-Flow

```mermaid
sequenceDiagram
    participant U as User
    participant API as eforms.cloud
    participant IdP as SAML IdP

    U->>API: GET /sso/{id}/redirect
    API->>API: AuthnRequest generieren (SAMLRequest)
    API-->>U: 302 Redirect zu IdP SSO-URL?SAMLRequest=...
    U->>IdP: Anmeldung am IdP
    IdP-->>U: 302 POST /sso/{id}/callback mit SAMLResponse
    U->>API: POST /sso/{id}/callback {SAMLResponse}
    API->>API: XML parsen, NameID + Attribute extrahieren
    API->>API: findOrCreateUser()
    API->>API: Sanctum Token erstellen
    API-->>U: 302 Redirect zu /admin/login?sso_token=...
```

1. `AuthnRequest` wird generiert (ID, IssueInstant, AssertionConsumerServiceURL, Issuer)
2. Base64-codiert als Query-Parameter an die IdP SSO-URL angehaengt
3. IdP authentifiziert den User und sendet `SAMLResponse` an die Callback-URL
4. Response wird Base64-decodiert und als XML geparst
5. `NameID` wird als `external_id` verwendet
6. Konfigurierte Attribute werden fuer E-Mail und Name extrahiert

### Verbindungstest

Prueft ob die IdP SSO-URL erreichbar ist (HTTP GET, Timeout 10s).

---

## OIDC (OpenID Connect)

### Konfiguration

| Feld | Beschreibung | Beispiel |
|------|--------------|---------|
| `client_id` | OAuth Client ID | `eforms-cloud` |
| `client_secret` | OAuth Client Secret | |
| `discovery_url` | OIDC Discovery URL (optional) | `https://idp.example.com/.well-known/openid-configuration` |
| `authorization_url` | Manuell (falls kein Discovery) | `https://idp.example.com/authorize` |
| `token_url` | Token-Endpunkt | `https://idp.example.com/token` |
| `userinfo_url` | UserInfo-Endpunkt | `https://idp.example.com/userinfo` |
| `scopes` | OAuth Scopes | `openid profile email` |
| `email_claim` | Claim fuer E-Mail | `email` |
| `name_claim` | Claim fuer Name | `name` |

### Auth-Flow

```mermaid
sequenceDiagram
    participant U as User
    participant API as eforms.cloud
    participant IdP as OIDC Provider

    U->>API: GET /sso/{id}/redirect
    API->>API: State generieren, in Session speichern
    API-->>U: 302 Redirect zu authorization_url?client_id=...&scope=...&state=...
    U->>IdP: Anmeldung am IdP
    IdP-->>U: 302 Redirect zu /sso/{id}/callback?code=...&state=...
    U->>API: GET /sso/{id}/callback?code=...
    API->>IdP: POST token_url {grant_type=authorization_code, code, redirect_uri, client_id, client_secret}
    IdP-->>API: {access_token, id_token}
    API->>IdP: GET userinfo_url (Bearer access_token)
    IdP-->>API: {sub, email, name}
    API->>API: findOrCreateUser()
    API-->>U: 302 Redirect zu /admin/login?sso_token=...
```

1. Authorization Code Flow mit State-Parameter
2. Discovery-URL wird (falls konfiguriert) fuer Endpoint-Aufloesung verwendet
3. Code-Exchange: Authorization Code → Access Token + ID Token
4. User-Info: Primaer via UserInfo-Endpunkt, Fallback auf ID Token Claims
5. `sub`-Claim wird als `external_id` verwendet

### Verbindungstest

- Mit Discovery-URL: Prueft ob Discovery-Dokument erreichbar ist, zeigt Issuer
- Ohne Discovery: Prueft ob `authorization_url` konfiguriert ist

---

## User-Linking und -Erstellung

Der `SsoService.findOrCreateUser()` implementiert eine dreistufige Logik:

### 1. Match by Provider + External ID

```sql
SELECT * FROM users WHERE sso_provider_id = ? AND external_id = ?
```

Bei Treffer: Name und E-Mail werden aktualisiert (falls geaendert).

### 2. Match by E-Mail (Account-Linking)

```sql
SELECT * FROM users WHERE email = ?
```

Bei Treffer: `sso_provider_id` und `external_id` werden auf dem bestehenden Account gesetzt. Der User kann sich fortan via SSO anmelden.

### 3. Neuer User

Kein Match → Neuer User wird erstellt:
- Rolle: `user`
- Passwort: Zufaellig (64 Zeichen, bcrypt) - User meldet sich ausschliesslich via SSO an
- `sso_provider_id` und `external_id` werden gesetzt

## Fehlerbehandlung

### Login-Fehler

- LDAP: `401` mit `"Invalid LDAP credentials"`
- SAML/OIDC: Redirect zu `/admin/login?sso_error=auth_failed`

### Audit-Logging

Alle SSO-Login-Versuche werden protokolliert:
- `sso_login_success` - Provider-ID + Typ
- `sso_login_failed` - Provider-ID + Typ + Grund (invalid_credentials, external_auth_failed)
