Authentication for Flutter with AWS Amplify

pandukanandara

Panduka Nandara

Posted on December 31, 2020

Authentication for Flutter with AWS Amplify

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
👶 Initial Project Github
😎 Finished Project Github

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

  • Flutter version 1.20.0

  • The latest version of Amplify CLI. You can install the latest Amplify CLI using the following command.

$ npm install -g @aws-amplify/cli@3.3.12
Enter fullscreen mode Exit fullscreen mode

What we are going to do

Prototype created using Adobe XD

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.

$ flutter create todo_amplify
Enter fullscreen mode Exit fullscreen mode

Then, to add amplify to the current project go project directory and open a new terminal. Then run the following command.

$ amplify init
Enter fullscreen mode Exit fullscreen mode

Select the default options and continue.

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

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.

$ amplify add auth
Enter fullscreen mode Exit fullscreen mode

Note that we will not be able to edit these settings later

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.

  • 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.

Let's integrate Amplify into our app.

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.

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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();
  }
...
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

  • 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.

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),
      ),
    ); 
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see when the user press the sign up button _createAccountOnPressed will be executed. Let’s implement the method as follows.

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),
          ),
        );
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode
  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.
  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

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()));
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  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),
          ),
        );
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

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.

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(),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

When the user presses the login button _loginButtonOnPressed will be executed.

  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),
          ),
        );
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

We can perform a login request using Amplify.Auth.signIn method.

💖 💪 🙅 🚩
pandukanandara
Panduka Nandara

Posted on December 31, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related