Enable 2fa within 2fa :okaypepe:

This commit is contained in:
vishnukvmd 2022-11-03 15:59:00 +05:30
parent 62c21749b5
commit ebf634ef1e
3 changed files with 468 additions and 4 deletions

View file

@ -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();
}
}
}

View 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);
}
}

View 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),
),
),
),
),
),
],
),
);
}
}