diff --git a/CHANGELOG.md b/CHANGELOG.md index aa1691e..04c1413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -## 0.1.3 +## 0.1.4 (2021-08-15) + +- Add multi-factor authentication +- Fix third party provider support + +## 0.1.3 (2021-07-16) - Add PasswordChangePage - Fix code formatting diff --git a/README.md b/README.md index 7ce1a8b..6cff233 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Flutter plugin that automatically generates authentication widget templates based on your Amplify CLI Authentication configuration. -Preview +Demo ## Features @@ -16,6 +16,7 @@ Flutter plugin that automatically generates authentication widget templates base - Facebook - Google - Amazon +- Sign in with multi-factor authentication (SMS) ### SignUpPage - Sign up via password and one of the following (plus confirmation code): @@ -58,7 +59,7 @@ Flutter plugin that automatically generates authentication widget templates base To use this plugin, add `flutter_amplify_auth_ui` as a `dev_dependency` in your pubspec.yaml: ``` dev_dependencies: - flutter_amplify_auth_ui: ^0.1.3 + flutter_amplify_auth_ui: ^0.1.4 ``` Run `flutter pub get` to install the plugin. diff --git a/example/screenshots/Demo.gif b/example/screenshots/Demo.gif new file mode 100644 index 0000000..7dff295 Binary files /dev/null and b/example/screenshots/Demo.gif differ diff --git a/lib/src/auth_config/auth_config.dart b/lib/src/auth_config/auth_config.dart index 41dcbb3..247a0b2 100644 --- a/lib/src/auth_config/auth_config.dart +++ b/lib/src/auth_config/auth_config.dart @@ -2,13 +2,17 @@ class AuthConfig { final bool allowUnauthenticatedIdentities; final List requiredAttributes; final List usernameAttributes; - final List authProvidersUserPool; + final List authProviders; + final String mfaConfiguration; + final List mfaTypes; const AuthConfig({ required this.allowUnauthenticatedIdentities, required this.requiredAttributes, required this.usernameAttributes, - required this.authProvidersUserPool, + required this.authProviders, + required this.mfaConfiguration, + required this.mfaTypes, }); static AuthConfig fromJson(dynamic json) { @@ -16,7 +20,9 @@ class AuthConfig { allowUnauthenticatedIdentities: json['allowUnauthenticatedIdentities'], requiredAttributes: _getAsList(json, 'requiredAttributes'), usernameAttributes: _getAsList(json, 'usernameAttributes'), - authProvidersUserPool: _getAsList(json, 'authProvidersUserPool'), + authProviders: _getAsList(json, 'authProviders'), + mfaConfiguration: json['mfaConfiguration'], + mfaTypes: _getAsList(json, 'mfaTypes'), ); } @@ -30,7 +36,9 @@ class AuthConfig { "allowUnauthenticatedIdentities": $allowUnauthenticatedIdentities, "requiredAttributes": $requiredAttributes, "usernameAttributes": $usernameAttributes, - "authProvidersUserPool": $authProvidersUserPool, + "authProviders": $authProviders, + "mfaConfiguration": $mfaConfiguration, + "mfaTypes": $mfaTypes, '''; } } diff --git a/lib/src/flutter_amplify_auth_ui_generator.dart b/lib/src/flutter_amplify_auth_ui_generator.dart index ab3e2f0..768ac28 100644 --- a/lib/src/flutter_amplify_auth_ui_generator.dart +++ b/lib/src/flutter_amplify_auth_ui_generator.dart @@ -17,6 +17,12 @@ class FlutterAmplifyAuthUIGenerator { Directory(targetDir).createSync(); CommandLine.printMessage('Generating classes...'); + _generateClassFromTemplate( + targetDir: targetDir, + templateName: 'sign_in/confirmation_code_dialog.dart', + templateHandler: null, + authConfig: authConfig, + ); _generateClassFromTemplate( targetDir: targetDir, templateName: 'sign_in/sign_in_page.dart', diff --git a/lib/src/template_handlers/impl/sign_in_page_template_handler.dart b/lib/src/template_handlers/impl/sign_in_page_template_handler.dart index 36dde90..c033982 100644 --- a/lib/src/template_handlers/impl/sign_in_page_template_handler.dart +++ b/lib/src/template_handlers/impl/sign_in_page_template_handler.dart @@ -8,9 +8,9 @@ class SignInPageTemplateHandler extends TemplateHandler { 'phone_number', ]; static const List configurableAuthProviders = [ - 'Facebook', - 'Google', - 'LoginWithAmazon', + 'graph.facebook.com', + 'accounts.google.com', + 'www.amazon.com', ]; @override @@ -19,6 +19,7 @@ class SignInPageTemplateHandler extends TemplateHandler { _handleUsernameAttributes(template: template, authConfig: authConfig); _handleGuestSignIn(template: template, authConfig: authConfig); _handleSignInWithWebUI(template: template, authConfig: authConfig); + _handleMfa(template: template, authConfig: authConfig); } void _handleUsernameAttributes( @@ -47,12 +48,18 @@ class SignInPageTemplateHandler extends TemplateHandler { void _handleSignInWithWebUI( {required Template template, required AuthConfig authConfig}) { configurableAuthProviders.forEach((provider) { - if (!authConfig.authProvidersUserPool.contains(provider)) { + if (!authConfig.authProviders.contains(provider)) { template.remove(identifier: 'authProvidersUserPool[$provider]'); } }); - if (authConfig.authProvidersUserPool.isEmpty) { + if (authConfig.authProviders.isEmpty) { template.remove(identifier: 'authProvidersUserPool[any]'); } } + + void _handleMfa( + {required Template template, required AuthConfig authConfig}) { + var reverseFlag = authConfig.mfaConfiguration == 'ON' ? 'OFF' : 'ON'; + template.remove(identifier: 'mfaConfiguration[$reverseFlag]'); + } } diff --git a/lib/src/template_handlers/impl/sign_up_page_template_handler.dart b/lib/src/template_handlers/impl/sign_up_page_template_handler.dart index 193440e..f59579d 100644 --- a/lib/src/template_handlers/impl/sign_up_page_template_handler.dart +++ b/lib/src/template_handlers/impl/sign_up_page_template_handler.dart @@ -34,8 +34,13 @@ class SignUpPageTemplateHandler extends TemplateHandler { void _handleRequiredAttributes( {required Template template, required AuthConfig authConfig}) { + var requiredAttributes = authConfig.requiredAttributes; + if (authConfig.mfaConfiguration == 'ON' && + authConfig.mfaTypes.contains('SMS Text Message')) { + requiredAttributes.add('phone_number'); + } configurableRequiredAttributes.forEach((attribute) { - if (!authConfig.requiredAttributes.contains(attribute)) { + if (!requiredAttributes.contains(attribute)) { template.remove(identifier: 'requiredAttributes[$attribute]'); } }); diff --git a/lib/templates/sign_in/confirmation_code_dialog.dart b/lib/templates/sign_in/confirmation_code_dialog.dart new file mode 100644 index 0000000..2f7817b --- /dev/null +++ b/lib/templates/sign_in/confirmation_code_dialog.dart @@ -0,0 +1,75 @@ +import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; +import 'package:amplify_flutter/amplify.dart'; +import 'package:flutter/material.dart'; + +class ConfirmationCodeDialog extends StatefulWidget { + final void Function(BuildContext) onSignIn; + + const ConfirmationCodeDialog({Key? key, required this.onSignIn}) + : super(key: key); + + @override + _ConfirmationCodeDialogState createState() => _ConfirmationCodeDialogState(); +} + +class _ConfirmationCodeDialogState extends State { + final TextEditingController _confirmationCodeController = + TextEditingController(); + + @override + void dispose() { + _confirmationCodeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Confirmation Code'), + content: SingleChildScrollView( + child: Column( + children: [ + Text('Please enter the confirmation code we sent you.'), + TextFormField( + controller: _confirmationCodeController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + icon: Icon(Icons.check_outlined), + hintText: 'Enter your confirmation code', + labelText: 'Confirmation code'), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + _confirm(); + }, + child: const Text('Sign In'), + ), + ], + ); + } + + void _confirm() async { + try { + SignInResult result = await Amplify.Auth.confirmSignIn( + confirmationValue: _confirmationCodeController.text.trim(), + ); + if (result.isSignedIn) { + Navigator.of(context).pop(); + widget.onSignIn(context); + } + } on AuthException catch (e) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(e.message))); + } + } +} diff --git a/lib/templates/sign_in/sign_in_page.dart b/lib/templates/sign_in/sign_in_page.dart index 8293a2a..ecf5864 100644 --- a/lib/templates/sign_in/sign_in_page.dart +++ b/lib/templates/sign_in/sign_in_page.dart @@ -3,6 +3,7 @@ import 'package:amplify_flutter/amplify.dart'; import 'package:flutter/material.dart'; import '../password_management/password_reset_page.dart'; import '../sign_up/sign_up_page.dart'; +import 'confirmation_code_dialog.dart'; class SignInPage extends StatefulWidget { final void Function(BuildContext) onSignIn; @@ -138,7 +139,7 @@ class _SignInPageState extends State { ButtonBar( alignment: MainAxisAlignment.center, children: [ - /*+++START authProvidersUserPool[Facebook]+++*/ + /*+++START authProvidersUserPool[graph.facebook.com]+++*/ ElevatedButton( onPressed: () => _signInWithWebUI(provider: AuthProvider.facebook), @@ -149,8 +150,8 @@ class _SignInPageState extends State { ), child: Text('Sign in with Facebook'), ), - /*+++END authProvidersUserPool[Facebook]+++*/ - /*+++START authProvidersUserPool[Google]+++*/ + /*+++END authProvidersUserPool[graph.facebook.com]+++*/ + /*+++START authProvidersUserPool[accounts.google.com]+++*/ ElevatedButton( onPressed: () => _signInWithWebUI(provider: AuthProvider.google), @@ -163,8 +164,8 @@ class _SignInPageState extends State { ), child: Text('Sign in with Google'), ), - /*+++END authProvidersUserPool[Google]+++*/ - /*+++START authProvidersUserPool[LoginWithAmazon]+++*/ + /*+++END authProvidersUserPool[accounts.google.com]+++*/ + /*+++START authProvidersUserPool[www.amazon.com]+++*/ ElevatedButton( onPressed: () => _signInWithWebUI(provider: AuthProvider.amazon), @@ -175,7 +176,7 @@ class _SignInPageState extends State { ), child: Text('Sign in with Amazon'), ), - /*+++END authProvidersUserPool[LoginWithAmazon]+++*/ + /*+++END authProvidersUserPool[www.amazon.com]+++*/ ], ), ], @@ -189,7 +190,7 @@ class _SignInPageState extends State { void _signIn() async { try { await Amplify.Auth.signOut(); - SignInResult result = await Amplify.Auth.signIn( + await Amplify.Auth.signIn( /*+++START usernameAttributes[username]+++*/ username: _usernameController.text.trim(), /*+++END usernameAttributes[username]+++*/ @@ -201,9 +202,17 @@ class _SignInPageState extends State { +++END usernameAttributes[phone_number]+++*/ password: _passwordController.text.trim(), ); - if (result.isSignedIn) { - widget.onSignIn(context); - } + /*+++START mfaConfiguration[OFF]+++*/ + widget.onSignIn(context); + /*+++END mfaConfiguration[OFF]+++*/ + /*+++START mfaConfiguration[ON]+++*/ + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return ConfirmationCodeDialog(onSignIn: widget.onSignIn); + }); + /*+++END mfaConfiguration[ON]+++*/ } on AuthException catch (e) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(e.message))); diff --git a/lib/templates/sign_up/sign_up_page.dart b/lib/templates/sign_up/sign_up_page.dart index 2b309ff..3847011 100644 --- a/lib/templates/sign_up/sign_up_page.dart +++ b/lib/templates/sign_up/sign_up_page.dart @@ -586,9 +586,6 @@ class _SignUpPageState extends State { }), ), ); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('Successfully signed up'), - )); setState(() { _stepIndex = 1; }); diff --git a/pubspec.yaml b/pubspec.yaml index b530568..3fbb223 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_amplify_auth_ui description: Flutter plugin that automatically generates authentication widget templates based on your Amplify CLI Authentication configuration. -version: 0.1.3 +version: 0.1.4 homepage: https://github.com/finkmoritz/flutter_amplify_auth_ui repository: https://github.com/finkmoritz/flutter_amplify_auth_ui issue_tracker: https://github.com/finkmoritz/flutter_amplify_auth_ui/issues