Sign in with Apple — OAuth 2.1 + OIDC mit form_post-Callback
Feinkonzept-Ergänzung · §3.9a Sign in with Apple
Abschnitt betitelt „Feinkonzept-Ergänzung · §3.9a Sign in with Apple“Einordnung. Ergänzung zu
kern/09-auth-self-service. Der Haupttext listet Google Workspace + Microsoft Entra ID als OAuth-Provider auf (§4.3 / §5.2); dieses Addendum erweitert den Provider-Katalog um Sign in with Apple (SIWA) als dritten, gleichwertigen Identity Provider. Motivation: iOS-Monteure, die ihre Apple-ID ohnehin täglich nutzen, bekommen eine systemnative Login-UX (Face ID / Touch ID → direkter Session-Start), ohne dass der Betrieb eine Office-365- oder Workspace-Lizenz braucht. SIWA erfüllt damit das gleiche Ziel wie Entra ID („kein zweites Passwort“), adressiert aber den handwerklichen Kleinbetrieb ohne zentrales IdP.
1. Header & Metadaten
Abschnitt betitelt „1. Header & Metadaten“feature_id: kern/09a-sign-in-with-appleparent: kern/09-auth-self-servicetitle: Sign in with Apple — OAuth 2.1 + OIDC mit form_post-Callbackfunktionsumfang_ref: §3.9 (Addendum)roadmap_horizont: MVPplattformen: mobile: vollständig # iOS native ASAuthorizationController, Android via Web-Fallback web: vollständig # Sign-in-with-Apple-Button (JS SDK nicht nötig — direkter OAuth-Flow) desktop: ab V1.5 bei Bedarfowner_rolle: Admin # schaltet SIWA pro Tenant (gleiche Policy wie Google/Microsoft)modul_gate_flag: kern.identitycompliance_flags: dsgvo: true # Apple ist Third-Country Transfer (USA) — DPA + Standardvertragsklauseln bfsg: true # SIWA-Button muss WCAG-konform, Kontrast ≥ 4.5 : 1, 48 × 48 Touch betrvg: true # login_events.oauth.* landet in derselben Chain wie Entra weitere: - "Apple Sign In REST API (Apple Developer Docs, 2025-Q4)" - "RFC 7521 Assertion Framework (client_assertion statt client_secret)" - "RFC 7523 JWT Bearer Profile" - "OpenID Connect Form Post Response Mode 1.0"estimate_eng_tage: 3 # Apple-Helpers + oauth.ts-Erweiterung + Tests + Terraform-Shellabhängigkeiten: - kern/09-auth-self-service # Haupt-Flow, Session-Management, login_events - infra/secrets-manager # Apple-Team-ID + .p8-Private-Key + Services-ID in AWS Secrets Manager2. Kontext & Problem
Abschnitt betitelt „2. Kontext & Problem“Marktrealität. Der 45-jährige SHK-Meister hat iPhone + MacBook + iCloud — und keine Office-365-Lizenz. Seine Entra-ID heißt „gibt’s nicht“; seine Google-ID heißt „hab ich, aber nur zum Einkaufen“. Apple-ID dagegen hängt am Gerät und ist für ihn der bequemste Login der Welt: ein Tap, Face ID, drin. Ohne SIWA lassen wir diesen User-Typ im Passwort-Fallback — während Apple ihm auf jeder anderen iOS-App den Ein-Klick-Login anbietet. Das ist die schmerzhafteste UX-Lücke im MVP-Auth-Kern.
Technischer Kontext. Sign in with Apple unterscheidet sich von Google/Microsoft in drei relevanten Punkten, die das Abstraktions-Design von kern/09 §5 nicht out-of-the-box abbildet:
- Kein
client_secret. Apple verlangt pro Token-Request eine JWT-Client-Assertion, signiert mit einem ES256-Private-Key (.p8-Format, von Apple Developer Portal bezogen). Der Key lebt in AWS Secrets Manager; die Assertion wird pro Request für 10 min signiert und enthältiss=teamId,sub=servicesId,aud=https://appleid.apple.com,exp=now+600. response_mode=form_poststatt Query-Redirect. Apple POSTet den Callback an unseren Endpunkt alsapplication/x-www-form-urlencoded(mit CSRF-Nonce viastate), nicht alsGET ?code=.... Grund: Apple will nicht, dass sensible Payloads (ID-Token mitemail) in Browser-History oder Referrer-Logs landen.- Email nur beim ersten Callback. Apple liefert
email+namegenau einmal — beim allerersten Login. Spätere Logins liefern nur die subject-ID (der opakesub). Wir müssen beim ersten Callbackemailinoauth_identities.emailpersistieren — sonst kann der User nie wieder per Apple-Login zugeordnet werden, wenn wir die Session-DB leeren.
Zusätzlich: Apple erlaubt dem User „Meine E-Mail verbergen“ → dann bekommen wir eine generierte Private-Relay-Adresse ([email protected]). Für MVP akzeptieren wir das — Mail an diese Adresse zu versenden funktioniert transparent, Apple leitet weiter. Später kann der Admin die Relay-Adresse in den Audit-Logs als solche markieren.
Schmerzpunkt der Alt-App. Die Alt-App hatte keinen SIWA und verlor laut Vertriebs-Feedback drei von zehn iOS-Handwerker-Demos am „Noch ein Passwort anlegen“-Schritt.
Erwarteter Outcome (SMART).
- Spezifisch:
POST /v1/auth/login/oauth/startakzeptiertprovider: 'apple'und liefert eine gültige Authorization-URL;POST /v1/auth/login/oauth/callbackakzeptiert sowohl Query (Google/Microsoft) als auchform_post(Apple) und legt eine Session an, wenn die Subject-ID einem existierenden User zugeordnet werden kann. - Messbar: 100 % der iOS-Testgeräte schließen den SIWA-Flow in ≤ 3 s vom Button-Tap bis zum authentisierten
/v1/auth/me; 0 % akzeptieren eine ID-Token-Signatur, die nicht gegen Apples JWKS verifizierbar ist (getestet durch einen Negativ-Fall mit manipuliertem Token). - Achievable: Apples OIDC-Endpunkt ist seit 2019 stabil; JWKS liegt unter
https://appleid.apple.com/auth/keysund rotiert selten (alle 6–12 Monate); Caching mit 12-h-TTL reicht. - Relevant: Ohne SIWA entgeht uns die wertvollste iOS-Zielgruppe (handwerkliche Einzelmeister mit iPhone und ohne IT-Abteilung).
- Time-bound: 3 Eng-Tage — Sprint-Slot zwischen MFA-Completion (gerade gelandet) und dem Passkey/WebAuthn-Block.
3. Zielgruppen & User-Research-Snapshot
Abschnitt betitelt „3. Zielgruppen & User-Research-Snapshot“Apple-Sign-In zielt auf die User-Typen aus §3.2 des Haupt-Feinkonzepts, die bisher „Passwort-only“ wählen:
| Rolle | Typisches Setup | Warum SIWA statt Google/Microsoft |
|---|---|---|
| SHK-Meister | iPhone + iCloud, kein Büro-365 | Apple-ID ist einziger Account, den er täglich nutzt |
| Maler-Bauleiter | iPad auf der Baustelle | iPadOS kennt SIWA system-nativ — ein Tap |
| Azubi-iOS | Privates iPhone | Hat keine Firmen-Google-Domain |
| Freier SubU | Privat-iPhone, wechselnde Kunden | Will nicht in jeden Kundentenant eine eigene Identität anlegen |
4. Funktionale Anforderungen (Delta zum Haupt-Feinkonzept)
Abschnitt betitelt „4. Funktionale Anforderungen (Delta zum Haupt-Feinkonzept)“Alle Anforderungen aus §4 gelten unverändert. Zusätzlich:
provider = 'apple'wird als dritter Wert in den Provider-Enum aufgenommen (DB-Check, Valibot, OpenAPI-Spec).response_mode=form_post— Apples/start-Response enthält zwingendresponse_mode=form_postundresponse_type=code id_token. Ohne beides wirft Appleinvalid_request.- Separate Callback-Variante: zusätzlich zum existierenden JSON-POST / Query-GET akzeptiert
/callbackauchapplication/x-www-form-urlencoded-POSTs von Apple. Die Feldercode,state,id_token, optionaluser(JSON-Escape, nur bei Erstlogin) werden dem bestehenden Handler wie ein JSON-Body zugeführt. - Client-Assertion signieren: pro Token-Request wird ein ES256-JWT erzeugt (TTL 600 s), als
client_assertionmitgeschickt (client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer). Der Private-Key wird aus AWS Secrets Manager gelesen und im Prozess-RAM gecached (Lambda-Cold-Start-Budget: ≤ 100 ms für Key-Fetch beim ersten Request). - ID-Token-Verifikation gegen
https://appleid.apple.com/auth/keys(JWKS):- Issuer muss
https://appleid.apple.comsein. - Audience muss der Werkszeit-Services-ID entsprechen (z. B.
de.werkszeit.auth). exp> now,iat≤ now + 60 s (Clock-Drift-Toleranz).nonceim Token muss mit dem in Redis/State-Store hinterlegten Nonce aus/startübereinstimmen.
- Issuer muss
- Private-Relay-Email-Handling: Wenn
email.endsWith('@privaterelay.appleid.com'), wird die Adresse inoauth_identities.emailgespeichert und inoauth_identities.domainalsprivaterelay.appleid.comgeführt — Admin-Reports können diese User später als „Relay-Identitäten“ filtern. Kein Warn-Popup im Flow (UX-Entscheidung: dem User ist nicht zu erklären, was Relay ist). - JIT-Provisioning-Policy: Apple ist nicht domain-gebunden — anders als
hd=musterbetrieb.debei Google Workspace. In MVP ist die SIWA-JIT-Policy: kein JIT. Der User muss bereits als Werkszeit-User in einem Tenant existieren (Match viaemail), sonst403 AUTH-SIWA-UNKNOWN-USER. Grund: ohne Domain-Mapping wissen wir nicht, in welchem Tenant wir den User anlegen sollten; ein einzelner SHK-Meister, der „auch mal“ mit Apple einloggt, kann vom Admin-Portal (kern/09 §9) manuell peroauth_identities-Insert verknüpft werden. Volle JIT (Tenant-Selection-Flow nach erstem Apple-Login) ist Phase-2. - Rate-Limit: Apple-Callback nutzt denselben Rate-Limiter wie Google/Microsoft (
oauthCallbackIpLimiter— 30/min pro IP). Die zusätzliche signing-Operation ist günstig (< 2 ms), kein separater Limiter.
Nicht-Ziele (Phase 1)
Abschnitt betitelt „Nicht-Ziele (Phase 1)“- Revocation via
/auth/revoke— Apple bietet einen REST-Endpunkt, um Sessions serverseitig zu invalidieren. Phase 2. - Refresh-Token-Rotation — Apple-Refresh-Tokens sind langlebig (180 Tage); wir speichern sie nicht und fordern bei Session-Ablauf einen frischen Login. Phase 2 ergänzt Refresh wenn Demand da.
- Private-Relay-Adressbuch — keine UI-Hinweise für Admin, welche User Relay nutzen. Logs reichen für MVP.
5. API & Daten-Kontrakt (Delta)
Abschnitt betitelt „5. API & Daten-Kontrakt (Delta)“5.1 Provider-Enum
Abschnitt betitelt „5.1 Provider-Enum“export const oauthProviderValues = ['google', 'microsoft', 'apple'] as const;5.2 Migration 0041
Abschnitt betitelt „5.2 Migration 0041“-- Add 'apple' to the provider check constraint + a nullable email column-- (Apple reveals email once, on first callback).ALTER TABLE oauth_identities DROP CONSTRAINT IF EXISTS oauth_identities_provider_check;
ALTER TABLE oauth_identities ADD CONSTRAINT oauth_identities_provider_check CHECK (provider IN ('google','microsoft','apple'));
ALTER TABLE oauth_identities ADD COLUMN IF NOT EXISTS email text;
-- Index for the Apple JIT-match by email.CREATE INDEX IF NOT EXISTS oauth_identities_email_idx ON oauth_identities(email) WHERE email IS NOT NULL;5.3 Apple-spezifische Provider-Config
Abschnitt betitelt „5.3 Apple-spezifische Provider-Config“// apps/api/src/auth/oauth-pkce.ts — erweitert PROVIDERSapple: { authEndpoint: 'https://appleid.apple.com/auth/authorize', tokenEndpoint: 'https://appleid.apple.com/auth/token', userInfoEndpoint: '', // Apple liefert keine userinfo — alles aus ID-Token scope: 'name email', extraAuthParams: { response_mode: 'form_post', response_type: 'code id_token', },},5.4 Environment-Variablen
Abschnitt betitelt „5.4 Environment-Variablen“APPLE_SERVICES_ID # z.B. de.werkszeit.auth (Services ID im Apple Developer Portal)APPLE_TEAM_ID # 10-char Team IDAPPLE_KEY_ID # 10-char Key ID der .p8-DateiAPPLE_PRIVATE_KEY # PEM-formatierter ES256-Private-Key (mehrzeilig)APPLE_REDIRECT_URI # https://api.werkszeit.de/v1/auth/login/oauth/callbackAlle fünf landen in AWS Secrets Manager (apple/siwa-oauth als JSON-Blob) und werden via App-Runner-Env-Mapping in den Container injiziert — gleiches Pattern wie die bereits hinterlegten apple/key-*-Shells (siehe infra/terraform/envs/prod/main.tf).
5.5 Callback-Form
Abschnitt betitelt „5.5 Callback-Form“Zusätzlich zum bestehenden POST /callback mit JSON-Body akzeptiert der Handler:
POST /v1/auth/login/oauth/callbackContent-Type: application/x-www-form-urlencoded
code=ABC123&state=...&id_token=eyJ...&user={"name":{"firstName":"Ron","lastName":"..."}}Der Handler erkennt den Content-Type und parst application/x-www-form-urlencoded → delegiert an die gemeinsame Exchange-Logic.
6. Sicherheit & Bedrohungsmodell (Delta)
Abschnitt betitelt „6. Sicherheit & Bedrohungsmodell (Delta)“| Bedrohung | Vektor | Mitigation |
|---|---|---|
Gestohlenes .p8-Private-Key-Material |
S3-Dump, Leaked-Env, Repo-Push | Key nur in Secrets Manager, nie im Repo; Prozess-RAM-Cache wird beim App-Runner-Rolling-Update invalidiert; Rotation-Scripts in scripts/rotate-apple-key.sh (Phase 2) |
Replay eines abgefangenen id_token |
MitM, Referrer-Leak | nonce-Claim im id_token muss mit State-Store-Nonce aus /start übereinstimmen; state-Store löscht den Eintrag nach Match (one-shot) |
iss/aud-Forgery |
Gefälschtes id_token von fremder Apple-App |
JWKS-Verify gegen https://appleid.apple.com/auth/keys; Audience-Check gegen APPLE_SERVICES_ID |
| Private-Relay-Adresse als JIT-Vektor | Angreifer mit beliebiger Apple-ID | JIT ist deaktiviert — User muss vorher existieren; Admin-Portal ist einziger Weg, SIWA zu einem User zu verknüpfen |
| Clock-Drift > 60 s | Falsch eingestellte Server-Uhr | iat-Toleranz ≤ 60 s; Server-Uhr via chrony auf ntp.ubuntu.com (App Runner nutzt Amazon Time Sync) |
7. Tests & Qualitätskriterien (Delta)
Abschnitt betitelt „7. Tests & Qualitätskriterien (Delta)“Unit.
apple-client-assertion.ts: ES256-Signature ist mit einem Test-Keypair gegen den Public-Key verifizierbar; Header enthältkid; Claims enthalteniss,sub,aud,exp.apple-jwks.ts: JWKS-Cache hält 12 h, invalidiert beikid-Miss; abgelehnt werden Tokens ohnekid, mit falschemaud, mitexp < now.
Route-Tests (tests/auth/oauth-apple.test.ts):
/start?provider=appleliefert Auth-URL mitresponse_mode=form_postundresponse_type=code id_token./callbackmit gültigem form-post (mocked ID-Token + gemocktem Token-Exchange) → 200 mit Session-Cookie +login.succeededevent./callbackmit manipuliertem ID-Token (falsche Signatur) → 401AUTH-SIWA-INVALID-TOKEN./callbackmit Apple-Subject, der noch keinem User zugeordnet ist → 403AUTH-SIWA-UNKNOWN-USER./callbackmit Private-Relay-Email → 200 +oauth_identities.domain='privaterelay.appleid.com'.
Integration (tests/integration/oauth-apple.test.ts):
- Echter Testcontainers-Postgres; mocked HTTP für Apple-Token-Endpunkt und JWKS. Erster Callback schreibt
oauth_identitiesmitemail; zweiter Callback desselben Subject findet den Eintrag und legt Session an, ohneemailzu re-schreiben.
Compliance.
- Nach erfolgreichem SIWA-Login muss ein
login.succeededmitmethod='oauth'im BetrVG-Chain-Log stehen. Shape identisch zu Google/Microsoft — Provider wird nicht in der Event-Payload genannt (Payload-Schlankheit), der Provider-Nachweis lebt imoauth_identities-Insert.
8. Rollout
Abschnitt betitelt „8. Rollout“Phase A (T+0 bis T+2). Code (Migration, Helpers, Handler, Tests) hinter dem bestehenden Env-Flag-Pattern. Wenn APPLE_SERVICES_ID leer → /start?provider=apple antwortet 503 AUTH-OAUTH-UNCONFIGURED (gleicher Code-Pfad wie Google/Microsoft).
Phase B (T+3). Apple-Developer-Konfiguration: Services ID anlegen, Return-URL https://api.werkszeit.de/v1/auth/login/oauth/callback, SIWA-Capability aktivieren. Eine der bestehenden apple/key-*-Secrets wird als SIWA-Key designiert (metadata.json updated). Dann terraform apply + App-Runner-Redeploy.
Phase C (T+5). Marketing-Seite (apps/marketing /login) ergänzt den „Mit Apple anmelden“-Button neben „Mit Google“ und „Mit Microsoft“.
Feature-Flags.
auth.oauth.apple.allow(per Tenant, default false für neue Tenants — Opt-in durch Admin). Bestehendesauth.oauth.*.allow-Schema aus kern/09 §14.
9. Offene Fragen / Risiken
Abschnitt betitelt „9. Offene Fragen / Risiken“- Apple-Services-ID-Naming —
de.werkszeit.authvs.com.werkszeit.auth? Annahme:de.werkszeit.auth, weil Werkszeit eine .de-Marke ist. Falls Apple das ablehnt (Services IDs müssen nicht zwingend Reverse-DNS sein, werden aber so empfohlen), Fallback aufcom.werkszeit.auth. - Key-Rotation — Apple-Keys rotieren wir manuell jährlich (Phase 2 automatisiert via
scripts/rotate-apple-key.sh). Solange nur eine aktive Services-ID existiert, ist Key-Rotation einfaches Secrets-Manager-Versionieren + App-Runner-Redeploy. - Android ohne Apple-ID — Android-User sehen den Button, können ihn aber nicht nativ nutzen (Apple zeigt Web-Flow). UX-Entscheidung: Button-Text „Mit Apple anmelden“ ohne Android-Hinweis — Apple handhabt den Web-Fallback.
- Private-Relay + DSGVO-Auskunft — Art. 15 Auskunftsanspruch funktioniert über die relay-Adresse (Apple leitet weiter). Keine Aktion nötig, aber im DSGVO-Export muss klar sein, dass die Adresse eine Relay ist.
10. Definition of Done
Abschnitt betitelt „10. Definition of Done“- Migration 0041 angewendet auf Dev + CI;
oauth_identities.providerakzeptiert'apple'. -
apple-client-assertion.ts+apple-jwks.tsexistieren mit Test-Coverage ≥ 90 %. -
/v1/auth/login/oauth/start?provider=appleliefert gültige Auth-URL mitresponse_mode=form_post. -
/v1/auth/login/oauth/callbackakzeptiert Apples form-post-Payload und legt Session an (mitoauth_identities-Insert undlogin.succeeded-Event). - Negative Tests (manipuliertes ID-Token, unbekannter Subject, abgelaufener State) liefern die definierten Fehlercodes.
- Terraform-Shell
apple/siwa-oauthexistiert +upload-apple-keys.shergänzt um SIWA-Payload. - Login-Page rendert
Mit Apple anmelden-Button (BFSG-konform: 48 × 48 px, Kontrast ≥ 4.5 : 1).
Stand 2026-04-24