Replace IDP token passthrough with backend-issued JWT for iOS auth
iOS clients now exchange the OIDC authorization code for a backend-signed HS256 JWT via POST /auth/token. All subsequent API requests authenticate using this JWT as a Bearer token, verified locally — no per-request IDP call is needed. Web frontend session-cookie auth is unchanged.
This commit is contained in:
@@ -11,6 +11,7 @@ final class AuthManager: ObservableObject {
|
||||
private let keychain: Keychain
|
||||
private let apiClient = APIClient()
|
||||
|
||||
/// The backend-issued JWT. Sent as `Authorization: Bearer <token>` on every API call.
|
||||
var accessToken: String? {
|
||||
get { try? keychain.get(AppConstants.KeychainKeys.accessToken) }
|
||||
set {
|
||||
@@ -22,17 +23,6 @@ final class AuthManager: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
var idToken: String? {
|
||||
get { try? keychain.get(AppConstants.KeychainKeys.idToken) }
|
||||
set {
|
||||
if let value = newValue {
|
||||
try? keychain.set(value, key: AppConstants.KeychainKeys.idToken)
|
||||
} else {
|
||||
try? keychain.remove(AppConstants.KeychainKeys.idToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
self.keychain = Keychain(service: "com.timetracker.app")
|
||||
.accessibility(.whenUnlockedThisDeviceOnly)
|
||||
@@ -66,7 +56,9 @@ final class AuthManager: ObservableObject {
|
||||
}
|
||||
|
||||
func logout() async throws {
|
||||
try await apiClient.requestVoid(
|
||||
// Best-effort server-side logout; the backend JWT is stateless so the
|
||||
// real security comes from clearing the local token.
|
||||
try? await apiClient.requestVoid(
|
||||
endpoint: APIEndpoint.logout,
|
||||
method: .post,
|
||||
authenticated: true
|
||||
@@ -76,14 +68,12 @@ final class AuthManager: ObservableObject {
|
||||
|
||||
func clearAuth() {
|
||||
accessToken = nil
|
||||
idToken = nil
|
||||
currentUser = nil
|
||||
isAuthenticated = false
|
||||
}
|
||||
|
||||
func handleTokenResponse(_ response: TokenResponse) async {
|
||||
accessToken = response.accessToken
|
||||
idToken = response.idToken
|
||||
currentUser = response.user
|
||||
isAuthenticated = true
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ final class AuthService: NSObject {
|
||||
|
||||
let callbackScheme = URL(string: AppConfig.authCallbackURL)?.scheme ?? "timetracker"
|
||||
|
||||
// Use a shared (non-ephemeral) session so the backend session cookie set during
|
||||
// /auth/login is automatically included in the /auth/token POST.
|
||||
// Use an ephemeral session — we only need the redirect URL back with the
|
||||
// authorization code; no cookies or shared state are needed.
|
||||
let webAuthSession = ASWebAuthenticationSession(
|
||||
url: authURL,
|
||||
callbackURLScheme: callbackScheme
|
||||
@@ -69,9 +69,8 @@ final class AuthService: NSObject {
|
||||
}
|
||||
|
||||
webAuthSession.presentationContextProvider = self
|
||||
// prefersEphemeralWebBrowserSession = false ensures the session cookie from
|
||||
// /auth/login is retained and sent with the subsequent /auth/token request.
|
||||
webAuthSession.prefersEphemeralWebBrowserSession = false
|
||||
// Ephemeral session: no shared cookies or browsing data with Safari.
|
||||
webAuthSession.prefersEphemeralWebBrowserSession = true
|
||||
|
||||
self.authSession = webAuthSession
|
||||
|
||||
@@ -138,8 +137,8 @@ final class AuthService: NSObject {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
// state is sent so the backend can look up and validate the original session.
|
||||
// code_verifier is not sent — the backend uses its own verifier from the session.
|
||||
// state is sent so the backend can look up the original PKCE session.
|
||||
// code_verifier is NOT sent — the backend holds it in the in-memory session.
|
||||
let body: [String: Any] = [
|
||||
"code": code,
|
||||
"state": state,
|
||||
@@ -198,14 +197,12 @@ extension Notification.Name {
|
||||
|
||||
struct TokenResponse: Codable {
|
||||
let accessToken: String
|
||||
let idToken: String?
|
||||
let tokenType: String
|
||||
let expiresIn: Int?
|
||||
let user: User
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case idToken = "id_token"
|
||||
case tokenType = "token_type"
|
||||
case expiresIn = "expires_in"
|
||||
case user
|
||||
|
||||
@@ -13,7 +13,6 @@ enum AppConstants {
|
||||
|
||||
enum KeychainKeys {
|
||||
static let accessToken = "accessToken"
|
||||
static let idToken = "idToken"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user