added support for 2fa login with browser flow

This commit is contained in:
2026-08-10 20:00:38 +02:00
parent 889d8f67c5
commit ccb22a497d
13 changed files with 805 additions and 35 deletions
+60 -13
View File
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
@@ -6,13 +5,29 @@ import 'package:flutter/foundation.dart';
import '../../api/demo/demo_mode.dart';
import '../../api/errors/auth_exception.dart';
import '../../api/errors/error_mapper.dart';
import '../../api/marianumcloud/app_password/get_app_password.dart';
import '../../api/marianumconnect/auth/device_token_name.dart';
import '../../api/marianumconnect/auth/token_storage.dart';
import '../../api/marianumconnect/queries/auth_login/auth_login.dart';
import '../../api/marianumconnect/queries/auth_logout/auth_logout.dart';
import '../../model/account_data.dart';
import '../../push/push_registration.dart';
import '../../widget_data/widget_sync.dart';
/// Outcome of a login attempt.
enum LoginResult {
/// Fully logged in — the view transitions to `loggedIn`.
success,
/// Credentials rejected or a transport problem; the error is exposed via
/// [LoginController.errorMessage].
failure,
/// MarianumConnect accepted the credentials, but Nextcloud rejects them
/// (two-factor authentication active or diverging password). The view must
/// complete the Nextcloud Login Flow v2 in the browser before proceeding.
nextcloudLoginRequired,
}
/// Owns the login flow's transient state (loading, last error) so it can be
/// driven from a thin Stateful view and unit-tested without a widget tree.
class LoginController extends ChangeNotifier {
@@ -24,10 +39,8 @@ class LoginController extends ChangeNotifier {
String? get errorMessage => _errorMessage;
String? get errorDetails => _errorDetails;
/// Returns `true` when the credential probe succeeded. The view should
/// then transition the AccountBloc to `loggedIn`.
Future<bool> submit(String username, String password) async {
if (_loading) return false;
Future<LoginResult> submit(String username, String password) async {
if (_loading) return LoginResult.failure;
_loading = true;
_errorMessage = null;
_errorDetails = null;
@@ -45,7 +58,7 @@ class LoginController extends ChangeNotifier {
await AccountData().setDemo(user);
_loading = false;
notifyListeners();
return true;
return LoginResult.success;
}
try {
@@ -65,13 +78,13 @@ class LoginController extends ChangeNotifier {
tokenName: await DeviceTokenName.resolve(),
);
await AccountData().setData(user, password);
// Mint the Nextcloud app password now so it's ready for the push
// registration and subsequent NC calls. Non-blocking: on failure push
// stays off and retries on the next start.
unawaited(PushRegistration().ensureAppPassword());
// Mint the Nextcloud app password now it doubles as the Nextcloud
// credential probe: a rejection means 2FA is active (or the NC password
// diverges) and the login must finish interactively in the browser.
final ncReady = await _prepareNextcloudAppPassword();
_loading = false;
notifyListeners();
return true;
return ncReady ? LoginResult.success : LoginResult.nextcloudLoginRequired;
} catch (e) {
log(e.toString());
await AccountData().removeData();
@@ -83,7 +96,41 @@ class LoginController extends ChangeNotifier {
_errorDetails = errorToTechnicalDetails(e);
_loading = false;
notifyListeners();
return false;
return LoginResult.failure;
}
}
/// Tries to mint the Nextcloud app password with the just-verified password.
/// `false` = Nextcloud rejected the credentials → Login Flow v2 required.
/// Transport/server problems stay non-blocking (like the previous
/// fire-and-forget mint): the mint retries with the push registration.
Future<bool> _prepareNextcloudAppPassword() async {
try {
final appPassword = await GetAppPassword().run();
await AccountData().setAppPassword(appPassword);
return true;
} on AuthException {
return false;
} on Object catch (e) {
log('Nextcloud app password mint failed (non-blocking): $e');
return true;
}
}
/// Rolls the half-finished login back after the user cancelled the
/// Nextcloud browser login: revoke the fresh MarianumConnect token and wipe
/// the stored credentials, then surface why the login did not complete.
Future<void> abortNextcloudLogin() async {
try {
await AuthLogout().run();
} on Object catch (e) {
log('Login rollback: MC logout failed: $e');
}
await AccountData().removeData();
await const MarianumConnectTokenStorage().clear();
_errorMessage =
'Die Anmeldung wurde abgebrochen — dein Konto erfordert die Bestätigung im Browser.';
_errorDetails = null;
notifyListeners();
}
}