1
0
forked from baron/baron-sso

Merge pull request 'dev/auth-auto-login' (#45) from dev/auth-auto-login into main

Reviewed-on: ai-team/baron-sso#45
This commit is contained in:
2026-01-20 15:07:04 +09:00
2 changed files with 62 additions and 3 deletions

View File

@@ -77,6 +77,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
// Helper to decode JWT and get User ID (sub claim)
String _getUserIdFromJwt(String jwt) {
try {
final parts = jwt.split('.');
if (parts.length != 3) return 'unknown';
final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1])));
final data = json.decode(payload) as Map<String, dynamic>;
return data['sub'] as String? ?? 'unknown';
} catch (e) {
debugPrint("[JWT] Could not extract User ID (sub): $e");
return 'unknown';
}
}
void _handleTabSelection() {
if (_tabController.index == 1 && _qrPendingRef == null) {
_startQrFlow();
@@ -202,6 +216,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
'unknown', [], 0, displayName, null, '', false, '', false, {}, '', '', '', false, 'enabled', [], [], [],
);
final session = DescopeSession.fromJwt(jwt, jwt, dummyUser);
// Refresh Token을 LocalStorage에 저장
Descope.sessionManager.manageSession(session);
// Notify and Go to Dashboard
@@ -380,12 +395,55 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
}
void _logTokenDetails(String jwt) {
try {
// JWT는 세 부분(Header, Payload, Signature)이 '.'으로 구분된 문자열입니다. 이를 분리합니다.
final parts = jwt.split('.');
// 세 부분으로 정확히 나뉘지 않았다면 유효한 JWT가 아니므로 중단합니다.
if (parts.length != 3) return;
// JWT의 두 번째 부분(Payload)은 Base64Url로 인코딩된 JSON 데이터입니다.
// 1. Base64Url 문자열을 디코딩하여 바이트 배열로 변환합니다.
// normalize()는 Base64 패딩(=) 문제를 처리해줍니다.
final decodedPayload = base64Url.decode(base64Url.normalize(parts[1]));
// 2. 바이트 배열을 UTF-8 형식의 일반 문자열(JSON)로 변환합니다.
final payloadJson = utf8.decode(decodedPayload);
// 3. JSON 문자열을 Dart에서 사용할 수 있는 Map 객체로 변환합니다.
final data = json.decode(payloadJson) as Map<String, dynamic>;
// [FIX] 'exp'는 int 또는 double일 수 있으므로, 안전하게 num으로 처리합니다.
final accessExpValue = data['exp'] as num?;
// 'exp' (Expiration Time) 필드는 Access Token의 만료 시간을 나타냅니다. Unix 타임스탬프(초 단위) 값입니다.
// 이 값을 Dart의 DateTime 객체로 변환합니다. (1000을 곱해 밀리초 단위로 만듦)
final accessExp = accessExpValue != null
? DateTime.fromMillisecondsSinceEpoch(accessExpValue.toInt() * 1000)
: 'N/A';
// 'rexp' (Refresh Expiration) 필드는 Descope가 사용하는 커스텀 필드로, Refresh Token의 만료 시간을 ISO 8601 형식의 문자열로 나타냅니다.
final refreshExp = data['rexp'] ?? 'N/A';
// 확인된 만료 시간 정보들을 디버그 콘솔에 출력합니다.
debugPrint("""
[Auth] Session Token Details ---
- Access Token Expires: $accessExp
- Refresh Token Expires: $refreshExp
""");
} catch (e) {
// JWT를 해석하는 과정에서 오류가 발생하면 콘솔에 에러를 출력합니다.
debugPrint("[Auth] Failed to decode or log token details: $e");
}
}
void _onLoginSuccess(String token) {
if (!mounted) return;
_logTokenDetails(token);
// [FIX] 감사 로그에 실제 사용자 ID를 전송하기 위해 토큰에서 ID를 추출합니다.
final userId = _getUserIdFromJwt(token);
// Record Audit Log
AuditService.logEvent(
userId: "unknown", // In real apps, parse token to get user ID
userId: userId,
eventType: "LOGIN_SUCCESS",
status: "SUCCESS",
details: "User logged in via Baron SSO",

View File

@@ -52,6 +52,7 @@ void main() async {
// Load saved session if any
try {
// 저장된 세션 불러옴
await Descope.sessionManager.loadSession();
} catch (e) {
_log.warning("Failed to load session: $e");
@@ -115,9 +116,9 @@ final _router = GoRouter(
],
redirect: (context, state) {
final isLoggedIn =
Descope.sessionManager.session?.refreshToken.isExpired == false;
Descope.sessionManager.session?.refreshToken?.isExpired == false;
final path = state.uri.path;
final isLoggingIn = path == '/' || path.startsWith('/verify/') || path.startsWith('/admin/') || path == '/approve';
final isLoggingIn = path == '/' || path.startsWith('/verify/') || path == '/approve';
_routerLogger.fine("Redirect check - Path: $path, IsLoggedIn: $isLoggedIn");