feat(auth): extract user info from JWT, skip core-api sync
- Decode JWT claims (name, email, groups) directly in Flutter - No longer calls /auth/sync endpoint (avoids CORS preflight issues) - Bearer token used for subsequent API authentication 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
346ca75d68
commit
b35f495537
@@ -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
|
||||
|
||||
@@ -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<dynamic>?)?.cast<String>() ?? [];
|
||||
|
||||
// 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<String, dynamic> _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<String, dynamic>;
|
||||
} catch (e) {
|
||||
developer.log('Failed to decode JWT: $e', name: 'auth');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign out and clear stored credentials.
|
||||
Future<void> signOut() async {
|
||||
await _clearStoredAuth();
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user