L'authentification AWS sur Android

I. Introduction

AWS gagne du terrain sur les applications mobiles. Quoi de plus logique que de commencer par le sujet de l’authentification ?
Tout au long de cet article, nous allons voir comment mettre en place un projet, le configurer correctement pour utiliser les outils Amazon (Cognito, APIs d’authentification) et enfin, personnaliser notre page d’authentification.

II. Les pré-requis

J’utiliserai le langage Kotlin, et vous conseille Android Studio comme IDE. Un émulateur sous Android 6.0 (API 23 ou supérieur) se révélera également très utile.

Côté AWS, il est nécessaire d’avoir une instance Cognito fonctionnelle avec au moins un compte actif. Si cette mise en place n’est pas couverte par cet article, sachez cependant qu’elle peut être réalisée de deux manières :

  • via l’interface Amazon
  • grâce au projet AWS Amplify en ligne de commande au sein du projet Android.

III. Mise en place du projet

Les étapes suivantes permettent la création et la configuration du projet dans votre IDE.

  1. Créer votre projet Android, avec une version cible minimale Android 6 (Level API 23)

  2. Configuration : ajouter les imports AWS
  • Dans le fichier build.gradle du projet :
classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.10.1'
  • Dans le fichier build.gradle de l’application :
apply plugin: 'com.amazonaws.appsync'

dependencies {
    // ... typical dependencies

    implementation 'com.amazonaws:aws-android-sdk-appsync:3.0.1'
    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.2'
    implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
}

Le apply plugin est à placer tout en haut avec les autres.

  • Dans le fichier AndroidManifest.xml, ajouter les permissions et le service :
<uses-permission android:name="android.permission.INTERNET" />
    <!--other code-->

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <service android:name="org.eclipse.paho.android.service.MqttService" />

        <!--other code-->
    </application>

  1. Vérifier que le projet build et est exécuté sans erreur.

  2. Ajouter la brique d’authentification
  • Ajouter les imports pour le SDK dans le fichier build.gradle de l’application :
// Mobile Client for initializing the SDK   
implementation('com.amazonaws:aws-android-sdk-mobile-client:2.16.10') 
    { transitive = true }     
// Cognito UserPools for SignIn    
implementation('com.amazonaws:aws-android-sdk-auth-userpools:2.16.10') 
    { transitive = true }     
// Sign in UI Library    
implementation('com.amazonaws:aws-android-sdk-auth-ui:2.16.10') 
    { transitive = true }
  • Créer une nouvelle Activity : New > Activity > Empty Activity et la nommer AuthenticationActivity.
    Veillez à bien sélectionner la case Launcher Activity, car cela permet de signaler que cette page est maintenant celle de lancement.

Le code de la classe :

class AuthenticationActivity : AppCompatActivity() {
   private val TAG = AuthenticationActivity::class.java.simpleName

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_authentication)

       initializeAws()
   }

   private fun initializeAws() {
       AWSMobileClient.getInstance()
           .initialize(applicationContext, object : Callback<UserStateDetails> {
               override fun onResult(userStateDetails: UserStateDetails) {
                   Log.i(TAG, userStateDetails.userState.toString())

                   when (userStateDetails.userState) {
                       UserState.SIGNED_IN -> signedIn()
                       UserState.SIGNED_OUT -> showSignIn()
                       else -> signOutAndRetry()
                   }
               }

               override fun onError(e: Exception) {
                   Log.e(TAG, e.toString())
               }
           })
   }

   private fun signedIn() {
       val i = Intent(this@AuthenticationActivity, MainActivity::class.java)
       startActivity(i)
   }

   private fun signOutAndRetry() {
       AWSMobileClient.getInstance().signOut()
       showSignIn()
   }

   private fun showSignIn() {
       try {
           AWSMobileClient.getInstance().showSignIn(
              this, 
              SignInUIOptions.builder().nextActivity(MainActivity::class.java).build()
           )
       } catch (e: Exception) {
           Log.e(TAG, e.toString())
       }
   }
}

J’attire votre attention sur la méthode onCreate, dans laquelle la routine initialize de AWSMobileClient est appelée. Cela va permettre de vérifier le statut avant de proposer une connexion. Ainsi, si l’utilisateur est déjà connecté au service (UserState.SIGNEDIN), il ne sera donc pas redirigé sur la page d'authentification.

