Enable 2fa within 2fa :okaypepe:
This commit is contained in:
parent
62c21749b5
commit
ebf634ef1e
3 changed files with 468 additions and 4 deletions
|
@ -12,12 +12,17 @@ import 'package:ente_auth/models/sessions.dart';
|
|||
import 'package:ente_auth/models/set_keys_request.dart';
|
||||
import 'package:ente_auth/models/set_recovery_key_request.dart';
|
||||
import 'package:ente_auth/models/user_details.dart';
|
||||
import 'package:ente_auth/ui/account/login_page.dart';
|
||||
import 'package:ente_auth/ui/account/ott_verification_page.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
import 'package:ente_auth/ui/account/password_reentry_page.dart';
|
||||
import 'package:ente_auth/ui/two_factor_authentication_page.dart';
|
||||
import 'package:ente_auth/ui/two_factor_recovery_page.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class UserService {
|
||||
|
@ -265,11 +270,16 @@ class UserService {
|
|||
await dialog.hide();
|
||||
if (response != null && response.statusCode == 200) {
|
||||
Widget page;
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
page = const PasswordReentryPage();
|
||||
final String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
if (twoFASessionID != null && twoFASessionID.isNotEmpty) {
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
} else {
|
||||
page = const PasswordEntryPage();
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
page = const PasswordReentryPage();
|
||||
} else {
|
||||
page = const PasswordEntryPage();
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
|
@ -484,4 +494,196 @@ class UserService {
|
|||
await Configuration.instance.setToken(response.data["token"]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_config.getHttpEndpoint() + "/users/two-factor/recover",
|
||||
queryParameters: {
|
||||
"sessionID": sessionID,
|
||||
},
|
||||
);
|
||||
if (response != null && response.statusCode == 200) {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return TwoFactorRecoveryPage(
|
||||
sessionID,
|
||||
response.data["encryptedSecret"],
|
||||
response.data["secretDecryptionNonce"],
|
||||
);
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
} finally {
|
||||
await dialog.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyTwoFactor(
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String code,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Authenticating...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/two-factor/verify",
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"code": code,
|
||||
},
|
||||
);
|
||||
await dialog.hide();
|
||||
if (response != null && response.statusCode == 200) {
|
||||
showToast(context, "Authentication successful!");
|
||||
await _saveConfiguration(response);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect code",
|
||||
"Authentication failed, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Authentication failed, please try again",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeTwoFactor(
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String recoveryKey,
|
||||
String encryptedSecret,
|
||||
String secretDecryptionNonce,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
String secret;
|
||||
try {
|
||||
secret = Sodium.bin2base64(
|
||||
await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(encryptedSecret),
|
||||
Sodium.hex2bin(recoveryKey.trim()),
|
||||
Sodium.base642bin(secretDecryptionNonce),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect recovery key",
|
||||
"The recovery key you entered is incorrect",
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/two-factor/remove",
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"secret": secret,
|
||||
},
|
||||
);
|
||||
if (response != null && response.statusCode == 200) {
|
||||
showShortToast(context, "Two-factor authentication successfully reset");
|
||||
await _saveConfiguration(response);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
} finally {
|
||||
await dialog.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
150
lib/ui/two_factor_authentication_page.dart
Normal file
150
lib/ui/two_factor_authentication_page.dart
Normal file
|
@ -0,0 +1,150 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/lifecycle_event_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pinput/pin_put/pin_put.dart';
|
||||
|
||||
class TwoFactorAuthenticationPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
|
||||
const TwoFactorAuthenticationPage(this.sessionID, {Key key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<TwoFactorAuthenticationPage> createState() =>
|
||||
_TwoFactorAuthenticationPageState();
|
||||
}
|
||||
|
||||
class _TwoFactorAuthenticationPageState
|
||||
extends State<TwoFactorAuthenticationPage> {
|
||||
final _pinController = TextEditingController();
|
||||
final _pinPutDecoration = BoxDecoration(
|
||||
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
);
|
||||
String _code = "";
|
||||
LifecycleEventHandler _lifecycleEventHandler;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_lifecycleEventHandler = LifecycleEventHandler(
|
||||
resumeCallBack: () async {
|
||||
if (mounted) {
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null && data.text.length == 6) {
|
||||
_pinController.text = data.text;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(_lifecycleEventHandler);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(_lifecycleEventHandler);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"Two-factor authentication",
|
||||
),
|
||||
),
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Text(
|
||||
"Enter the 6-digit code from\nyour authenticator app",
|
||||
style: TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(32)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
|
||||
child: PinPut(
|
||||
fieldsCount: 6,
|
||||
onSubmit: (String code) {
|
||||
_verifyTwoFactorCode(code);
|
||||
},
|
||||
onChanged: (String pin) {
|
||||
setState(() {
|
||||
_code = pin;
|
||||
});
|
||||
},
|
||||
controller: _pinController,
|
||||
submittedFieldDecoration: _pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
selectedFieldDecoration: _pinPutDecoration,
|
||||
followingFieldDecoration: _pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(45, 194, 98, 0.5),
|
||||
),
|
||||
),
|
||||
inputDecoration: const InputDecoration(
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(24)),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
child: OutlinedButton(
|
||||
onPressed: _code.length == 6
|
||||
? () async {
|
||||
_verifyTwoFactorCode(_code);
|
||||
}
|
||||
: null,
|
||||
child: const Text("Verify"),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(30)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
UserService.instance.recoverTwoFactor(context, widget.sessionID);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"Lost device?",
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _verifyTwoFactorCode(String code) async {
|
||||
await UserService.instance.verifyTwoFactor(context, widget.sessionID, code);
|
||||
}
|
||||
}
|
112
lib/ui/two_factor_recovery_page.dart
Normal file
112
lib/ui/two_factor_recovery_page.dart
Normal file
|
@ -0,0 +1,112 @@
|
|||
// @dart=2.9
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TwoFactorRecoveryPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
final String encryptedSecret;
|
||||
final String secretDecryptionNonce;
|
||||
|
||||
const TwoFactorRecoveryPage(
|
||||
this.sessionID,
|
||||
this.encryptedSecret,
|
||||
this.secretDecryptionNonce, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TwoFactorRecoveryPage> createState() => _TwoFactorRecoveryPageState();
|
||||
}
|
||||
|
||||
class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
|
||||
final _recoveryKey = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"Recover account",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Enter your recovery key",
|
||||
contentPadding: EdgeInsets.all(20),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
controller: _recoveryKey,
|
||||
autofocus: false,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(24)),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
child: OutlinedButton(
|
||||
onPressed: _recoveryKey.text.isNotEmpty
|
||||
? () async {
|
||||
await UserService.instance.removeTwoFactor(
|
||||
context,
|
||||
widget.sessionID,
|
||||
_recoveryKey.text,
|
||||
widget.encryptedSecret,
|
||||
widget.secretDecryptionNonce,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text("Recover"),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Contact support",
|
||||
"Please drop an email to support@ente.io from your registered email address",
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"No recovery key?",
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue