diff --git a/CHANGELOG.md b/CHANGELOG.md index c84c64c..15b21e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.11] - 2026-01-04 + +### Changed +- Web auth now extracts user info directly from JWT instead of syncing with core-api + - Eliminates CORS preflight issues with /auth/sync endpoint + - Decodes JWT claims (name, email, groups) client-side + - Bearer token will be used for API authentication + ## [1.0.10] - 2026-01-04 ### Fixed diff --git a/lib/core/auth/auth_provider.dart b/lib/core/auth/auth_provider.dart index 9ebbb5b..bb4b0a5 100644 --- a/lib/core/auth/auth_provider.dart +++ b/lib/core/auth/auth_provider.dart @@ -1,4 +1,4 @@ -import 'dart:convert'; +import 'dart:convert' show base64Url, jsonDecode, jsonEncode, utf8; import 'dart:developer' as developer; import 'package:flutter/foundation.dart' show kIsWeb; @@ -265,23 +265,27 @@ class AuthNotifier extends _$AuthNotifier { final oidcService = OidcServiceWeb(); final tokens = await oidcService.exchangeCode(code, callbackState); - // Step 2: Sync with core-api to get user profile and roles - developer.log('Syncing with core-api', name: 'auth'); - final authDatasource = ref.read(authDatasourceProvider); - final syncResponse = await authDatasource.syncUser(tokens.accessToken); + // Step 2: Decode JWT to extract user info (skip core-api sync) + final claims = _decodeJwtClaims(tokens.accessToken); + final userName = claims['name'] as String? ?? + claims['preferred_username'] as String? ?? + 'User'; + final userEmail = claims['email'] as String? ?? ''; + final authentikId = claims['sub'] as String?; + final groups = (claims['groups'] as List?)?.cast() ?? []; - // Step 3: Store credentials and user data + developer.log('JWT claims: name=$userName, email=$userEmail, groups=$groups', name: 'auth'); + + // Step 3: Store credentials and user data from JWT await _storeAuth( accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, expiresAt: tokens.expiresAt, - userId: syncResponse.userId, - authentikId: syncResponse.authentikId, - userName: syncResponse.name, - userEmail: syncResponse.email, - avatarUrl: syncResponse.avatarUrl, - roles: syncResponse.roles, - preferences: syncResponse.preferences, + authentikId: authentikId, + userName: userName, + userEmail: userEmail, + // Roles from groups - for now just store group names + // Full role parsing can be done later if needed ); state = AsyncData(AuthState( @@ -289,19 +293,12 @@ class AuthNotifier extends _$AuthNotifier { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, expiresAt: tokens.expiresAt, - userId: syncResponse.userId, - authentikId: syncResponse.authentikId, - userName: syncResponse.name, - userEmail: syncResponse.email, - avatarUrl: syncResponse.avatarUrl, - roles: syncResponse.roles, - preferences: syncResponse.preferences, + authentikId: authentikId, + userName: userName, + userEmail: userEmail, )); - developer.log( - 'Authenticated as ${syncResponse.name} with ${syncResponse.roles.length} roles', - name: 'auth', - ); + developer.log('Authenticated as $userName', name: 'auth'); // Clean up the URL by removing the query parameters web_utils.replaceUrl('/'); @@ -314,6 +311,35 @@ class AuthNotifier extends _$AuthNotifier { } } + /// Decode JWT payload without verification (validation happens server-side). + Map _decodeJwtClaims(String jwt) { + try { + final parts = jwt.split('.'); + if (parts.length != 3) { + developer.log('Invalid JWT format', name: 'auth'); + return {}; + } + + // Decode the payload (second part) + String payload = parts[1]; + // Add padding if needed for base64 + switch (payload.length % 4) { + case 2: + payload += '=='; + break; + case 3: + payload += '='; + break; + } + + final decoded = utf8.decode(base64Url.decode(payload)); + return jsonDecode(decoded) as Map; + } catch (e) { + developer.log('Failed to decode JWT: $e', name: 'auth'); + return {}; + } + } + /// Sign out and clear stored credentials. Future signOut() async { await _clearStoredAuth(); diff --git a/pubspec.yaml b/pubspec.yaml index f62089d..db8754c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.10+1 +version: 1.0.11+1 environment: sdk: ^3.10.4