L’affichage de l’UI d’Amazon est généré par :

AWSMobileClient.getInstance().showSignIn(
    this,
    SignInUIOptions.builder().nextActivity(MainActivity.class).build()
);
  • Modifier le AndroidManifest.xml, pour que seule AuthenticationActivity soit la page de lancement. Supprimer le < intent-filter> et android:theme du MainActivity :
<activity
    android:name=".AuthenticationActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
  • Modifier activitymain.xml et MainActivity.kt

Vérifier que la ligne suivante est présente dans activitymain.xml (si votre projet a été généré via Android Studio, la ligne devrait déjà être présente), sinon l’ajouter :

<androidx.coordinatorlayout.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- other code -->

    <include layout="@layout/content_main" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Vérifier onCreate dans MainActivity.kt, afin que setSupportActionBar(toolbar) ne soit plus présent comme ce n’est plus la page de lancement. Supprimer également son composant dans le fichier activitymain.xml

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   
   // … other code
}
  • Ajouter le fichier awsconfiguration.json dans res > raw
    Ce fichier est très important car il fait le lien avec votre interface AWS. Vous trouverez les informations nécessaires pour le compléter dans le service Cognito de la console AWS.
{
 "UserAgent": "aws-amplify-cli/2.0",
 "Version": "0.1.0",
 "IdentityManager": {
   "Default": {}
 },
 "CredentialsProvider": {
   "CognitoIdentity": {
     "Default": {
       "PoolId": "eu-west-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
       "Region": "eu-west-1"
     }
   }
 },
 "CognitoUserPool": {
   "Default": {
     "AppClientId": "myAppClientId",
     "PoolId": "eu-west-1_xxxxxxxxx",
     "Region": "eu-west-1"
   }
 }
}
  1. C’est terminé ! Votre application doit s’ouvrir sur la page d'authentification AWS et être fonctionnelle.

Capture-d-e-cran-2020-06-29-a--17.37.08

Outre cette page de connexion fournie par AWS, on remarquera que les fonctions d’inscription et de mot de passe oublié sont présentes par défaut.

Cette page est fournie par la méthode showSignIn() venant des librairies du SDK Android d’AWS que l’on a importé un peu plus haut. Il est possible de customiser quelques éléments de cet écran.

Le code complet est disponible par ici : https://github.com/alebret/android-aws-authentication/tree/authentication

IV. Pour aller plus loin … une page d'authentification à son image

AWS fournit un écran pour l’authentification, c’est pratique et rapide, et nous allons voir qu’il est également très rapide de créer la sienne.

  1. Comme pour la partie précédente (III), je considère qu’Amazon Cognito user pool est déjà en place et configuré via l’interface Amazon.
    Vous devez donc ajouter le fichier awsconfiguration.json dans res > raw

  2. La brique d’authentification

  • Ajouter les imports pour le SDK dans le fichier build.gradle de l’application
def aws_sdk_version = "2.16.10"
//For AWSMobileClient only:
implementation "com.amazonaws:aws-android-sdk-mobile-client:$aws_sdk_version"
//For the drop-in UI also:
implementation "com.amazonaws:aws-android-sdk-auth-userpools:$aws_sdk_version"
implementation "com.amazonaws:aws-android-sdk-auth-ui:$aws_sdk_version"
//For hosted UI also:
implementation "com.amazonaws:aws-android-sdk-cognitoauth:$aws_sdk_version"
  • Créer une nouvelle Activity et la nommer AuthenticationActivity, de la même manière que précédemment, mais laisser le code de la classe tel qu’il a été généré.

  • Ajouter les composants à l’écran d’authentification (login, mot de passe, bouton de connexion), ainsi qu’un bouton de déconnexion dans la MainActivity (ou son fragment).

Voici l’UI obtenue dans cet exemple :
Capture-d-e-cran-2020-06-29-a--17.37.53

  • Initialisation et connexion

Comme dans la partie précédente (III), il faut appeler la routine initialize de AWSMobileClient dans la méthode onCreate, et rediriger l’utilisateur vers la suite de l’application s’il est déjà connecté. Dans le cas contraire, l’écran de connexion (Img1) s’affichera.

Lors de l’appui sur le bouton de connexion (buttonLogin.setOnClickListener), on déclenchera l’API signIn.
Cette API redirigera l’utilisateur vers la suite de l’application si la connexion se passe bien, mais elle vous indiquera également si c’est une première connexion et que le mot de passe doit être changé par exemple (à l’aide des objets SignInState).

