forked from baron/baron-sso
tenant 제한 에러 처리 보안
This commit is contained in:
@@ -516,8 +516,13 @@ class AuthProxyService {
|
|||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
} else {
|
} else {
|
||||||
final errorBody = jsonDecode(response.body);
|
final errorBody = jsonDecode(response.body);
|
||||||
throw Exception(
|
final rawDetails = errorBody['details'];
|
||||||
errorBody['error'] ?? tr('err.userfront.auth_proxy.oidc_accept'),
|
throw AuthProxyException(
|
||||||
|
errorCode: (errorBody['code'] ?? '').toString(),
|
||||||
|
message:
|
||||||
|
(errorBody['error'] ?? tr('err.userfront.auth_proxy.oidc_accept'))
|
||||||
|
.toString(),
|
||||||
|
details: rawDetails is Map<String, dynamic> ? rawDetails : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,29 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:userfront/core/i18n/locale_utils.dart';
|
||||||
import 'package:userfront/core/services/auth_proxy_service.dart';
|
import 'package:userfront/core/services/auth_proxy_service.dart';
|
||||||
|
|
||||||
bool shouldRouteConsentErrorToErrorScreen(Object error) {
|
bool shouldRouteTenantAccessErrorToErrorScreen(Object error) {
|
||||||
return error is AuthProxyException && error.errorCode == 'tenant_not_allowed';
|
return error is AuthProxyException && error.errorCode == 'tenant_not_allowed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool shouldRouteConsentErrorToErrorScreen(Object error) {
|
||||||
|
return shouldRouteTenantAccessErrorToErrorScreen(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
String buildTenantAccessErrorPath(Object error, Uri baseUri) {
|
||||||
|
final authError = error as AuthProxyException;
|
||||||
|
final localeCode =
|
||||||
|
extractLocaleFromPath(baseUri) ?? resolvePreferredLocaleCode();
|
||||||
|
return buildLocalizedPath(
|
||||||
|
localeCode,
|
||||||
|
Uri(
|
||||||
|
path: '/error',
|
||||||
|
queryParameters: {
|
||||||
|
'error': authError.errorCode,
|
||||||
|
'error_description': authError.message,
|
||||||
|
if (authError.details != null) 'details': jsonEncode(authError.details),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:userfront/i18n.dart';
|
import 'package:userfront/i18n.dart';
|
||||||
@@ -153,19 +151,7 @@ class _ConsentScreenState extends State<ConsentScreen> {
|
|||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final localeCode =
|
final target = buildTenantAccessErrorPath(e, Uri.base);
|
||||||
extractLocaleFromPath(Uri.base) ?? resolvePreferredLocaleCode();
|
|
||||||
final target = buildLocalizedPath(
|
|
||||||
localeCode,
|
|
||||||
Uri(
|
|
||||||
path: '/error',
|
|
||||||
queryParameters: {
|
|
||||||
'error': e.errorCode,
|
|
||||||
'error_description': e.message,
|
|
||||||
if (e.details != null) 'details': jsonEncode(e.details),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
context.go(target);
|
context.go(target);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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/login_challenge_resolver.dart';
|
||||||
import '../domain/cookie_session_policy.dart';
|
import '../domain/cookie_session_policy.dart';
|
||||||
|
import '../domain/consent_error_routing.dart';
|
||||||
import '../domain/login_link_route_policy.dart';
|
import '../domain/login_link_route_policy.dart';
|
||||||
import '../domain/verification_completion_route.dart';
|
import '../domain/verification_completion_route.dart';
|
||||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||||
@@ -1666,6 +1667,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
return;
|
return;
|
||||||
} else {}
|
} else {}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e is AuthProxyException &&
|
||||||
|
shouldRouteTenantAccessErrorToErrorScreen(e)) {
|
||||||
|
final target = buildTenantAccessErrorPath(e, Uri.base);
|
||||||
|
if (mounted) {
|
||||||
|
context.go(target);
|
||||||
|
} else {
|
||||||
|
webWindow.redirectTo(target);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
_showError(tr('msg.userfront.login.oidc_failed'));
|
_showError(tr('msg.userfront.login.oidc_failed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,37 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'acceptOidcLogin error는 code/message/details를 AuthProxyException으로 보존한다',
|
||||||
|
() async {
|
||||||
|
client.enqueueJson({
|
||||||
|
'code': 'tenant_not_allowed',
|
||||||
|
'error': 'tenant blocked',
|
||||||
|
'details': {
|
||||||
|
'allowed_tenants': ['gp'],
|
||||||
|
},
|
||||||
|
}, statusCode: 403);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
AuthProxyService.acceptOidcLogin('login-challenge', token: 'jwt'),
|
||||||
|
throwsA(
|
||||||
|
isA<AuthProxyException>()
|
||||||
|
.having(
|
||||||
|
(error) => error.errorCode,
|
||||||
|
'code',
|
||||||
|
'tenant_not_allowed',
|
||||||
|
)
|
||||||
|
.having((error) => error.message, 'message', 'tenant blocked')
|
||||||
|
.having(
|
||||||
|
(error) => error.details?['allowed_tenants'],
|
||||||
|
'details',
|
||||||
|
['gp'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'approveQrLogin은 credential mode와 bearer token payload를 지원한다',
|
'approveQrLogin은 credential mode와 bearer token payload를 지원한다',
|
||||||
() async {
|
() async {
|
||||||
|
|||||||
@@ -20,4 +20,33 @@ void main() {
|
|||||||
|
|
||||||
expect(shouldRouteConsentErrorToErrorScreen(error), isFalse);
|
expect(shouldRouteConsentErrorToErrorScreen(error), isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tenant_not_allowed auth error also routes to error screen', () {
|
||||||
|
const error = AuthProxyException(
|
||||||
|
errorCode: 'tenant_not_allowed',
|
||||||
|
message: '허용되지 않은 테넌트입니다.',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(shouldRouteTenantAccessErrorToErrorScreen(error), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildTenantAccessErrorPath builds userfront error route', () {
|
||||||
|
const error = AuthProxyException(
|
||||||
|
errorCode: 'tenant_not_allowed',
|
||||||
|
message: '허용되지 않은 테넌트입니다.',
|
||||||
|
details: {
|
||||||
|
'allowed_tenants': ['tenant-a'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final target = buildTenantAccessErrorPath(
|
||||||
|
error,
|
||||||
|
Uri.parse('https://sso-test.hmac.kr/ko?login_challenge=abc'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(target, contains('/error?'));
|
||||||
|
expect(target, contains('error=tenant_not_allowed'));
|
||||||
|
expect(target, contains('error_description='));
|
||||||
|
expect(target, contains('details='));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user