forked from baron/baron-sso
OIDC로 빠지는 분기 점검 login_challenge 복구 fallback 추가
This commit is contained in:
@@ -3,6 +3,14 @@ class WebWindow {
|
|||||||
|
|
||||||
void redirectTo(String url) {}
|
void redirectTo(String url) {}
|
||||||
|
|
||||||
|
String currentHref() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentSearch() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
void alert(String message) {}
|
void alert(String message) {}
|
||||||
|
|
||||||
void close() {}
|
void close() {}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
||||||
|
|
||||||
import 'dart:html' as html;
|
import 'dart:html' as html;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class WebWindow {
|
class WebWindow {
|
||||||
void setTitle(String title) {
|
void setTitle(String title) {
|
||||||
@@ -8,7 +9,48 @@ class WebWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void redirectTo(String url) {
|
void redirectTo(String url) {
|
||||||
|
final currentHref = html.window.location.href;
|
||||||
|
Uri? targetUri;
|
||||||
|
try {
|
||||||
|
targetUri = Uri.parse(url);
|
||||||
|
} catch (_) {
|
||||||
|
debugPrint("[WebWindow] redirectTo parse failed: url=$url");
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentPort = int.tryParse(html.window.location.port);
|
||||||
|
final sameOrigin =
|
||||||
|
targetUri != null &&
|
||||||
|
targetUri.scheme == html.window.location.protocol.replaceAll(':', '') &&
|
||||||
|
targetUri.host == html.window.location.hostname &&
|
||||||
|
(!targetUri.hasPort || targetUri.port == currentPort);
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
"[WebWindow] redirectTo start: current=$currentHref, target=$url, target_host=${targetUri?.host ?? ''}, target_path=${targetUri?.path ?? ''}, same_origin=$sameOrigin",
|
||||||
|
);
|
||||||
|
|
||||||
html.window.location.href = url;
|
html.window.location.href = url;
|
||||||
|
|
||||||
|
// 이동이 차단되거나 즉시 원위치되는 경우를 추적하기 위한 후속 로그입니다.
|
||||||
|
Future<void>.delayed(const Duration(milliseconds: 800), () {
|
||||||
|
final nowHref = html.window.location.href;
|
||||||
|
if (nowHref == currentHref) {
|
||||||
|
debugPrint(
|
||||||
|
"[WebWindow] redirectTo no-op detected: current URL did not change after navigation attempt",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
"[WebWindow] redirectTo post-check: location changed to $nowHref",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentHref() {
|
||||||
|
return html.window.location.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentSearch() {
|
||||||
|
return html.window.location.search ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
void alert(String message) {
|
void alert(String message) {
|
||||||
|
|||||||
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/auth_token_store.dart';
|
||||||
import '../../../core/services/oidc_redirect_guard.dart';
|
import '../../../core/services/oidc_redirect_guard.dart';
|
||||||
import '../../../core/notifiers/auth_notifier.dart';
|
import '../../../core/notifiers/auth_notifier.dart';
|
||||||
|
import '../domain/login_challenge_resolver.dart';
|
||||||
import '../domain/password_login_flow_policy.dart';
|
import '../domain/password_login_flow_policy.dart';
|
||||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||||
import '../../../core/services/web_window.dart';
|
import '../../../core/services/web_window.dart';
|
||||||
@@ -99,8 +100,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_loginChallenge =
|
final challengeResolution = _resolveLoginChallenge(uri);
|
||||||
widget.loginChallenge ?? uri.queryParameters['login_challenge'];
|
_loginChallenge = challengeResolution.value;
|
||||||
|
_logLoginChallengeDiagnostics(
|
||||||
|
phase: 'init',
|
||||||
|
resolution: challengeResolution,
|
||||||
|
);
|
||||||
final loginIdParam = uri.queryParameters['loginId'];
|
final loginIdParam = uri.queryParameters['loginId'];
|
||||||
final codeParam = uri.queryParameters['code'];
|
final codeParam = uri.queryParameters['code'];
|
||||||
final pendingRefParam = uri.queryParameters['pendingRef'];
|
final pendingRefParam = uri.queryParameters['pendingRef'];
|
||||||
@@ -273,6 +278,32 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
bool get _hasLoginChallenge =>
|
bool get _hasLoginChallenge =>
|
||||||
_loginChallenge != null && _loginChallenge!.isNotEmpty;
|
_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({
|
void _logOidcRedirectDiagnostics({
|
||||||
required String source,
|
required String source,
|
||||||
required OidcRedirectCheckResult checked,
|
required OidcRedirectCheckResult checked,
|
||||||
@@ -864,6 +895,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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(
|
final res = await AuthProxyService.loginWithPassword(
|
||||||
loginId,
|
loginId,
|
||||||
password,
|
password,
|
||||||
@@ -883,6 +923,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
"[Auth] Password login outcome: has_login_challenge=$_hasLoginChallenge, next_action=$nextAction, has_jwt=$hasJwt",
|
"[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) {
|
switch (nextAction) {
|
||||||
case PasswordLoginNextAction.redirectToOidc:
|
case PasswordLoginNextAction.redirectToOidc:
|
||||||
|
|||||||
@@ -147,14 +147,17 @@ final _router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'login',
|
path: 'login',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
final loginChallenge =
|
||||||
|
state.uri.queryParameters['login_challenge'];
|
||||||
final redirectUrl =
|
final redirectUrl =
|
||||||
state.uri.queryParameters['redirect_uri'] ??
|
state.uri.queryParameters['redirect_uri'] ??
|
||||||
state.uri.queryParameters['redirect_url'];
|
state.uri.queryParameters['redirect_url'];
|
||||||
_routerLogger.info(
|
_routerLogger.info(
|
||||||
"Navigating to /login, redirect: $redirectUrl",
|
"Navigating to /login with login_challenge: $loginChallenge, redirect: $redirectUrl",
|
||||||
);
|
);
|
||||||
return LoginScreen(
|
return LoginScreen(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
|
loginChallenge: loginChallenge,
|
||||||
redirectUrl: redirectUrl,
|
redirectUrl: redirectUrl,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
69
userfront/test/login_challenge_resolver_test.dart
Normal file
69
userfront/test/login_challenge_resolver_test.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:userfront/features/auth/domain/login_challenge_resolver.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('login_challenge_resolver', () {
|
||||||
|
test('widget 값이 있으면 최우선으로 사용', () {
|
||||||
|
final resolved = resolveLoginChallenge(
|
||||||
|
widgetLoginChallenge: 'widget-challenge',
|
||||||
|
uri: Uri.parse('/ko/login'),
|
||||||
|
rawSearch: '?login_challenge=raw-search',
|
||||||
|
rawHref: 'https://sso-test.hmac.kr/ko/login?login_challenge=raw-href',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved.value, 'widget-challenge');
|
||||||
|
expect(resolved.source, LoginChallengeSource.widget);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('widget 값이 없으면 URI query에서 복구', () {
|
||||||
|
final resolved = resolveLoginChallenge(
|
||||||
|
widgetLoginChallenge: null,
|
||||||
|
uri: Uri.parse('/ko/login?login_challenge=uri-query'),
|
||||||
|
rawSearch: '',
|
||||||
|
rawHref: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved.value, 'uri-query');
|
||||||
|
expect(resolved.source, LoginChallengeSource.uriQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('URI query가 비어 있으면 raw search에서 복구', () {
|
||||||
|
final resolved = resolveLoginChallenge(
|
||||||
|
widgetLoginChallenge: null,
|
||||||
|
uri: Uri.parse('/ko/login'),
|
||||||
|
rawSearch: '?login_challenge=raw-search-value&x=1',
|
||||||
|
rawHref: '',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved.value, 'raw-search-value');
|
||||||
|
expect(resolved.source, LoginChallengeSource.rawSearch);
|
||||||
|
expect(resolved.rawSearchHasLoginChallenge, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('raw search도 비어 있으면 raw href에서 복구', () {
|
||||||
|
final resolved = resolveLoginChallenge(
|
||||||
|
widgetLoginChallenge: null,
|
||||||
|
uri: Uri.parse('/ko/login'),
|
||||||
|
rawSearch: '',
|
||||||
|
rawHref:
|
||||||
|
'https://sso-test.hmac.kr/ko/login?a=1&login_challenge=raw-href-value#fragment',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved.value, 'raw-href-value');
|
||||||
|
expect(resolved.source, LoginChallengeSource.rawHref);
|
||||||
|
expect(resolved.rawHrefHasLoginChallenge, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('값이 전부 없으면 missing', () {
|
||||||
|
final resolved = resolveLoginChallenge(
|
||||||
|
widgetLoginChallenge: null,
|
||||||
|
uri: Uri.parse('/ko/login'),
|
||||||
|
rawSearch: '',
|
||||||
|
rawHref: 'https://sso-test.hmac.kr/ko/login?x=1',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolved.value, isNull);
|
||||||
|
expect(resolved.source, LoginChallengeSource.missing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -28,6 +28,21 @@ Widget _buildTestApp(String initialLocation) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'login',
|
||||||
|
builder: (context, state) {
|
||||||
|
final challenge = state.uri.queryParameters['login_challenge'];
|
||||||
|
final redirect =
|
||||||
|
state.uri.queryParameters['redirect_uri'] ??
|
||||||
|
state.uri.queryParameters['redirect_url'] ??
|
||||||
|
'';
|
||||||
|
return Scaffold(
|
||||||
|
body: Text(
|
||||||
|
'login|challenge=${challenge ?? ''}|redirect=$redirect',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
builder: (context, state) =>
|
builder: (context, state) =>
|
||||||
@@ -45,7 +60,7 @@ Widget _buildTestApp(String initialLocation) {
|
|||||||
final isLoggedIn =
|
final isLoggedIn =
|
||||||
AuthTokenStore.getToken() != null || AuthTokenStore.usesCookie();
|
AuthTokenStore.getToken() != null || AuthTokenStore.usesCookie();
|
||||||
final path = stripLocalePath(state.uri);
|
final path = stripLocalePath(state.uri);
|
||||||
final isPublicPath = path == '/signin';
|
final isPublicPath = path == '/signin' || path == '/login';
|
||||||
if (isPublicPath) {
|
if (isPublicPath) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -70,6 +85,25 @@ void main() {
|
|||||||
LocaleRegistry.resetForTest();
|
LocaleRegistry.resetForTest();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('/login: login_challenge와 redirect_uri를 전달', (tester) async {
|
||||||
|
final encodedRedirectUri = Uri.encodeComponent(
|
||||||
|
'https://rp.example.com/callback?x=1',
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_buildTestApp(
|
||||||
|
'/en/login?login_challenge=lc_999&redirect_uri=$encodedRedirectUri',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.text(
|
||||||
|
'login|challenge=lc_999|redirect=https://rp.example.com/callback?x=1',
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('비로그인: redirect_uri/login_challenge가 signin으로 전달', (
|
testWidgets('비로그인: redirect_uri/login_challenge가 signin으로 전달', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
Reference in New Issue
Block a user