In this post we will look at how to get started around creating a Flutter mobile app including user authentication based on Amazon Cognito.

( Photo by Jason Chen )

In the last post I shared my initial impressions around experimenting with Flutter as a rapid way to implement cross platform (iOS and Android) apps. That was almost three weeks ago and since then I have been hunkered down in my bear cave, spending some time building a prototype (and not writing blog articles).

It is not quite ready for prime time yet, but I wanted to share some of the lessons I have learnt along the way so far.

Clean code with Flutter Redux

As I mentioned in my previous article, the initial code I put together from the tutorials was very unmaintanable. Experimenting more with Flutter Redux has really helped me in organising my code in a much better way and ensuring good separation of concerns. There is an higher learning curve at the beginning - and I would argue I am very much still on the ascending path on that curve 😅 ... but the initial pain is very quickly offset by a much more organised code structure.

This really helps with the understanding of what the code is doing and also facilitating the addition of new features. Even rewrites and refactoring become more manageable.It is definitely easier to update the implementation and follow better patterns - as one's own understanding improves over time.

Authentication with Amazon Cognito

One of the first thing I wanted to achieve was to include user authentication in my app using Amazon Cognito. This is the building block that will unlock using more advanced AWS services and most importantly AppSync. As I mentioned, there is already a Flutter package handling this.

My challenge here has been to understand it and then replicate the functionality available in the demo app , but using Flutter Redux to decouple the app logic from the actual authentication provider logic, and ensure I ended up with something I can build on top and extend.

Creating the User and Identity Pool on AWS has been quite simple. The Cognito console offers a guided procedure to configure the pool. Key aspects here are:

  • Note down the Pool Id generated
  • Decide which attributes will used. I have decided to start simple and use e-mail address as the sign-in identifier, and allow the user to pick a nickname as their alias in the app.
  • Allow users to sign up by themselves - as most apps will allow this, and set up a password policy (validation will need to be handled in the Flutter UI accordingly)
  • Disable Multi Factor (to keep things simple) and set up e-mail verification.
  • Leave advanced security off (it's a paid feature - usually not required for initial prototyping)
  • Turn off remembering user devices (same as above)
  • Create an app client and remember the generated app client ID

The trickiest part for me was to join this all up with the Redux part. The Cognito based authentication has been implemented as a middleware, and I was not sure where it would be best to handle the outcome of the authentication calls. After some research and after asking , it seems that handling this in middleware is not a bad idea after all.

Here is a snippet of what I ended up doing in the middleware to handle differnet login outcomes actions triggered by my screens:

final GlobalKey<NavigatorState> navigatorKey 

List<Middleware<AppState>> createAuthMiddleware() {

  // navKey is a static reference to GlobalKey<NavigatorState>();
  final GlobalKey<NavigatorState> navigatorKey = NavKeys.navKey;

  final logIn = _createLogInMiddleware(navigatorKey);

  return [
    TypedMiddleware<AppState, LogOutAction>(logOut),
  ];
}
    
Middleware<AppState> _createLogInMiddleware(
    GlobalKey<NavigatorState> navigatorKey) {
  return (Store store, action, NextDispatcher next) async {
   
    // A Dart singleton using 'amazon_cognito_identity_dart' to implement
    // Amazon Cognito authentication against my configured User Pool.
    CognitoAuthService authService = CognitoAuthService();

    if (action is LogInAction) {
      // The user object is used in my AppState
      User user =
          await authService.authenticateUser(action.email, action.password);
      if (user.authState == UserRemoteAuthMessageCode.login_successful) {
        // Will trigger a user state update in the reducer
        store.dispatch(LogInSuccessfulAction(user: user));
        
        // As I have a dedicated login screen, this will send the user back 
        // to the previous screen they were on
        navigatorKey.currentState.pop();
      } else if (user.authState ==
          UserRemoteAuthMessageCode.user_not_confirmed) {
        store.dispatch(LogInErrorAction(user));
    
        // In this case the user did not confirm registration, therefore 
        // the current view is replaced by the 'Registration Confirmation' screen
        navigatorKey.currentState
            .pushReplacementNamed(myApp.registrationConfirm);
      } else {
        // Something has gone wrong, the user object generated by the authentication
        // service will contain the details
        store.dispatch(LogInErrorAction(user));
      }
    }

    next(action);
  };
}

In my CognitoAuthService class I also handle all the configuration for the Cognito Pool Id and App Id needed to initialise the Cognito package. For this I used  a standard config json asset which is then made available via another package, global_configuration.

💡 To avoid security blunders and checking in secrets in Github, I have initialised my json with placeholder values, committed it, then ensured that git ignores any change going forward by using git update-index --assume-unchanged my_config.json (which is an OK approach for the time being). I don't think the Cognito Pool Id and the App Id are secret anyway, but better be safe rather than sorry.

Logging

Once the code becomes split across multiple files and involving remote calls to cloud services, you will want to have proper logging in addition to the standard debugging tools and the good old print statement. Again, there is a nifty package already for that too called (surprise surprise) logging.

It's very simple to initialise it in main.dart (at least in the naive PoC way)

Logger.root.level = Level.FINEST; 
Logger.root.onRecord.listen((record) {
    print(
        '${record.level.name}: ${record.time}: [${record.loggerName}]: ${record.message}');
  });

It allows to define the log format, the log level and the name of the logger so you can see exactly what is going on. You can also use redux_logging to figure out exactly what's going on with your action and state with very little effort.

Flutter Secure Storage

The Amazon Cognito Identity Dart package supports persistently stored user sessions. In the code sample, the author uses the shared_preferences package to persist the Cognito User session on the device (NSUserDefaults on iOS and SharedPreferences on Android.

I decided to try something different and used the flutter_secure_storage plugin instead (which uses the iOS Keychain and AES encryption with Keystore support on Android): here is the class if you are interested.

Photo by Sebastian Pena Lambarri

Conclusions

It's still early days but I have to say there is so much documentation around that it is really easy to get going. Having spent some time to get acquainted with Flutter (and Dart) and Redux, and having now an authenticated Cognito session in my app, the next step is to focus on the functionality I want to add - and for which authentication was the key enabling factor to achieve. It's going to be fun!

Oh, and of course, don't forget to tune in on the 10 of April 2019 (15:00 CEST) to learn what the guys at the Event Horizon Telescope (Proudly Made in 🇪🇺) project have managed to achieve, that's going to be even more fun I hope!