Authentication for Flutter with AWS Amplify
Tags: flutter, aws, amplify, cognito
Published: 2020-12-31 22:05:10

In this article, I am going to explain how to add sign-in and signup process using AWS Cognito feature to your app using Amplify.

As you already know flutter is a cross-platform mobile development framework for both Android and iOS. Amplify is a set of AWS services gathered to build scalable full-stack applications. In the other hand, as Google has firebase, AWS has Amplify.

We will create a simple sign in and sign up application.

▶ Check out the [Youtube video](https://www.youtube.com/watch?v=B6HyL8HUAnA&t=17s)
👶 Initial Project [Github](https://github.com/enlearacademy/todo-Amplify-Auth/tree/Project-Init-Tut)
😎 Finished Project [Github](https://github.com/enlearacademy/todo-Amplify-Auth/tree/Amplify-auth-tutorial) 

To create the app you will need the following tools, 🔧

- [Flutter](https://flutter.dev/docs/get-started/install) version 1.20.0

- The latest version of [Amplify CLI](https://docs.amplify.aws/cli). You can install the latest Amplify CLI using the following command.

```bash
$ npm install -g @aws-amplify/cli@3.3.12
```
## What we are going to do

![Prototype created using Adobe XD](https://dev-to-uploads.s3.amazonaws.com/i/jzo8r0svfu3z0biow2wf.gif)

## 1. Create a Flutter Project

You can simply create a flutter project using the following command. In this case, I will use the project name as `amplify_login_app`.

```bash
$ flutter create todo_amplify
```
Then, to add amplify to the current project go project directory and open a new terminal. Then run the following command.

```bash
$ amplify init
```

![](https://dev-to-uploads.s3.amazonaws.com/i/ncuwhy9gal01jxsu4lqv.png)

Select the default options and continue.

After you initialized the Amplify, your project structure will be something like this.

![](https://dev-to-uploads.s3.amazonaws.com/i/81er3mddfe50188ec6h9.png)

As you can see there's an auto generated file `calledamplifyconfiguration.dart`. This file is generated during initializing process. Do not rename or modify this file we will need this later.

## Adding Authentication

Adding authentication to our app is pretty storage forward. Let's how it's done. First, run the following command in your current working directory.
```bash
$ amplify add auth
```
Note that we will not be able to edit these settings later

![](https://dev-to-uploads.s3.amazonaws.com/i/ermu9za1qvwq7jb0dm3q.png)

In our case, we select the sign-in method as Email. Depending on your requirement you can choose either username, phone number or email-based authentication.

Then we should run `amplify push` to deploy the changes to the cloud and to update `amplifyconfiguration.dart`.

Now let's do some coding stuff. We have to add AWS Cognito dependency to our project. We can do that by adding the dependencies `amplify_core` and `amplify_auth_cognito` to `pubspec.yaml`.

![](https://dev-to-uploads.s3.amazonaws.com/i/riyzq2js69yp1xeklb82.png)

- `amplify_core` is the core module for flutter amplify. You must always add this dependency if you use any amplify library.
- `amplify_auth_cognito` is used to add the amplify auth feature to your app.

Then you can download all the dependencies using `flutter pub get`.

If you are an android developer you have to update the `minSdkVersion` version to 21. Because `amplify_core` requires the minimum API level to be 21. To do that go to app-level `build.gradle` file.

![](https://dev-to-uploads.s3.amazonaws.com/i/lvflohgm23llr9upnjei.png)

Let's integrate Amplify into our app.

![](https://dev-to-uploads.s3.amazonaws.com/i/oxsoano8adp46zibn9ic.png)

As you can see in our `main.dart` file, MyApp is the root widget in our app. I am going to remove all the widgets and change it as follows.

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Amplify TODO',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: AmplifyTODO(),
    ),
  );
}

class AmplifyTODO extends StatefulWidget {
  AmplifyTODO({Key key}) : super(key: key);

  @override
  _AmplifyTODOState createState() => _AmplifyTODOState();
}

class _AmplifyTODOState extends State<AmplifyTODO> {
  @override
  Widget build(BuildContext context) {
    return Scaffold();
  }
}
```

To initialize amplify we should run the following method inside `initState` method.

```dart
import 'package:amplify_core/amplify_core.dart';
import 'package:amplify_todo/amplifyconfiguration.dart';
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';


class _AmplifyTODOState extends State<AmplifyTODO> {
  bool _amplifyConfigured = false;
  Amplify _amplifyInstance = Amplify();

  Future<void> _configureAmplify() async {
    try {
      AmplifyAuthCognito auth = AmplifyAuthCognito();
      _amplifyInstance.addPlugin(
        authPlugins: [auth],
      );
      await _amplifyInstance.configure(amplifyconfig);

      setState(() => _amplifyConfigured = true);
    } catch (e) {
      print(e);
      setState(() => _amplifyConfigured = false);
    }
  }

  @override
  void initState() {
    super.initState();
    _configureAmplify();
  }
...
```
First, we create an instance of the `AmplifyAuthCognito` and then we add it `amplify` instance. Remember the auto-generated file during the `amplify init`? In the final step, we add our `amplifyconfig` value to `_amplifyInstance`. If this process is successfully completed we set `_amplifyConfigured` to true. If some error occurs we will set to false.
Now let’s run the app to see everything is working fine.
⚠ Note: If you do a hot restart, you may get the following exception.

```
PlatformException(AmplifyException, User-Agent was already configured successfully., {cause: null, recoverySuggestion: User-Agent is configured internally during Amplify configuration. This method should not be called externally.}, null)
```
It’s because we have already configured Amplify and the library does not detect the hot restart. In the future releases, they will fix this bug.

It’s time to create the login and sign up screens. I am going to create the screens inside the screen folder as follows.

![](https://dev-to-uploads.s3.amazonaws.com/i/attoxesgwblkhansln1d.png)

- The file `email_validator.dart` contains a function that validates the string is an email or not.

Let’s implement the `signup_screen.dart` first as follows.

```dart
import 'package:todo_amplify/screens/email_confirmation_screen.dart';
import 'package:todo_amplify/util/email_validator.dart';
import 'package:flutter/material.dart';

class SignUpScreen extends StatelessWidget {
  SignUpScreen({Key key}) : super(key: key);

  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  final _formKey = GlobalKey<FormState>();
  final _scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: Text("Sign up"),
      ),
      body: Form(
        key: _formKey,
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              TextFormField(
                keyboardType: TextInputType.emailAddress,
                decoration: InputDecoration(labelText: "Email"),
                controller: _emailController,
                validator: (value) =>
                    !validateEmail(value) ? "Email is Invalid" : null,
              ),
              TextFormField(
                keyboardType: TextInputType.visiblePassword,
                decoration: InputDecoration(labelText: "Password"),
                obscureText: true,
                controller: _passwordController,
                validator: (value) => value.isEmpty
                    ? "Password is invalid"
                    : value.length < 9
                        ? "Password must contain at least 8 characters"
                        : null,
              ),
              SizedBox(
                height: 20,
              ),
              RaisedButton(
                  child: Text("CREATE ACCOUNT"),
                  onPressed: () => _createAccountOnPressed(context),
                  color: Theme.of(context).primaryColor,
                  colorBrightness: Theme.of(context).primaryColorBrightness),
            ],
          ),
        ),
      ),
    );
  }

  Future<void> _createAccountOnPressed(BuildContext context) async {
    if (_formKey.currentState.validate()) {
      final email = _emailController.text;
      final password = _passwordController.text;
      
      // TODO: Implment sign-up process

    }
  }

  void _gotToEmailConfirmationScreen(BuildContext context, String email) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (_) => EmailConfirmationScreen(email: email),
      ),
    ); 
  }
}
```
As you can see when the user press the sign up button `_createAccountOnPressed` will be executed. Let’s implement the method as follows.

```dart
Future<void> _createAccountOnPressed(BuildContext context) async {

    if (_formKey.currentState.validate()) {
      final email = _emailController.text.trim();
      final password = _passwordController.text;

    /// In this user attribute, you can define the custom fields associated with the user.
    /// For example birthday, telephone number, etc
      Map<String, dynamic> userAttributes = {
        "email": email,
      };

      try {
        final result = await Amplify.Auth.signUp(
          username: email,
          password: password,
          options: CognitoSignUpOptions(userAttributes: userAttributes),
        );
        if (result.isSignUpComplete) {
          _gotToEmailConfirmationScreen(context, email);
        }
      } on AuthError catch (e) {
        _scaffoldKey.currentState.showSnackBar(
          SnackBar(
            content: Text(e.cause),
          ),
        );
      }
    }
  }
```
1. First we get the email and password from the controllers.
2. Since we are using email and password-based authentication we have to create a map called `userAttributes` to store the email. This map can be used to store user-related data such as phone number, birthday, etc along with the user’s credentials.
3. Then we execute the method `Amplify.Auth.signUp` to sign up the user to the AWS Cognito user pool. We use email as the username. But don’t forget to add `userAttributes` to this method.
4. If some error occurred, for now, we show it using a [SnackBar](https://flutter.dev/docs/cookbook/design/snackbars).
5. If the user successfully signs up AWS will send an email confirmation code to the given email. So we should redirect the user to a new screen called `email_confimation_screen`.

Let’s implement `email_confimation_screen.dart`

```dart
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:todo_amplify/screens/main_screen.dart';
import 'package:flutter/material.dart';

class EmailConfirmationScreen extends StatelessWidget {
  final String email;

  EmailConfirmationScreen({
    Key key,
    @required this.email,
  }) : super(key: key);

  final _scaffoldKey = GlobalKey<ScaffoldState>();

  final TextEditingController _confirmationCodeController =
      TextEditingController();

  final _formKey = GlobalKey<FormFieldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: Text("Confirm your email"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Form(
          child: Column(
            children: [
              Text(
                "An email confirmation code is sent to $email. Please type the code to confirm your email.",
                style: Theme.of(context).textTheme.headline6,
              ),
              TextFormField(
                keyboardType: TextInputType.number,
                controller: _confirmationCodeController,
                decoration: InputDecoration(labelText: "Confirmation Code"),
                validator: (value) => value.length != 6
                    ? "The confirmation code is invalid"
                    : null,
              ),
              RaisedButton(
                onPressed: () => _submitCode(context),
                child: Text("CONFIRM"),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future<void> _submitCode(BuildContext context) async {
    if(_formKey.currentState.validate()){
      final confirmationCode = _confirmationCodeController.text;
      // TODO: Submit the code to Amplify
    }
  }

  void _gotoMainScreen(BuildContext context) {
    Navigator.push(context, MaterialPageRoute(builder: (_) => MainScreen()));
  }
}
```

A confirmation code is 6 digit code. So after validating the code we can send the code to the Amplify as follows.

```dart
  Future<void> _submitCode(BuildContext context) async {
    if (_formKey.currentState.validate()) {
      final confirmationCode = _confirmationCodeController.text;
      try {
        final SignUpResult response = await Amplify.Auth.confirmSignUp(
          username: email,
          confirmationCode: confirmationCode,
        );
        if (response.isSignUpComplete) {
          _gotoMainScreen(context);
        }
      } on AuthError catch (e) {
        _scaffoldKey.currentState.showSnackBar(
          SnackBar(
            content: Text(e.cause),
          ),
        );
      }
    }
  }
```
We can confirm the email using `Amplify.Auth.confirmSignUp`. If the user email confirmation process is successfully complemented, we can redirect the user to the main screen.
Now let’s implement `login_screen.dart`.

```dart
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:todo_amplify/screens/main_screen.dart';
import 'package:todo_amplify/screens/signup_screen.dart';
import 'package:todo_amplify/util/email_validator.dart';
import 'package:flutter/material.dart';

class LoginScreen extends StatelessWidget {
  LoginScreen({Key key}) : super(key: key);

  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  final _formKey = GlobalKey<FormState>();

  final _scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      body: Form(
        key: _formKey,
        child: Padding(
          padding: const EdgeInsets.all(50),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              TextFormField(
                keyboardType: TextInputType.emailAddress,
                decoration: InputDecoration(labelText: "Email"),
                controller: _emailController,
                validator: (value) =>
                    !validateEmail(value) ? "Email is Invalid" : null,
              ),
              TextFormField(
                keyboardType: TextInputType.visiblePassword,
                decoration: InputDecoration(labelText: "Password"),
                obscureText: true,
                controller: _passwordController,
                validator: (value) =>
                    value.isEmpty ? "Password is invalid" : null,
              ),
              SizedBox(
                height: 20,
              ),
              RaisedButton(
                child: Text("LOG IN"),
                onPressed: () => _loginButtonOnPressed(context),
                color: Theme.of(context).primaryColor,
              ),
              OutlineButton(
                child: Text("Create New Account"),
                onPressed: () => _gotoSignUpScreen(context),
                color: Theme.of(context).primaryColor,
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future<void> _loginButtonOnPressed(BuildContext context) async {
    if (_formKey.currentState.validate()) {
      //TODO: Login code
    }
  }

  void _gotoSignUpScreen(BuildContext context) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (_) => SignUpScreen(),
      ),
    );
  }
}
```
When the user presses the login button `_loginButtonOnPressed` will be executed.

```dart
  Future<void> _loginButtonOnPressed(BuildContext context) async {
    if (_formKey.currentState.validate()) {
      /// Login code
      try {
        final email = _emailController.text.trim();
        final password = _passwordController.text;

        final response = await Amplify.Auth.signIn(
          username: email,
          password: password,
        );

        if (response.isSignedIn) {
          Navigator.pushReplacement(
            context,
            MaterialPageRoute(
              builder: (_) => MainScreen(),
            ),
          );
        }
      } on AuthError catch (e) {
        _scaffoldKey.currentState.showSnackBar(
          SnackBar(
            content: Text(e.cause),
          ),
        );
      }
    }
  }
```
We can perform a login request using `Amplify.Auth.signIn` method.
{% youtube B6HyL8HUAnA %}