Notre classe contient le code suivant :

class AuthenticationActivity : AppCompatActivity() {
   private val TAG = AuthenticationActivity::class.simpleName

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_authentication)

       initializeAws()
       setButton()
   }

   private fun initializeAws() {
       AWSMobileClient.getInstance()
           .initialize(applicationContext, object : Callback<UserStateDetails> {
               override fun onResult(userStateDetails: UserStateDetails) {
                   Log.i(TAG, userStateDetails.userState.toString())

                   when (userStateDetails.userState) {
                       UserState.SIGNED_IN -> goToMainActivity()
                       else -> AWSMobileClient.getInstance().signOut()
                   }
               }

               override fun onError(e: Exception) {
                   Log.e(TAG, e.toString())
               }
           })
   }

   private fun setButton() {
       buttonLogin.setOnClickListener {
           signIn(editTextLogin.text.toString(), editTextPwd.text.toString())
       }
   }

   private fun signIn(login: String, password: String) {
       showLoader()

       AWSMobileClient.getInstance()
           .signIn(login, password, null, object : Callback<SignInResult> {
               override fun onResult(signInResult: SignInResult) {
                   Log.d(TAG, "Sign-in callback state: " + signInResult.signInState)

                   when (signInResult.signInState) {
                       SignInState.DONE -> goToMainActivity()
                       SignInState.SMS_MFA -> Log.i(
                           TAG, "Please confirm sign-in with SMS.")
                       SignInState.NEW_PASSWORD_REQUIRED -> Log.i(
                           TAG, "Please confirm sign-in with new password.")
                       else -> Log.i(
                           TAG, "Unsupported sign-in confirmation: " 
                           + signInResult.signInState
                       )
                   }
                   hideLoader()
               }

               override fun onError(e: Exception) {
                   Log.e(TAG, e.toString())
                   showError(e.toString())
                   hideLoader()
               }
           })
   }

   // … other code
}

Certaines méthodes comme les loaders ou l’affichage des erreurs ne sont pas détaillées ici, mais vous pouvez les retrouver dans le code complet du projet.

  1. La déconnexion

Lorsque l’on arrive à se connecter, on veut également pouvoir se déconnecter. Et là rien de plus simple, il suffit d’appeler l’API signOut. Dans notre cas, on l'appellera sur le bouton créé à cet effet.

buttonLogout.setOnClickListener {
   AWSMobileClient.getInstance().signOut()
  
   // Close Activity and go to previous page
   activity?.finish()
}

Contrairement aux autres appels, pour faire simple ici, il n’y a pas de callback. Mais si vous en avez besoin, il est tout à fait possible d’en avoir un. La documentation est disponible ici.

  1. Les API AWSMobileClient

Toutes les API AWSMobileClient sont disponibles dans la documentation fournie par AWS :
https://docs.amplify.aws/sdk/auth/working-with-api/q/platform/android
Vous y trouverez tout ce qu’il faut pour le mot de passe oublié, sa modification, l’envoi du code de confirmation, ou encore des informations sur l’utilisateur et le token.

Le code complet du projet est disponible par ici : https://github.com/alebret/android-aws-authentication/tree/custom-authentication

V. Derniers points d’attention pour la route

Dans le cadre d‘un projet, j’ai tenté d’utiliser le projet aws-amplify fourni par AWS, qui recommande l’utilisation de GraphQL plutôt que REST.
Effectivement, j’ai rencontré de nombreux soucis avec l’utilisation de REST que j’ai finalement pu solutionner en me tournant vers le SDK.

La documentation AWS pour générer votre SDK : https://docs.aws.amazon.com/fr_fr/apigateway/latest/developerguide/how-to-generate-sdk.html

Dans le cadre de cet article, je n’ai pas abordé les tests, mais vous pouvez aussi bien y intégrer des TU que des tests fonctionnels automatisés.

Si vous souhaitez utiliser ce mécanisme d’authentification depuis une application web, vous pouvez vous rendre par ici : https://docs.amplify.aws/lib/auth/getting-started/q/platform/js

VI. Sources

https://aws.amazon.com/fr/blogs/mobile/building-an-android-app-with-aws-amplify-part-1/
https://aws-amplify.github.io/docs/sdk/android/start?ref=amplify-android-btn