You are currently viewing Cognito in Flutter

Cognito in Flutter

Introduction

When we started working on a new Flutter application, we made a decision to minimize or completely avoid the use of AWS Amplify.

We’ve come to the agreement on using AWS Cognito to manage User Authentication since it provides a lot of things for us out of the box. This topic will cover integration with AWS Cognito without the use of AWS Amplify.

Integration

This integration is done in the following 4 steps:

  1. Creating a Cognito User pool
  2. Adding the configuration to the Flutter project
  3. Making use of the AWS SDK
  4. Testing

Creating a Cognito User pool

Cognito User pool creation requires the following of these 5 steps (currently), as well as the final review step at the end:

image

The first four steps are out of the scope of this topic, so finish them according to your own needs. Assuming these steps have been done, you should be at Step 5 – Integrate your app. More specifically, the following section:

image

Since this topic is about developing a Flutter mobile application, the Public client option is selected with “mobile” for the App client name (more info here). If no other configuration options provided by this page are relevant for you, you can skip the rest of it and continue to Step 6 – Review and create.

Adding the configuration to the Flutter project

This step assumes that the Cognito User pool has been successfully created. There are a couple of things needed from the AWS Management Console:

  • Cognito User pool ID:
cognito-user-pool-id
  • App client ID and the secret, which can be found under the App integration → App client list and selecting the “mobile” app:
cognito-app-client-id
  • Region in which the Cognito User pool has been created

These four values can now be put to use in the Flutter project. The configuration should be saved as a JSON variable, this example puts them in the cognito_configuration.dart file, under the cognitoConfig variable:

const cognitoConfig = ''' {
    "UserAgent": "aws-amplify-cli/2.0",
    "Version": "1.0",
    "auth": {
        "plugins": {
            "IdentityManager": {
                "Default": {}
            },
            "awsCognitoAuthPlugin": {
                "CognitoUserPool": {
                    "Default": {
                        "PoolId": "$POOL_ID",
                        "AppClientId": "$APP_CLIENT_ID",
                        "AppClientSecret": "$APP_CLIENT_SECRET",
                        "Region": "$REGION"
                    }
                },
                "Auth": {
                    "Default": {
                        "authenticationFlowType": "USER_SRP_AUTH"
                    }
                }
            }
        }
    }
}''';

The values copied from AWS Management Console will go under the PoolIdAppClientIdAppClientSecret, and Region fields in the JSON object.

Making use of the AWS SDK

Once the configuration is in the Flutter project, dependencies can be specified and actually configured with these values. The two dependencies needed are amplify_auth_cognito and amplify_flutter. Make sure that pub get has been run to actually get these dependencies.

In order to keep things testable, an abstract Authentication Service is added:

abstract class AuthenticationService {
  Future<bool> signIn(String email, String password);
  Future<void> signOut();
  Future<bool> isUserAuthenticated();
  Future<String> getAuthorization();
}

And an Amplify Service:

class AmplifyService {
  Future<void> addPlugin(AmplifyPluginInterface plugin) async =>
      await Amplify.addPlugin(plugin);

  Future<void> configure(String configuration) async =>
      await Amplify.configure(configuration);

  AuthCategory getAuth() => Amplify.Auth;
}

The actual Cognito implementation class starts with AmplifyService (for AWS configuration) and NavigationService (for navigation to and from the Login screen):

class CognitoAuthenticationService implements AuthenticationService {
  final AmplifyService amplifyService;
  final NavigationService navigationService;
...
}

The only way to construct the CognitoAuthenticationService is using the init static method, which makes sure the Amplify is configured properly. This is done by first adding the AmplifyAuthCognito plugin and only then calling the configure(cognitoConfig) (which has been defined in the previous step):

static Future<CognitoAuthenticationService> init(
      {AmplifyService? amplifyService,
      NavigationService? navigationService}) async {
    if (amplifyService == null) {
      amplifyService = AmplifyService();
      await amplifyService.addPlugin(AmplifyAuthCognito());
      await amplifyService.configure(cognitoConfig);
    }
    return CognitoAuthenticationService._(amplifyService,
        navigationService: navigationService);
  }

  CognitoAuthenticationService._(this.amplifyService,
      {NavigationService? navigationService})
      : navigationService = navigationService ?? locator<NavigationService>();

After this, the amplifyService.getAuth() method can be used for authentication methods:

amplifyService.getAuth().signIn(username: email, password: password);
amplifyService.getAuth().getCurrentUser();
amplifyService.getAuth().signOut();

Testing

For example, the login_screen_test.dart uses the AuthenticationService abstract class:

...
class MockAuthenticationService extends Mock implements AuthenticationService {}

void main() {
  NavigationService navigationService = MockNavigationService();
  AuthenticationService authenticationService = MockAuthenticationService();

  group('Login Screen', () {
    ...

    testWidgets('should enable login button when credentials are valid',
        (WidgetTester tester) async {
      ...
      when(() => authenticationService.signIn(aValidEmail, aValidPassword))
          .thenAnswer((_) async => true);
      await tester.pumpWidget(
        MaterialApp(
          home: LoginScreen(
            navigationService: navigationService,
            authenticationService: authenticationService,
          ),
        ),
      );
      await tester.pumpAndSettle();
      ...
      verify(() => authenticationService.signIn(aValidEmail, aValidPassword))
          .called(1);
    });
  });
}

And an example test case from cognito_authentication_service_test.dart:

...
class MockAmplifyService extends Mock implements AmplifyService {}

class MockAuthCategory extends Mock implements AuthCategory {}

class MockNavigationService extends Mock implements NavigationService {}

void main() async {
  AmplifyService amplifyService = MockAmplifyService();
  NavigationService navigationService = MockNavigationService();
  AuthCategory authCategory = MockAuthCategory();
  CognitoAuthenticationService cognitoAuthenticationService =
      await CognitoAuthenticationService.init(
          amplifyService: amplifyService, navigationService: navigationService);

  group('Cognito Authentication Service Tests', () {
    setUp(() {
      when(() => amplifyService.getAuth()).thenReturn(authCategory);
    });

    test('returns true if amplify auth sign in succeeds', () async {
      when(() => authCategory.signIn(
              username: aValidEmail, password: aValidPassword))
          .thenAnswer((_) async => SignInResult(isSignedIn: true));
      when(() => authCategory.getCurrentUser())
          .thenThrow(const AuthException(""));

      bool actual = await cognitoAuthenticationService.signIn(
          aValidEmail, aValidPassword);

      expect(actual, true);
      verify(() =>
          authCategory.signIn(username: aValidEmail, password: aValidPassword));
    });
    ...