added support for 2fa login with browser flow
This commit is contained in:
@@ -17,6 +17,10 @@ class AccountData {
|
||||
// token, so two registrations need two app passwords.
|
||||
static const _appPasswordField = 'nextcloud_app_password';
|
||||
static const _appPasswordTalkField = 'nextcloud_app_password_talk';
|
||||
// Marks accounts whose Nextcloud credentials came from Login Flow v2 (2FA):
|
||||
// the real password is not valid against Nextcloud, only the flow-issued
|
||||
// app password is — and no further app passwords can be minted silently.
|
||||
static const _loginFlowField = 'nextcloud_login_flow';
|
||||
// Persists the demo session across cold starts (see DemoMode).
|
||||
static const _demoField = 'is_demo';
|
||||
// Keeps isPopulated()/getPassword() valid; demo mode never uses a real one.
|
||||
@@ -38,10 +42,17 @@ class AccountData {
|
||||
String? _appPassword;
|
||||
String? _appPasswordTalk;
|
||||
bool _isDemo = false;
|
||||
bool _usesLoginFlow = false;
|
||||
|
||||
/// True while the active session is a local demo session (see DemoMode).
|
||||
bool get isDemo => _isDemo;
|
||||
|
||||
/// True when the Nextcloud credentials were obtained via Login Flow v2
|
||||
/// (browser login, e.g. because the account has two-factor authentication).
|
||||
/// In that mode the stored real password only authenticates MarianumConnect;
|
||||
/// every Nextcloud call must use the flow-issued app password.
|
||||
bool get usesLoginFlow => _usesLoginFlow;
|
||||
|
||||
String getUsername() {
|
||||
if (_username == null) throw Exception('Username not initialized');
|
||||
return _username!;
|
||||
@@ -86,9 +97,11 @@ class AccountData {
|
||||
_appPassword = null;
|
||||
_appPasswordTalk = null;
|
||||
_isDemo = false;
|
||||
_usesLoginFlow = false;
|
||||
await _secureStorage.delete(key: _usernameField);
|
||||
await _secureStorage.delete(key: _passwordField);
|
||||
await _secureStorage.delete(key: _demoField);
|
||||
await _secureStorage.delete(key: _loginFlowField);
|
||||
await _clearAppPasswordStorage();
|
||||
await _clearAppPasswordTalkStorage();
|
||||
}
|
||||
@@ -111,6 +124,17 @@ class AccountData {
|
||||
await _clearAppPasswordStorage();
|
||||
}
|
||||
|
||||
/// Adopts an app password obtained via Login Flow v2 and switches the
|
||||
/// account into flow mode (see [usesLoginFlow]). Any previously stored Talk
|
||||
/// app password belonged to the old session era and is dropped — the second
|
||||
/// (optional) flow pass stores a fresh one via [setAppPasswordTalk].
|
||||
Future<void> setLoginFlow(String appPassword) async {
|
||||
await setAppPassword(appPassword);
|
||||
await clearAppPasswordTalk();
|
||||
_usesLoginFlow = true;
|
||||
await _secureStorage.write(key: _loginFlowField, value: 'true');
|
||||
}
|
||||
|
||||
bool hasAppPassword() => _appPassword != null && _appPassword!.isNotEmpty;
|
||||
|
||||
/// Persists the app password backing the Talk push registration.
|
||||
@@ -156,6 +180,7 @@ class AccountData {
|
||||
_username = await _secureStorage.read(key: _usernameField);
|
||||
_password = await _secureStorage.read(key: _passwordField);
|
||||
_isDemo = (await _secureStorage.read(key: _demoField)) == 'true';
|
||||
_usesLoginFlow = (await _secureStorage.read(key: _loginFlowField)) == 'true';
|
||||
try {
|
||||
_appPassword = await pushSecureStorage.read(key: _appPasswordField);
|
||||
_appPasswordTalk = await pushSecureStorage.read(
|
||||
@@ -209,6 +234,10 @@ class AccountData {
|
||||
String getTalkBasicAuthHeader() {
|
||||
_requirePopulated();
|
||||
if (!hasAppPasswordTalk()) {
|
||||
// Login-flow account whose second (talk) flow pass was skipped: no
|
||||
// silent minting possible, the talk registration shares the single
|
||||
// flow-issued credential.
|
||||
if (_usesLoginFlow && hasAppPassword()) return _basicAuth(_appPassword!);
|
||||
throw StateError('Talk app password not available yet');
|
||||
}
|
||||
return _basicAuth(_appPasswordTalk!);
|
||||
@@ -222,6 +251,15 @@ class AccountData {
|
||||
return _basicAuth(_password!);
|
||||
}
|
||||
|
||||
/// Secret authenticating against Nextcloud: the app password once available
|
||||
/// (minted or flow-issued), otherwise the real password. Mirrors the
|
||||
/// preference of [getBasicAuthHeader] for clients that need the raw secret
|
||||
/// (WebDAV client construction).
|
||||
String getNextcloudSecret() {
|
||||
_requirePopulated();
|
||||
return _appPassword ?? _password!;
|
||||
}
|
||||
|
||||
void _requirePopulated() {
|
||||
if (!isPopulated()) {
|
||||
throw Exception(
|
||||
|
||||
Reference in New Issue
Block a user