TF-3826 Try guessing webfinger when dns look up fail

This commit is contained in:
DatDang
2025-06-23 15:29:33 +07:00
committed by Dat H. Pham
parent d57b4cdb63
commit c558f89bd7
12 changed files with 357 additions and 2 deletions
@@ -0,0 +1,24 @@
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
enum OidcGuessingOrigin {
empty(origin: ''),
autoDiscover(origin: 'autodiscover'),
jmap(origin: 'jmap');
const OidcGuessingOrigin({required this.origin});
final String origin;
String url(String email) {
if (!EmailUtils.isEmailAddressValid(email)) {
throw ArgumentError('Invalid email address: $email');
}
final emailDomain = email.split('@').last;
return switch (this) {
OidcGuessingOrigin.empty => 'https://$emailDomain',
_ => origin.trim().isEmpty
? OidcGuessingOrigin.empty.url(email)
: 'https://$origin.$emailDomain',
};
}
}
@@ -13,6 +13,10 @@ class DNSLookupToGetJmapUrlSuccess extends UIState {
}
class DNSLookupToGetJmapUrlFailure extends FeatureFailure {
DNSLookupToGetJmapUrlFailure(
dynamic exception, {
required this.email,
}) : super(exception: exception);
DNSLookupToGetJmapUrlFailure(dynamic exception) : super(exception: exception);
final String email;
}
@@ -0,0 +1,18 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:model/oidc/response/oidc_response.dart';
class TryingGuessingWebFinger extends LoadingState {}
class TryGuessingWebFingerSuccess extends UIState {
TryGuessingWebFingerSuccess(this.oidcResponse);
final OIDCResponse oidcResponse;
@override
List<Object?> get props => [oidcResponse];
}
class TryGuessingWebFingerFailure extends FeatureFailure {
TryGuessingWebFingerFailure({super.exception});
}
@@ -15,7 +15,10 @@ class DNSLookupToGetJmapUrlInteractor {
final jmapUrl = await _loginRepository.dnsLookupToGetJmapUrl(emailAddress);
yield Right<Failure, Success>(DNSLookupToGetJmapUrlSuccess(jmapUrl));
} catch (e) {
yield Left<Failure, Success>(DNSLookupToGetJmapUrlFailure(e));
yield Left<Failure, Success>(DNSLookupToGetJmapUrlFailure(
e,
email: emailAddress,
));
}
}
}
@@ -0,0 +1,62 @@
import 'dart:async';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/utils/app_logger.dart';
import 'package:dartz/dartz.dart';
import 'package:model/oidc/request/oidc_request.dart';
import 'package:model/oidc/response/oidc_response.dart';
import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart';
import 'package:tmail_ui_user/features/login/domain/state/try_guessing_web_finger_state.dart';
class TryGuessingWebFingerInteractor {
const TryGuessingWebFingerInteractor(this._authenticationOIDCRepository);
final AuthenticationOIDCRepository _authenticationOIDCRepository;
Stream<Either<Failure, Success>> execute(
List<OIDCRequest> oidcRequests,
) async* {
try {
yield Right(TryingGuessingWebFinger());
final futures = oidcRequests.map(_checkOIDCAvailableFromOidcRequest).toList();
final completer = Completer<OIDCResponse?>();
for (final future in futures) {
future.then((response) {
if (response != null && !completer.isCompleted) {
completer.complete(response);
}
});
}
Future.wait(futures).then((_) {
if (!completer.isCompleted) {
completer.complete(null);
}
});
final firstNonNullResponse = await completer.future;
if (firstNonNullResponse == null) {
yield Left(TryGuessingWebFingerFailure());
} else {
yield Right(TryGuessingWebFingerSuccess(firstNonNullResponse));
}
} catch (e) {
logError('$runtimeType::execute(): Exception = $e');
yield Left(TryGuessingWebFingerFailure(exception: e));
}
}
Future<OIDCResponse?> _checkOIDCAvailableFromOidcRequest(
OIDCRequest oidcRequest,
) async {
try {
return await _authenticationOIDCRepository.checkOIDCIsAvailable(
oidcRequest,
);
} catch (_) {
return null;
}
}
}
@@ -0,0 +1,19 @@
import 'package:model/oidc/request/oidc_request.dart';
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
import 'package:tmail_ui_user/features/login/domain/model/oidc_guessing_origin.dart';
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
extension GenerateOidcGuessingUrls on LoginController {
List<OIDCRequest> generateOidcGuessingUrls(String email) {
if (!EmailUtils.isEmailAddressValid(email)) {
return [];
}
return OidcGuessingOrigin.values.map(
(guessingOrigin) => OIDCRequest(
baseUrl: guessingOrigin.url(email),
resourceUrl: guessingOrigin.url(email)
)
).toList();
}
}
@@ -25,6 +25,7 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_con
import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/try_guessing_web_finger_interactor.dart';
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart';
import 'package:tmail_ui_user/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart';
@@ -52,6 +53,7 @@ class LoginBindings extends BaseBindings {
Get.find<GetAllRecentLoginUsernameOnMobileInteractor>(),
Get.find<DNSLookupToGetJmapUrlInteractor>(),
Get.find<SignInTwakeWorkplaceInteractor>(),
Get.find<TryGuessingWebFingerInteractor>(),
));
}
@@ -112,6 +114,9 @@ class LoginBindings extends BaseBindings {
Get.find<AccountRepository>(),
Get.find<CredentialRepository>(),
));
Get.lazyPut(() => TryGuessingWebFingerInteractor(
Get.find<AuthenticationOIDCRepository>(),
));
}
@override
@@ -38,6 +38,7 @@ import 'package:tmail_ui_user/features/login/domain/state/get_authentication_inf
import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_state.dart';
import 'package:tmail_ui_user/features/login/domain/state/get_stored_oidc_configuration_state.dart';
import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.dart';
import 'package:tmail_ui_user/features/login/domain/state/try_guessing_web_finger_state.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/authenticate_oidc_on_browser_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart';
@@ -50,6 +51,8 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_con
import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/try_guessing_web_finger_interactor.dart';
import 'package:tmail_ui_user/features/login/presentation/extensions/generate_oidc_guessing_urls.dart';
import 'package:tmail_ui_user/features/login/presentation/extensions/handle_openid_configuration.dart';
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart';
@@ -58,6 +61,7 @@ import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twak
import 'package:tmail_ui_user/main/deep_links/deep_link_data.dart';
import 'package:tmail_ui_user/main/deep_links/deep_links_manager.dart';
import 'package:tmail_ui_user/main/deep_links/open_app_deep_link_data.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -80,6 +84,7 @@ class LoginController extends ReloadableController {
final GetAllRecentLoginUsernameOnMobileInteractor _getAllRecentLoginUsernameOnMobileInteractor;
final DNSLookupToGetJmapUrlInteractor _dnsLookupToGetJmapUrlInteractor;
final SignInTwakeWorkplaceInteractor _signInTwakeWorkplaceInteractor;
final TryGuessingWebFingerInteractor _tryGuessingWebFingerInteractor;
final TextEditingController urlInputController = TextEditingController();
final TextEditingController usernameInputController = TextEditingController();
@@ -111,6 +116,7 @@ class LoginController extends ReloadableController {
this._getAllRecentLoginUsernameOnMobileInteractor,
this._dnsLookupToGetJmapUrlInteractor,
this._signInTwakeWorkplaceInteractor,
this._tryGuessingWebFingerInteractor,
);
@override
@@ -164,6 +170,8 @@ class LoginController extends ReloadableController {
SmartDialog.dismiss();
clearAllData();
} else if (failure is DNSLookupToGetJmapUrlFailure) {
_handleDNSLookupToGetJmapUrlFailure(failure);
} else if (failure is TryGuessingWebFingerFailure) {
_username = null;
_clearTextInputField();
_showBaseUrlForm();
@@ -188,6 +196,9 @@ class LoginController extends ReloadableController {
_loginSuccessAction(success);
} else if (success is DNSLookupToGetJmapUrlSuccess) {
_handleDNSLookupToGetJmapUrlSuccess(success);
} else if (success is TryGuessingWebFingerSuccess) {
onBaseUrlChange(success.oidcResponse.subject);
getOIDCConfiguration(success.oidcResponse);
} else if (success is SignInTwakeWorkplaceSuccess) {
_synchronizeTokenAndGetSession(
baseUri: success.baseUri,
@@ -217,6 +228,10 @@ class LoginController extends ReloadableController {
} else if (failure is GetSessionFailure) {
SmartDialog.dismiss();
clearAllData();
} else if (connectionErrorWhenLookupDns(failure)) {
_handleDNSLookupToGetJmapUrlFailure(
failure as DNSLookupToGetJmapUrlFailure,
);
} else {
super.handleUrgentException(failure: failure, exception: exception);
}
@@ -232,6 +247,19 @@ class LoginController extends ReloadableController {
);
}
bool connectionErrorWhenLookupDns(Failure? failure) {
return failure is DNSLookupToGetJmapUrlFailure &&
failure.exception is ConnectionError;
}
void _handleDNSLookupToGetJmapUrlFailure(
DNSLookupToGetJmapUrlFailure failure,
) {
consumeState(_tryGuessingWebFingerInteractor.execute(
generateOidcGuessingUrls(failure.email),
));
}
void _registerDeepLinks() {
_deepLinksManager = getBinding<DeepLinksManager>();
_deepLinksManager?.clearPendingDeepLinkData();