TL;DR
Die Implementierung von Enterprise Single Sign-On erfordert eine fundierte Entscheidung zwischen SAML 2.0 und OpenID Connect, wobei jedes Protokoll spezifische Stärken und Anwendungsfälle aufweist. SAML 2.0, entwickelt 2005, eignet sich mit seinem XML-Format und breiter App-Kompatibilität besonders für Legacy-Enterprise-Anwendungen wie SAP oder Salesforce. OpenID Connect (OIDC), seit 2014 auf OAuth 2.0 basierend, nutzt JSON/JWT und ist ideal für moderne Web- und Mobile-Apps sowie APIs. Der Artikel beleuchtet detailliert den SP-initiierten SAML-Ablauf und den empfohlenen OIDC Authorization Code Flow mit PKCE, inklusive der Struktur von JWT-Tokens. Zudem werden kritische Sicherheitsprobleme wie XML-Signature-Wrapping bei SAML und deren Schutzmaßnahmen aufgezeigt.
Diese Zusammenfassung wurde KI-gestützt erstellt (EU AI Act Art. 52).
Inhaltsverzeichnis (7 Abschnitte)
Single Sign-On ist heute Standard in Unternehmensumgebungen - aber die Implementierung birgt zahlreiche Fallstricke. Ob SAML oder OIDC, Keycloak oder Azure AD: jede Entscheidung hat Konsequenzen für Sicherheit, Usability und Wartbarkeit. Dieser Guide zeigt die vollständige SSO-Implementierung.
SAML 2.0 vs. OpenID Connect: Die Wahl
Welches Protokoll für welchen Use Case?
SAML 2.0 (Security Assertion Markup Language):
- Entwickelt: 2005 (für Browser-basierte Enterprise-Apps)
- Format: XML (komplex, aber vollständig)
- Stärken: Enterprise-Standard, breite App-Kompatibilität
- Schwächen: kein nativer Mobile-Support, XML-Overhead
- Typische Use Cases: SharePoint, SAP, Salesforce, ServiceNow, Legacy Enterprise Applications, Behörden und stark regulierte Branchen
OpenID Connect (OIDC):
- Entwickelt: 2014 (über OAuth 2.0 aufgebaut)
- Format: JSON/JWT (einfach, flexibel)
- Stärken: Modern, Mobile-freundlich, APIs, SPA
- Schwächen: Jüngere Apps fehlen teils noch
- Typische Use Cases: Web-Apps (React, Vue, Angular), Mobile Apps (iOS, Android), APIs und Microservices, Google/GitHub/Microsoft Auth
Entscheidungsmatrix
| App-Typ | SAML | OIDC |
|---|---|---|
| Browser-SPA | nein | ja |
| Mobile App | nein | ja |
| Legacy Enterprise | ja | selten |
| Microservices | nein | ja |
| Office 365 | beide | beide (OIDC bevorzugt) |
| SAP/Oracle | ja | nein |
SAML 2.0 im Detail
SAML-Ablauf (SP-initiiert - Standard)
Akteure:
- User: Benutzer (Browser)
- SP: Service Provider = App (z.B. Salesforce)
- IdP: Identity Provider = Auth-Server (z.B. Keycloak, AD FS)
SP-initiierter Flow:
- User → Salesforce.com/protected
- Salesforce: kein gültiges SAML-Session → redirect zu IdP
- Redirect-URL enthält SAMLRequest (base64-encoded XML):
https://sso.company.com/saml/...?SAMLRequest=...&RelayState=... - IdP: zeigt Login-Seite (wenn kein Session)
- User: Username + Passwort + MFA eingeben
- IdP erstellt SAML Assertion (signiert mit Private Key):
<saml:Assertion>
<saml:AttributeStatement>
<saml:Attribute Name="email">
<saml:AttributeValue>user@company.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="groups">
<saml:AttributeValue>Salesforce-Admins</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
<saml:AuthnStatement SessionIndex="_abc123">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
</saml:Assertion>
- IdP: POST zur ACS-URL des SP mit SAMLResponse
- SP: validiert Signatur mit IdP-Zertifikat
- SP: erstellt lokale Session für User → Zugang gewährt!
Wichtige SAML-Parameter
| Parameter | Beschreibung |
|---|---|
| NameID Format | emailAddress (empfohlen), persistent, transient |
| ACS-URL | Assertion Consumer Service URL des SP |
| Entity ID | Eindeutige SP-Identifikation |
| RelayState | Ursprüngliche URL nach Auth |
| NotBefore/After | Zeitfenster der Assertion (max. 5 Min.!) |
SAML-Sicherheitsprobleme
1. XML-Signature-Wrapping (XSW):
- Angreifer wickelt bösartige Assertion um legitime
- SP validiert legitime, aber nutzt bösartige!
- Schutz: xpath-basierte Signatur-Validierung + Zertifikat-Pinning
2. Assertion-Replay:
- Gültige Assertion = nochmal verwendbar
- Schutz: InResponseTo + Assertion-ID-Cache (1h)
OpenID Connect (OIDC) und OAuth 2.0
Authorization Code Flow mit PKCE (Empfehlung 2024)
Akteure:
- User: Endnutzer
- Client: Web-App / Mobile App
- Authorization Server: IdP (Keycloak, Auth0, etc.)
- Resource Server: API (mit Access Token geschützt)
# Client generiert code_verifier:
# code_verifier = random(43-128 Zeichen URL-safe)
# code_challenge = BASE64URL(SHA256(code_verifier))
# Schritt 1: Authorization Request:
GET https://sso.company.com/auth/realms/company/protocol/openid-connect/auth
?response_type=code
&client_id=my-app
&redirect_uri=https://app.company.com/callback
&scope=openid profile email groups
&state=random_state_value # CSRF-Schutz
&code_challenge=abc123... # PKCE
&code_challenge_method=S256
# Schritt 2: User authentifiziert sich am IdP
# IdP leitet zurück zu:
# https://app.company.com/callback?code=AUTH_CODE&state=random_state_value
# Schritt 3: Token Exchange (Server-zu-Server!):
POST https://sso.company.com/.../token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://app.company.com/callback
&client_id=my-app
&client_secret=...
&code_verifier=original_verifier # PKCE-Verifikation
# Antwort:
{
"access_token": "eyJ...", # JWT, gültig 5 Min.
"id_token": "eyJ...", # User-Info, signiert
"refresh_token": "...", # Für Token-Erneuerung
"expires_in": 300
}
JWT-Aufbau (Access Token)
// Header:
{"alg": "RS256", "typ": "JWT", "kid": "key-id-1"}
// Payload:
{
"iss": "https://sso.company.com/realms/company",
"sub": "user-uuid-123",
"aud": ["my-app", "account"],
"exp": 1709550000,
"iat": 1709549700,
"email": "user@company.com",
"groups": ["admins", "developers"],
"preferred_username": "john.doe"
}
// Signatur: RS256 mit IdP-Private-Key
JWT-Validierung (Server-Side)
# Python (PyJWT):
import jwt
from cryptography.hazmat.primitives import serialization
# JWKS-Endpoint: public keys des IdP
jwks_uri = "https://sso.company.com/realms/company/protocol/openid-connect/certs"
jwks_client = jwt.PyJWKClient(jwks_uri)
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="my-app",
issuer="https://sso.company.com/realms/company"
)
# → Automatisch: exp, iat, iss, aud geprüft!
Keycloak als Open-Source Identity Provider
Installation (Docker)
# docker-compose.yml:
services:
keycloak:
image: quay.io/keycloak/keycloak:24.0.2
command: start-dev --import-realm
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${KC_DB_PASSWORD}
KC_HOSTNAME: sso.company.com
KC_PROXY: edge # Hinter Nginx/Traefik!
volumes:
- ./realm-export.json:/opt/keycloak/data/import/realm.json
Realm-Konzept
- Realm “company”: für alle Unternehmensanwendungen
- Realm “customers”: für Kunden-SSO (separater Bereich!)
- Kein Realm “master” für Anwendungen verwenden! (nur Admin)
Client-Konfiguration (OIDC-Web-App)
Client ID: my-app
Client Protocol: openid-connect
Access Type: confidential (mit Client Secret)
Valid Redirect URIs: https://app.company.com/*
Web Origins: https://app.company.com
Standard Flow: ON (Authorization Code)
Direct Access: OFF (kein Username+Password direkt!)
# Client Secret anzeigen:
Clients → my-app → Credentials → Client secret
Rollen und Gruppen
# Realm-Rollen (für alle Clients):
admin, user, developer, viewer
# Client-Rollen (app-spezifisch):
my-app: admin, user, read-only
# Composite Roles (Rollengruppen):
developer = user + deployer + read-only
# Gruppen → Rollen mapping:
Gruppe "Engineering" → hat Realm-Rolle "developer"
→ Alle Gruppen-Mitglieder erhalten automatisch developer-Rechte
Attribute Mapper (Custom Claims in Token)
# Mapper: Gruppen im Token:
Name: groups
Mapper Type: Group Membership
Token Claim: groups
Full group path: ON (z.B. /Engineering/Backend)
# Mapper: Abteilung aus User-Attribut:
Name: department
Mapper Type: User Attribute
User Attribute: department (custom Feld in User-Profil)
Token Claim: department
Active Directory Integration
LDAP/AD-Integration in Keycloak
User Federation → Add Provider → ldap
LDAP-Konfiguration:
Vendor: Active Directory
Connection URL: ldaps://dc01.company.local:636
Bind DN: CN=keycloak-svc,OU=ServiceAccounts,DC=company,DC=local
Bind Credential: [Service-Account-Passwort]
User DN: OU=Users,DC=company,DC=local
User Object Classes: person, organizationalPerson, user
Import Users: ON (oder Read-Only Sync)
# LDAP-Filter (nur aktive Accounts):
Custom User LDAP Filter: (&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
Gruppen-Sync
Mapper → group-ldap-mapper:
LDAP Groups DN: OU=SecurityGroups,DC=company,DC=local
Group Object Classes: group
Membership LDAP Attribute: member
Group Name LDAP Attribute: cn
# AD-Gruppe "SSO-Admins" → Keycloak-Rolle "admin"
Group Roles Mapper: SSO-Admins → admin
Sync-Strategie
# Sync-Intervalle:
# Initial Sync: alle 4h (Full Sync)
# Changed Users Sync: alle 15 Min. (Changed Sync)
# Via Admin CLI:
kcadm.sh trigger-ldap-full-sync --realm company \
--federation-id ldap-provider-id
Kerberos/SPNEGO (Seamless SSO für Windows-Clients):
- Keycloak mit Kerberos konfigurieren
- Windows-Nutzer im Firmennetz: kein Login nötig!
- Browser sendet automatisch Kerberos-Ticket an Keycloak
- Funktioniert: Chrome, Edge (Firefox braucht Konfiguration)
MFA und Adaptive Authentication
TOTP einrichten
Authentication → Policies → OTP Policy:
OTP Type: TOTP
OTP Hash Algo: SHA1 (Compat) oder SHA256 (sicher)
Period: 30 Sekunden
Initial Counter: 0
# Flow: Browser-MFA erzwingen:
Authentication → Flows → Browser:
✓ Username Password Form (REQUIRED)
✓ OTP Form (REQUIRED) ← hinzufügen!
FIDO2/WebAuthn
# Keycloak 21+: WebAuthn nativer Support!
Authentication → Policies → WebAuthn Policy:
Relying Party Entity Name: company.com
Attestation Conveyance: direct
Authenticator Attachment: platform (nur Gerät) | cross-platform (externe Keys)
User Verification Requirement: required
Adaptive Authentication (Risikobasiert)
Beispiel-Regeln:
- Neues Gerät + neuer Standort → MFA erzwingen
- VPN-IP-Range → kein MFA nötig (trusted network)
- Fehlgeschlagene Logins > 3 → CAPTCHA
- Außerhalb Geschäftszeiten → MFA + Alert
# Conditional Flow (Keycloak eingebaut):
Condition - User Configured:
→ Prüft ob User TOTP hat
→ Wenn ja: TOTP anfordern
→ Wenn nein: TOTP einrichten (enrollment flow)
SSO-Migration ohne User-Disruption
Phasen-Migration (Zero-Downtime)
Phase 1: IdP-Deployment (keine User-Auswirkung)
- Keycloak installieren und konfigurieren
- AD/LDAP-Sync einrichten
- Test-Apps konfigurieren
- Pilot-Gruppe: IT-Team (~10 User)
Phase 2: Neue Apps direkt mit SSO (kein Migration nötig)
- Alle neuen Anwendungen ab jetzt → SSO-only
- Kein Login-Formular mehr im Code!
Phase 3: Bestehende Apps migrieren
Just-In-Time Provisioning:
- User loggt sich das erste Mal via SSO ein
- App prüft: Konto mit dieser E-Mail vorhanden?
- Wenn ja: bestehenden Account mit SSO verknüpfen
- Wenn nein: neues Konto anlegen
- User merkt (fast) nichts!
def handle_oidc_callback(user_info):
email = user_info['email']
user = User.find_by_email(email)
if not user:
user = User.create(email=email, name=user_info['name'])
# SSO-Sub-ID speichern für zukünftige Logins:
user.sso_sub = user_info['sub']
user.save()
session['user_id'] = user.id
Phase 4: Passwort-Logins deaktivieren
- Nach 90 Tagen: lokale Passwörter deaktivieren
- Exception-Prozess für Notfälle (Service-Accounts etc.)
Rollback-Plan
- Feature-Flag:
SSO_ENABLED = false→ lokaler Login als Fallback - Nach erfolgreicher Migration: Feature-Flag entfernen
Nächster Schritt
Unsere zertifizierten Sicherheitsexperten beraten Sie zu den Themen aus diesem Artikel — unverbindlich und kostenlos.
Kostenlos · 30 Minuten · Unverbindlich
