forked from baron/baron-sso
OIDC로 빠지는 분기 점검 login_challenge 복구 fallback 추가
This commit is contained in:
195
userfront/lib/features/auth/domain/login_challenge_resolver.dart
Normal file
195
userfront/lib/features/auth/domain/login_challenge_resolver.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
enum LoginChallengeSource { widget, uriQuery, rawSearch, rawHref, missing }
|
||||
|
||||
class LoginChallengeResolution {
|
||||
final String? value;
|
||||
final LoginChallengeSource source;
|
||||
final bool uriHasLoginChallenge;
|
||||
final bool rawSearchHasLoginChallenge;
|
||||
final bool rawHrefHasLoginChallenge;
|
||||
|
||||
const LoginChallengeResolution({
|
||||
required this.value,
|
||||
required this.source,
|
||||
required this.uriHasLoginChallenge,
|
||||
required this.rawSearchHasLoginChallenge,
|
||||
required this.rawHrefHasLoginChallenge,
|
||||
});
|
||||
|
||||
Map<String, Object?> toDiagnostics() {
|
||||
return {
|
||||
'resolved_value_len': value?.length ?? 0,
|
||||
'resolved_source': source.name,
|
||||
'uri_has_login_challenge': uriHasLoginChallenge,
|
||||
'raw_search_has_login_challenge': rawSearchHasLoginChallenge,
|
||||
'raw_href_has_login_challenge': rawHrefHasLoginChallenge,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
LoginChallengeResolution resolveLoginChallenge({
|
||||
String? widgetLoginChallenge,
|
||||
required Uri uri,
|
||||
String? rawSearch,
|
||||
String? rawHref,
|
||||
}) {
|
||||
final widgetValue = _normalizeChallenge(widgetLoginChallenge);
|
||||
if (widgetValue != null) {
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.widget,
|
||||
uriHasLoginChallenge: false,
|
||||
rawSearchHasLoginChallenge: false,
|
||||
rawHrefHasLoginChallenge: false,
|
||||
).copyWith(value: widgetValue);
|
||||
}
|
||||
|
||||
final uriValue = _normalizeChallenge(uri.queryParameters['login_challenge']);
|
||||
if (uriValue != null) {
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.uriQuery,
|
||||
uriHasLoginChallenge: true,
|
||||
rawSearchHasLoginChallenge: false,
|
||||
rawHrefHasLoginChallenge: false,
|
||||
).copyWith(value: uriValue);
|
||||
}
|
||||
|
||||
final rawSearchValue = _normalizeChallenge(
|
||||
_extractQueryParamFromRawQuery(rawSearch, 'login_challenge'),
|
||||
);
|
||||
if (rawSearchValue != null) {
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.rawSearch,
|
||||
uriHasLoginChallenge: false,
|
||||
rawSearchHasLoginChallenge: true,
|
||||
rawHrefHasLoginChallenge: false,
|
||||
).copyWith(value: rawSearchValue);
|
||||
}
|
||||
|
||||
final rawHrefValue = _normalizeChallenge(
|
||||
_extractQueryParamFromRawHref(rawHref, 'login_challenge'),
|
||||
);
|
||||
if (rawHrefValue != null) {
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.rawHref,
|
||||
uriHasLoginChallenge: false,
|
||||
rawSearchHasLoginChallenge: false,
|
||||
rawHrefHasLoginChallenge: true,
|
||||
).copyWith(value: rawHrefValue);
|
||||
}
|
||||
|
||||
return const LoginChallengeResolution(
|
||||
value: null,
|
||||
source: LoginChallengeSource.missing,
|
||||
uriHasLoginChallenge: false,
|
||||
rawSearchHasLoginChallenge: false,
|
||||
rawHrefHasLoginChallenge: false,
|
||||
);
|
||||
}
|
||||
|
||||
String? _normalizeChallenge(String? value) {
|
||||
final trimmed = value?.trim();
|
||||
if (trimmed == null || trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
String? _extractQueryParamFromRawHref(String? rawHref, String key) {
|
||||
final href = rawHref?.trim();
|
||||
if (href == null || href.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final parsed = Uri.tryParse(href);
|
||||
final fromParsed = parsed?.queryParameters[key];
|
||||
final normalizedParsed = _normalizeChallenge(fromParsed);
|
||||
if (normalizedParsed != null) {
|
||||
return normalizedParsed;
|
||||
}
|
||||
|
||||
final question = href.indexOf('?');
|
||||
if (question < 0) {
|
||||
return null;
|
||||
}
|
||||
final hash = href.indexOf('#', question + 1);
|
||||
final rawQuery = hash < 0
|
||||
? href.substring(question + 1)
|
||||
: href.substring(question + 1, hash);
|
||||
return _extractQueryParamFromRawQuery(rawQuery, key);
|
||||
}
|
||||
|
||||
String? _extractQueryParamFromRawQuery(String? rawQuery, String key) {
|
||||
final query = rawQuery?.trim();
|
||||
if (query == null || query.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final normalizedQuery = query.startsWith('?') ? query.substring(1) : query;
|
||||
if (normalizedQuery.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final parsed = Uri.splitQueryString(normalizedQuery);
|
||||
final value = _normalizeChallenge(parsed[key]);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
} catch (_) {
|
||||
// URI 파싱이 실패하면 수동 파싱으로 보완합니다.
|
||||
}
|
||||
|
||||
for (final pair in normalizedQuery.split('&')) {
|
||||
if (pair.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final equalIndex = pair.indexOf('=');
|
||||
final rawKey = equalIndex < 0 ? pair : pair.substring(0, equalIndex);
|
||||
final decodedKey = _decodeQueryComponentSafe(rawKey);
|
||||
if (decodedKey != key) {
|
||||
continue;
|
||||
}
|
||||
if (equalIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
final rawValue = pair.substring(equalIndex + 1);
|
||||
final decodedValue = _normalizeChallenge(
|
||||
_decodeQueryComponentSafe(rawValue),
|
||||
);
|
||||
if (decodedValue != null) {
|
||||
return decodedValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _decodeQueryComponentSafe(String value) {
|
||||
try {
|
||||
return Uri.decodeQueryComponent(value);
|
||||
} catch (_) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
extension on LoginChallengeResolution {
|
||||
LoginChallengeResolution copyWith({
|
||||
String? value,
|
||||
LoginChallengeSource? source,
|
||||
bool? uriHasLoginChallenge,
|
||||
bool? rawSearchHasLoginChallenge,
|
||||
bool? rawHrefHasLoginChallenge,
|
||||
}) {
|
||||
return LoginChallengeResolution(
|
||||
value: value ?? this.value,
|
||||
source: source ?? this.source,
|
||||
uriHasLoginChallenge: uriHasLoginChallenge ?? this.uriHasLoginChallenge,
|
||||
rawSearchHasLoginChallenge:
|
||||
rawSearchHasLoginChallenge ?? this.rawSearchHasLoginChallenge,
|
||||
rawHrefHasLoginChallenge:
|
||||
rawHrefHasLoginChallenge ?? this.rawHrefHasLoginChallenge,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import '../../../core/services/auth_proxy_service.dart';
|
||||
import '../../../core/services/auth_token_store.dart';
|
||||
import '../../../core/services/oidc_redirect_guard.dart';
|
||||
import '../../../core/notifiers/auth_notifier.dart';
|
||||
import '../domain/login_challenge_resolver.dart';
|
||||
import '../domain/password_login_flow_policy.dart';
|
||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||
import '../../../core/services/web_window.dart';
|
||||
@@ -99,8 +100,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
}
|
||||
|
||||
_loginChallenge =
|
||||
widget.loginChallenge ?? uri.queryParameters['login_challenge'];
|
||||
final challengeResolution = _resolveLoginChallenge(uri);
|
||||
_loginChallenge = challengeResolution.value;
|
||||
_logLoginChallengeDiagnostics(
|
||||
phase: 'init',
|
||||
resolution: challengeResolution,
|
||||
);
|
||||
final loginIdParam = uri.queryParameters['loginId'];
|
||||
final codeParam = uri.queryParameters['code'];
|
||||
final pendingRefParam = uri.queryParameters['pendingRef'];
|
||||
@@ -273,6 +278,32 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
bool get _hasLoginChallenge =>
|
||||
_loginChallenge != null && _loginChallenge!.isNotEmpty;
|
||||
|
||||
LoginChallengeResolution _resolveLoginChallenge(Uri uri) {
|
||||
return resolveLoginChallenge(
|
||||
widgetLoginChallenge: widget.loginChallenge,
|
||||
uri: uri,
|
||||
rawSearch: webWindow.currentSearch(),
|
||||
rawHref: webWindow.currentHref(),
|
||||
);
|
||||
}
|
||||
|
||||
void _logLoginChallengeDiagnostics({
|
||||
required String phase,
|
||||
required LoginChallengeResolution resolution,
|
||||
}) {
|
||||
final current = Uri.base;
|
||||
final currentQueryKeys = current.queryParameters.keys.toList()..sort();
|
||||
final payload = <String, Object?>{
|
||||
'phase': phase,
|
||||
'current_path': current.path,
|
||||
'current_query_keys': currentQueryKeys,
|
||||
'stored_has_login_challenge': _hasLoginChallenge,
|
||||
'stored_login_challenge_len': _loginChallenge?.length ?? 0,
|
||||
...resolution.toDiagnostics(),
|
||||
};
|
||||
debugPrint("[Auth] login_challenge diagnostics: ${jsonEncode(payload)}");
|
||||
}
|
||||
|
||||
void _logOidcRedirectDiagnostics({
|
||||
required String source,
|
||||
required OidcRedirectCheckResult checked,
|
||||
@@ -864,6 +895,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
}
|
||||
|
||||
try {
|
||||
final challengeResolution = _resolveLoginChallenge(Uri.base);
|
||||
if (!_hasLoginChallenge && challengeResolution.value != null) {
|
||||
_loginChallenge = challengeResolution.value;
|
||||
}
|
||||
_logLoginChallengeDiagnostics(
|
||||
phase: 'password_submit',
|
||||
resolution: challengeResolution,
|
||||
);
|
||||
|
||||
final res = await AuthProxyService.loginWithPassword(
|
||||
loginId,
|
||||
password,
|
||||
@@ -883,6 +923,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
||||
debugPrint(
|
||||
"[Auth] Password login outcome: has_login_challenge=$_hasLoginChallenge, next_action=$nextAction, has_jwt=$hasJwt",
|
||||
);
|
||||
if (!_hasLoginChallenge) {
|
||||
debugPrint(
|
||||
"[Auth] WARNING: password login proceeded without login_challenge; treated as local login flow",
|
||||
);
|
||||
}
|
||||
|
||||
switch (nextAction) {
|
||||
case PasswordLoginNextAction.redirectToOidc:
|
||||
|
||||
Reference in New Issue
Block a user