v1.1.1 — Session-Token statt JWT für Account-Calls
Wire-Konvention für authenticated Account-Endpoints (changeEmail, changePassword, deleteAccount) geklärt. Server-seitig wurde in mana-auth Better Auths bearer-Plugin aktiviert (requireSignature: false), das Session-Tokens zu Session-Cookies konvertiert. Native- Apps senden daher jetzt den Session-Token (refreshToken-Feldwert) statt des JWT als Authorization: Bearer für diese drei Endpoints. Der JWT bleibt für app-eigene Backends (memoro-api, cardecky-api, manaspur-api) der richtige Authorization-Header — die Trennung ist nur für mana-auth interne Endpoints. currentSessionToken() als public Helper hinzu (symmetrisch zu currentAccessToken). 38/38 Tests grün. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
716509e10e
commit
3459c78731
4 changed files with 68 additions and 16 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -4,6 +4,36 @@ Alle Änderungen werden hier dokumentiert. Format orientiert an
|
|||
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
|
||||
[Semver](https://semver.org).
|
||||
|
||||
## [1.1.1] — 2026-05-13
|
||||
|
||||
Patch — Wire-Konvention für authenticated Account-Calls geklärt.
|
||||
|
||||
### Geändert
|
||||
|
||||
- `AuthClient.changeEmail`, `changePassword`, `deleteAccount` senden
|
||||
jetzt den Session-Token (`refreshToken`-Feldwert) statt des JWT als
|
||||
`Authorization: Bearer`. Hintergrund: Server-seitig wurde in
|
||||
`mana-auth` Better Auths `bearer`-Plugin aktiviert
|
||||
(`requireSignature: false`), das Session-Tokens zu Session-Cookies
|
||||
konvertiert. Damit funktionieren `auth.api.changeEmail` etc. für
|
||||
Native-Apps ohne Cookie-Container.
|
||||
- `AuthClient.currentSessionToken()` als public Helper hinzu. Symmetrisch
|
||||
zu `currentAccessToken()`.
|
||||
|
||||
### Trade-Off bewusst akzeptiert
|
||||
|
||||
Session-Token wird bei jedem Account-Call versendet (vorher nur beim
|
||||
`/refresh`). Mit TLS-Baseline akzeptables Risiko; Compromise-Surface
|
||||
nicht relevant größer als JWT-Leak. Alternative wäre ein Custom-
|
||||
Bearer-JWT-to-Cookie-Resolver im Server (40+ Zeilen Hono-Middleware,
|
||||
HMAC-Cookie-Synthese) — bewusst nicht gewählt, weil der bearer-Plugin
|
||||
genau für diesen Use-Case existiert.
|
||||
|
||||
### Tests
|
||||
|
||||
- Test `changePassword schickt Bearer-Header` umbenannt auf
|
||||
`schickt Session-Token als Bearer (nicht JWT)` und geupdated.
|
||||
|
||||
## [1.1.0] — 2026-05-13
|
||||
|
||||
Phase 1 aus dem Native-Auth-Vollausbau-Plan (Option A — alles nativ,
|
||||
|
|
|
|||
|
|
@ -17,16 +17,19 @@ import Foundation
|
|||
/// - `POST /change-password` — Passwort ändern (current + new)
|
||||
/// - `DELETE /account` — Account löschen (App-Store-Pflicht 5.1.1(v))
|
||||
///
|
||||
/// **Server-Limitation (Stand 2026-05-13):** `change-email`,
|
||||
/// `change-password` und `DELETE /account` forwarden Original-Request-
|
||||
/// Headers an Better Auth. Better Auth liest Session-Cookies. Der
|
||||
/// `bearer`-Plugin von Better Auth ist NICHT installiert, daher
|
||||
/// scheitern diese Endpoints heute mit reinem `Authorization: Bearer`.
|
||||
/// → Server-Fix in Phase 3 nötig (entweder `bearerPlugin()` in
|
||||
/// `better-auth.config.ts` aktivieren oder Custom-Bearer-Resolver in
|
||||
/// `mana-auth/src/routes/auth.ts` ergänzen). ManaCore sendet Bearer
|
||||
/// bereits korrekt — sobald der Server das akzeptiert, funktionieren
|
||||
/// die Methoden ohne Swift-Änderung.
|
||||
/// **Wire-Konvention für authenticated Account-Calls:**
|
||||
/// `change-email`, `change-password` und `DELETE /account` forwarden
|
||||
/// die Original-Request-Headers an Better Auth (`auth.api.changeEmail`
|
||||
/// etc.). Better Auth's `bearer`-Plugin (seit 2026-05-13 in mana-auth
|
||||
/// aktiv) konvertiert `Authorization: Bearer <session-token>` in
|
||||
/// einen synthetischen Session-Cookie. **Native-Apps senden hier den
|
||||
/// Session-Token (`refreshToken`-Feldwert aus /login bzw. /refresh),
|
||||
/// NICHT den JWT.** Der JWT bleibt für app-eigene Backends
|
||||
/// (memoro-api, cardecky-api etc.) der richtige Header.
|
||||
///
|
||||
/// Trade-Off: Session-Token wird häufiger versendet als der reine
|
||||
/// Refresh-Pfad. Bei TLS-only-Baseline akzeptabel; Compromise-Surface
|
||||
/// nicht relevant größer als JWT-Leak.
|
||||
public extension AuthClient {
|
||||
// MARK: - Registrierung
|
||||
|
||||
|
|
@ -296,7 +299,9 @@ public extension AuthClient {
|
|||
|
||||
extension AuthClient {
|
||||
/// Generischer JSON-POST/DELETE-Helper. Wenn `authenticated == true`,
|
||||
/// wird der aktuelle Bearer-Token mitgeschickt.
|
||||
/// wird der Session-Token (`refreshToken`-Feldwert, von Better Auth
|
||||
/// als Session-ID interpretierbar via `bearer`-Plugin) als Bearer-
|
||||
/// Header mitgeschickt — NICHT der JWT. Siehe Doc-Header dieser Datei.
|
||||
fileprivate func postJSON<Body: Encodable>(
|
||||
path: String,
|
||||
method: String = "POST",
|
||||
|
|
@ -308,7 +313,7 @@ extension AuthClient {
|
|||
request.httpMethod = method
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
if authenticated {
|
||||
let token = try currentAccessToken()
|
||||
let token = try currentSessionToken()
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
do {
|
||||
|
|
|
|||
|
|
@ -133,6 +133,19 @@ public final class AuthClient {
|
|||
return token
|
||||
}
|
||||
|
||||
/// Liefert den Session-Token (das wire-protocol `refreshToken`-Feld).
|
||||
/// Wird von Better-Auth-`auth.api.*`-Endpoints akzeptiert wenn der
|
||||
/// `bearer`-Plugin server-seitig aktiv ist — z.B. für `changeEmail`,
|
||||
/// `changePassword`, `deleteAccount`. Der App-eigene JWT (aus
|
||||
/// `currentAccessToken`) gilt für app-spezifische Backends, der
|
||||
/// Session-Token nur für mana-auths Account-Aktionen.
|
||||
public func currentSessionToken() throws -> String {
|
||||
guard let token = keychain.getString(for: .refreshToken) else {
|
||||
throw AuthError.notSignedIn
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
/// Liefert einen Access-Token, der nicht innerhalb der nächsten
|
||||
/// `refreshLeeway` Sekunden abläuft. Refreshed proaktiv, wenn nötig.
|
||||
public func freshAccessToken(refreshLeeway: TimeInterval = 300) async throws -> String {
|
||||
|
|
|
|||
|
|
@ -201,12 +201,16 @@ struct AuthClientAccountTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test("changePassword schickt Bearer-Header wenn eingeloggt")
|
||||
@Test("changePassword schickt Session-Token als Bearer (nicht JWT)")
|
||||
func changePasswordSendsBearer() async throws {
|
||||
let (client, _) = Self.makeClient()
|
||||
// Mock-Token im Keychain ablegen via persistSession-Helper.
|
||||
// Authenticated Account-Calls senden den Session-Token (refreshToken)
|
||||
// statt des JWT, weil server-seitig Better Auths bearer-Plugin
|
||||
// den Session-Token zu einem Session-Cookie konvertiert. Siehe
|
||||
// AuthClient+Account.swift Doc-Header.
|
||||
let access = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsImV4cCI6MjAwMDAwMDAwMH0.sig"
|
||||
try client.persistSession(email: "u@x.de", accessToken: access, refreshToken: "r")
|
||||
let session = "session-token-value"
|
||||
try client.persistSession(email: "u@x.de", accessToken: access, refreshToken: session)
|
||||
|
||||
let captured = MockURLProtocol.Capture()
|
||||
MockURLProtocol.handler = { request in
|
||||
|
|
@ -216,7 +220,7 @@ struct AuthClientAccountTests {
|
|||
|
||||
try await client.changePassword(currentPassword: "alt", newPassword: "neu")
|
||||
let request = try #require(captured.request)
|
||||
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer \(access)")
|
||||
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer \(session)")
|
||||
#expect(request.url?.path == "/api/v1/auth/change-password")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue