feat(auth): SSO-Anmeldung ueber OpenID Connect (AppAuth)

Zusaetzlicher Anmeldeweg neben Benutzername/Passwort. Der Login-Screen holt
die konfigurierten Provider von GET /api/auth/oidc/providers; ohne Provider
oder gegen einen aelteren Server bleibt der Screen unveraendert.

Die App laeuft als Public Client: Authorization Code Flow mit PKCE in einer
ASWebAuthenticationSession (nie ein WKWebView), ohne Client Secret. Das
ID-Token wird an POST /api/auth/oidc/exchange gegen ein Calendarr-Token
getauscht; die Antwort hat dieselbe Form wie der Passwort-Login, sodass der
bestehende Session-Pfad unveraendert weiterlaeuft.

- OIDCAuthenticator kapselt Discovery, PKCE-Request, Code-Tausch und die
  stille Erneuerung. Der AuthState mit dem Refresh-Token liegt in der
  Keychain (kSecAttrAccessibleAfterFirstUnlock) und wird beim Logout aus
  beiden Zugriffsgruppen entfernt.
- OIDCSessionRefresher erneuert die Sitzung beim Start, wenn das
  Calendarr-Token binnen 24 h ablaeuft — ohne Browser, ueber den
  Refresh-Token des Providers.
- Die angefragten Scopes kommen vom Server (mobile_scopes); ein Scope, den
  der Provider nicht kennt, wuerde die ganze Anfrage scheitern lassen.
- Bei aktiviertem gefuehrten Zugriff weicht AppAuth auf SFSafariViewController
  aus, dessen Redirect nur ueber ein registriertes URL-Schema zurueckkaeme.
  Da das App-Target seine Info.plist generiert und CFBundleURLTypes dort
  nicht setzbar ist, wird der Fall vorab sauber abgelehnt statt endlos auf
  einen Callback zu warten, der nicht kommen kann.
- Abbrueche werden mit Fehlerdomaene geprueft (user- und programCanceled) und
  still behandelt; alle uebrigen Fehler tragen Domain, Code und OAuth-Fehler
  ins DEBUG-Log, statt spurlos zu verschwinden.

project.pbxproj: AppAuth-iOS 1.7.5 als SPM-Abhaengigkeit, nur am App-Target.
OTHER_LDFLAGS bekommt -ObjC, weil OIDAuthState+IOS.m ausschliesslich eine
Kategorie enthaelt und ohne Symbol sonst wegoptimiert werden koennte.

Achtung: mangels macOS/Xcode ist dieser Stand nicht kompiliert. Beim ersten
Oeffnen loest Xcode das Paket auf und legt Package.resolved an; die bitte
mit committen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-09 18:33:59 +02:00
co-authored by Claude Opus 5
parent d1da8de06a
commit 7b51cab693
8 changed files with 548 additions and 0 deletions
+28
View File
@@ -10,6 +10,7 @@
D0001A0CABCDEF0100AB5001 /* CalendarrWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D0001A02ABCDEF0100AB5001 /* CalendarrWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
CA1E4D0A0002000000000001 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000001 /* CalendarrCore */; };
CA1E4D0A0002000000000002 /* CalendarrCore in Frameworks */ = {isa = PBXBuildFile; productRef = CA1E4D0A0001000000000002 /* CalendarrCore */; };
AA9701000001000000000001 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA9701000002000000000001 /* AppAuth */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -98,6 +99,7 @@
buildActionMask = 2147483647;
files = (
CA1E4D0A0002000000000001 /* CalendarrCore in Frameworks */,
AA9701000001000000000001 /* AppAuth in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -180,6 +182,7 @@
name = "Calendarr iOS";
packageProductDependencies = (
CA1E4D0A0001000000000001 /* CalendarrCore */,
AA9701000002000000000001 /* AppAuth */,
);
productName = "Calendarr iOS";
productReference = C0000B01FC4E10100AB5001 /* Calendarr iOS.app */;
@@ -243,6 +246,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
39450EFC302A266A0090DDD7 /* XCLocalSwiftPackageReference "../CalendarrKit" */,
AA9701000003000000000001 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = C0000C01FB4E10100AB5001 /* Products */;
@@ -521,6 +525,10 @@
"$(inherited)",
"@executable_path/Frameworks",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
PRODUCT_NAME = "Calendarr iOS";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -566,6 +574,10 @@
"$(inherited)",
"@executable_path/Frameworks",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
);
PRODUCT_BUNDLE_IDENTIFIER = com.scarriffleservices.calendarr.ios;
PRODUCT_NAME = "Calendarr iOS";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -702,6 +714,11 @@
isa = XCSwiftPackageProductDependency;
productName = CalendarrCore;
};
AA9701000002000000000001 /* AppAuth */ = {
isa = XCSwiftPackageProductDependency;
package = AA9701000003000000000001 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */;
productName = AppAuth;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCLocalSwiftPackageReference section */
@@ -710,6 +727,17 @@
relativePath = ../CalendarrKit;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCRemoteSwiftPackageReference section */
AA9701000003000000000001 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/openid/AppAuth-iOS.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.7.5;
};
};
/* End XCRemoteSwiftPackageReference section */
};
rootObject = C0000301FB4E10100AB5001 /* Project object */;
}