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]
|
## [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
|
## [1.0.10] - 2026-01-04
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert' show base64Url, jsonDecode, jsonEncode, utf8;
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
@@ -265,23 +265,27 @@ class AuthNotifier extends _$AuthNotifier {
|
|||||||
final oidcService = OidcServiceWeb();
|
final oidcService = OidcServiceWeb();
|
||||||
final tokens = await oidcService.exchangeCode(code, callbackState);
|
final tokens = await oidcService.exchangeCode(code, callbackState);
|
||||||
|
|
||||||
// Step 2: Sync with core-api to get user profile and roles
|
// Step 2: Decode JWT to extract user info (skip core-api sync)
|
||||||
developer.log('Syncing with core-api', name: 'auth');
|
final claims = _decodeJwtClaims(tokens.accessToken);
|
||||||
final authDatasource = ref.read(authDatasourceProvider);
|
final userName = claims['name'] as String? ??
|
||||||
final syncResponse = await authDatasource.syncUser(tokens.accessToken);
|
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(
|
await _storeAuth(
|
||||||
accessToken: tokens.accessToken,
|
accessToken: tokens.accessToken,
|
||||||
refreshToken: tokens.refreshToken,
|
refreshToken: tokens.refreshToken,
|
||||||
expiresAt: tokens.expiresAt,
|
expiresAt: tokens.expiresAt,
|
||||||
userId: syncResponse.userId,
|
authentikId: authentikId,
|
||||||
authentikId: syncResponse.authentikId,
|
userName: userName,
|
||||||
userName: syncResponse.name,
|
userEmail: userEmail,
|
||||||
userEmail: syncResponse.email,
|
// Roles from groups - for now just store group names
|
||||||
avatarUrl: syncResponse.avatarUrl,
|
// Full role parsing can be done later if needed
|
||||||
roles: syncResponse.roles,
|
|
||||||
preferences: syncResponse.preferences,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
state = AsyncData(AuthState(
|
state = AsyncData(AuthState(
|
||||||
@@ -289,19 +293,12 @@ class AuthNotifier extends _$AuthNotifier {
|
|||||||
accessToken: tokens.accessToken,
|
accessToken: tokens.accessToken,
|
||||||
refreshToken: tokens.refreshToken,
|
refreshToken: tokens.refreshToken,
|
||||||
expiresAt: tokens.expiresAt,
|
expiresAt: tokens.expiresAt,
|
||||||
userId: syncResponse.userId,
|
authentikId: authentikId,
|
||||||
authentikId: syncResponse.authentikId,
|
userName: userName,
|
||||||
userName: syncResponse.name,
|
userEmail: userEmail,
|
||||||
userEmail: syncResponse.email,
|
|
||||||
avatarUrl: syncResponse.avatarUrl,
|
|
||||||
roles: syncResponse.roles,
|
|
||||||
preferences: syncResponse.preferences,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
developer.log(
|
developer.log('Authenticated as $userName', name: 'auth');
|
||||||
'Authenticated as ${syncResponse.name} with ${syncResponse.roles.length} roles',
|
|
||||||
name: 'auth',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up the URL by removing the query parameters
|
// Clean up the URL by removing the query parameters
|
||||||
web_utils.replaceUrl('/');
|
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.
|
/// Sign out and clear stored credentials.
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
await _clearStoredAuth();
|
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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.10.4
|
sdk: ^3.10.4
|
||||||
|
|||||||
Reference in New Issue
Block a user