Serverless : Authentification via Amplify iOS

AWS a sorti un outil qui permet d’augmenter la productivité des développeurs d’applications mobiles et webs. Amplify vient avec un Toolchain pour pouvoir créer et gérer son environnement cloud. Dans notre cas, il permet surtout de simplifier et optimiser les appels sécurisés vers les services AWS. Au moment de l’écriture de ces lignes, Amplify pour iOS et android vient de sortir sa première release et est en constante évolution. Il est possible que dans un futur proche, l’outil aura évolué et cet article ne représentera plus totalement la réalité.

Amplify existe aussi pour les technologies JS et React Native. Concernant iOS et Android, le framework a été développé en se basant sur les SDKs natifs.

Dans cet article, je vais vous montrer une configuration et des exemples d’utilisation de Amplify. Je pars du principe que vous avez déjà des connaissances sur les services Cognito, API Gateway et IAM. La configuration de la partie consommation API est présente pour préparer le prochain article sur API REST.

Configuration & Authentification

Préparation & prérequis

Vous trouverez la documentation officielle du projet ici :

https://aws-amplify.github.io/docs/ios/start

Le code source du projet qui va illustrer mon article est présent ici :

https://gitlab.ippon.fr/rberthome/ios-ippon-amplify-test

Vous pouvez vous baser sur ce code pour votre configuration et le faire évoluer à votre convenance. Il s’agit d’un projet de base qui sera amené à évoluer et s’améliorer dans le temps.  
L’application contient un écran de connexion, de mot de passe oublié, de changement de mot de passe et un principal.

Il faut au préalable avoir NPM présent sur votre machine.

Installation cli Amplify

Exécutez cette commande dans votre terminal.

npm install -g @aws-amplify/cli@latest

Installation des pods

Ajoutez ces lignes dans votre fichier Podfile :

pod 'Amplify'
pod 'Amplify/Tools'
pod 'AWSPluginsCore'
pod 'AmplifyPlugins/AWSAPIPlugin'

Ces lignes permettent d’ajouter Amplify et le plugin de consommation des API gateway.

Ajout Amplify Tools

Il faut ajouter une build phase à notre projet afin de générer les fichiers de configuration.

La ligne suivante fera le nécessaire au build :
"${PODS_ROOT}/AmplifyTools/amplify-tools.sh"

Figure 1 : ajout de script dans les phases de build

Maintenant vous pouvez construire le projet. (Cmd + B)

Les fichiers et répertoires suivants seront générés :

  • amplify (répertoire) - Le répertoire amplify est celui qui gère la configuration du backend (il est possible de gérer des services AWS  depuis le CLI amplify)
  • amplify/.config (répertoire) - Contient les fichiers de configuration cloud et les préférences/ paramètres utilisateur.
  • amplify/current-cloud-backend (répertoire) - Contient les dernières spécifications des ressources du backend depuis la dernière synchronisation, par une commande amplify push ou amplify env pull. Chaque plugin stocke ses contenus dans un sous-répertoire.
  • amplify/backend (répertoire) - Contient les dernières modifications locales des spécifications des ressources du backend à déployer. Chaque plugin stocke ses contenus dans un sous-répertoire.
  • amplifytools.xcconfig - Ce fichier contrôle le comportement des amplify tools
  • amplifyconfiguration.json - Ce fichier sera ajouté au projet et intégré à votre bundle. Il est requis pour le fonctionnement des amplify libraries.
  • awsconfiguration.json -  Ce fichier sera ajouté au projet et intégré à votre bundle. Il est requis pour le fonctionnement des amplify libraries.

Si après le build, l’erreur suivante survient :
Undefined symbol: _OBJC_CLASS_$_AWSSignatureV4Signer
il suffit de nettoyer le répertoire de build (Cmd + Shift + k)

Configurations

Dans cet article, nous allons voir comment authentifier un utilisateur. Pour se faire, nous allons utiliser Cognito. Pour plus d’information sur Cognito, je vous conseille d’aller faire un tour sur la documentation officielle. (https://docs.aws.amazon.com/cognito/)

Nous utiliserons IAM pour définir les rôles et les droits sur les API.

Cognito

à la racine du projet, un fichier awsconfiguration.json est présent. C’est dans ce fichier que nous allons renseigner les informations de Cognito. Il faut remplacer les valeurs ci-dessous PoolId, Region et AppClientId par celles présentes sur votre console AWS dans la partie Cognito.

{
    "UserAgent": "aws-amplify-cli/2.0",
    "Version": "0.1.0",
    "IdentityManager": {
        "Default": {}
    },
    "CredentialsProvider": {
        "CognitoIdentity": {
            "Default": {
                "PoolId": "eu-west-1:XXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXX",
                "Region": "eu-west-1"
            }
        }
    },
    "CognitoUserPool": {
        "Default": {
            "AppClientId": "1234567890ABCDEF",
            "PoolId": "eu-west-1_XXXXXXXXXX",
            "Region": "eu-west-1"
        }
    }
}

Amplify & API plugin

À la racine du projet, il y a un fichier nommé amplifyconfiguration.json, ce fichier contient les informations nécessaires pour utiliser les services accessibles via AMplify. Dans notre cas nous n’aurons que API. Il faut remplacer certaines des valeurs ci-dessous dev.name, dev.endpoint, recette.region, recette.name et recette.endpoint par celles présentes sur votre console AWS dans la partie API Gateway.

{
    "UserAgent": "aws-amplify-cli/2.0",
    "Version": "1.0",
    "api" : {
        "plugins" : {
            "awsAPIPlugin" : {
                "dev" : {
                    "region": "eu-west-1",
                    "name": "dev",
                    "endpoint": "https://XXXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev",
                    "authorizationType" : "AWS_IAM",
                    "endpointType" : "REST"
                },
                "recette" : {
                    "region": "eu-west-1",
                    "name": "recette",
                    "endpoint": "https://XXXXXXXXXX.execute-api.eu-west-1.amazonaws.com/recette",
                    "authorizationType" : "AWS_IAM",
                    "endpointType" : "REST"
                }
 
            }
        }
    }
}

La valeur "AWS_IAM" pour authorizationType est utilisée pour indiquer que nous allons passer par les rôles IAM pour authentifier un utilisateur sur les appels API. De cette manière nous pourrons gérer précisément les droits d’accès aux API que nous développons. Pour plus d’information sur ce sujet, vous pouvez aller suivre ce lien :

https://aws.amazon.com/fr/premiumsupport/knowledge-center/iam-authentication-api-gateway/

La première clé présente sous awsAPIPlugin sera la propriété à utiliser lors des requêtes à l’API.

Initialisation

L’initialisation d’Amplify est pratiquement automatisée une fois que les informations de configurations ont été correctement renseignées. Dans la classe AppDelegate, il faut ajouter ces lignes d’import :

import AmplifyPlugins
import Amplify

Dans la fonction application de cette classe, il faut ajouter ces lignes :

let apiPlugin = AWSAPIPlugin()
try! Amplify.add(plugin: apiPlugin)
try! Amplify.configure()

La première ligne initialise le plugin qui permettra de faire nos requêtes à l’API Gateway.

La seconde ligne permet d’ajouter le plugin à Amplify.

La dernière permet d’initialiser Amplify entièrement. Les fichiers de configurations seront utilisés. Il est possible de passer en paramètre de la fonction configure un objet qui contient les données de configurations.

Une fois Amplify configuré, il est possible de l’utiliser dans toute l’application.

Authentification

Dans cette partie nous allons implémenter l’authentification. Il y a plusieurs éléments que nous pouvons retrouver sur toutes les technologies d'authentification.

Le premier élément est l’initialisation de notre client (ici AWSMobileClient). Les autres éléments sont l’authentification, la déconnexion, le changement de mot de passe et la gestion du mot de passe oublié.

Initialisation

Cette étape nous permet de récupérer l’état actuel de l’utilisateur, de cet état nous pouvons orienter l’utilisateur vers un écran d’authentification ou directement à l’écran principal de notre application.

Le pattern Coordinator a été utilisé dans ce projet. Dans celui de gestion de l'authentification AuthentificationCoordinator, nous ajoutons un listener sur l’état de l’utilisateur et nous exécutons une première requête pour connaître l’état actuel.

Initialiser et déterminer l’état de l’utilisateur courant :

AWSMobileClient.default().initialize()

Ajouter le listener de l’état de l’utilisateur sur la classe actuelle :

AWSMobileClient.default().addUserStateListener(self)

De cette manière nous pouvons gérer le UserState et agir en fonction. Pour plus d’information sur les états possibles :  
https://aws-amplify.github.io/aws-sdk-ios/docs/reference/AWSMobileClient/Enums/UserState.html

J’ai ajouté une partie dans le SignedIn pour prendre en compte l’état passwordResetRequired qui est une demande faite par l’administrateur depuis la console Cognito. Dans ce cas de figure, l’utilisateur a déjà obtenu un code de vérification et il sera redirigé vers la partie confirmation du mot de passe oublié.

Services

Un classe AuthentificationService a été créée pour gérer tous les appels vers Cognito. Le principe de fonctionnement est similaire. Pour chaque cas, nous utiliserons une delegate AuthentificationResultDelegate, elle permet de retourner un succès ou une erreur sur chaque demande.

Nous utilisons la configuration par défaut :

AWSMobileClient.default()

Exemple pour la gestion de la connexion :

static func login(_ sender: AuthentificationResultDelegate, username: String, password: String) {
        AWSMobileClient.default().signIn(username: username, password: password)
        { signInResult, error in
            self.manageSignIn(sender, signInResult: signInResult, error: error)
        }
    }
 
private static func manageSignIn(_ sender: AuthentificationResultDelegate, signInResult: SignInResult?, error: Error?) {
        if let signInResult = signInResult {
            switch(signInResult.signInState) {
            case .signedIn:
                sender.onSuccess(SignInCustomStatus.signedIn("User is signed in."))
            case .newPasswordRequired:
                sender.onSuccess(SignInCustomStatus.newPasswordRequired("New Password required"))
            default:
                sender.onError(error: SignInError.signInUnknown("Unexpected case"))
            }
        } else if let error = error as? AWSMobileClientError {
            switch error {
            case .passwordResetRequired(let message):
                sender.onError(error: SignInError.newPasswordRequired(message))
            default:
                // You can manage a multiple type of error here to know exactly what happened
                sender.onError(error: error)
            }
        }
    }

Dans manageSignIn, nous allons traiter signInResult et error afin de notifier à l’appelant (sender) si l’opération a été réussie ou non. La méthode onSuccess ou onError de sender sera appelée.

Connexion

Cette étape consiste à proposer à l’utilisateur un écran sur lequel il va saisir un login et un mot de passe. Il appuie sur un bouton afin de valider sa saisie et effectuer une demande d’authentification.

L’utilisateur saisit ses informations de connexion, en cas d’erreur le message associé est affiché sous le champ de saisie du mot de passe. Il y a ensuite deux cas de figure en cas de succès d’authentification :

  • Connecté (`signedIn`) : identifiants corrects et compte valide
  • Nouveau mot de passe requis (`newPasswordRequired`) : identifiants corrects mais obligation de changer de mot de passe.

Dans le dernier cas, nous allons rediriger l’utilisateur vers l’écran de changement de mot de passe.

Figure 2 : écrans d'authentification

LoginViewController

NewPasswordRequiredViewController

ConfirmForgotPasswordViewController

Mot de passe Oublié

Cette partie est dédiée au mot de passe oublié. Dans notre cas, AWS nous envoie un code de confirmation. Ce code est utilisé pour confirmer la perte du mot de passe. L’utilisateur doit saisir son nouveau mot de passe et ce code pour terminer son changement. L’obtention de ce code se fait via un simple appel contenant l’email de l’utilisateur.

Figure 3 : écran pour changer un mot de passe oublié

Changer mon mot de passe

Dans cette section, nous proposons à l’utilisateur de changer son mot de passe. L’appui sur le bouton “crayon” en haut à droite le redirigera vers la page dédiée.

alt_text
Figure 4 : écran pour changer de mot de passe

Déconnexion

Dans cette section, nous allons simplement déconnecter l’utilisateur courant. Il sera redirigé vers l’écran de connexion. La déconnexion se fait par l’appui sur le bouton en haut à droite. Un simple appel à logout, de la classe utilitaire d'Amplify AWSMobileClient, permet de notifier AuthentificationCoordinator du changement d’état d’authentification de l’utilisateur courant. Dans notre cas, nous avions indiquer d’afficher la page de connexion dans le cas d’un signedOut

Création de compte

Dans cette section, nous autorisons la création d’un compte. Ici, nous sommes sur un compte avec seulement login/password obligatoire. Le reste n’est pas renseigné mais il est possible d’ajouter ces attributs facilement. Une fois les informations saisies, un code de confirmation est envoyé. Dans notre cas, c’est un mail qui est envoyé au mail correspondant au login. Une fois la validation effectuée, l’utilisateur est redirigé vers l’écran d’authentification pour qu’il puisse profiter du contenu l’application.

alt_text
Figure 5 : écran pour créer un compte

Conclusion

L’authentification est assez simple à mettre en place. Dans notre cas, j’ai développé les écrans mais il est possible d’utiliser les interfaces proposées par AWS. La partie ajout des informations d’authentification de l’utilisateur sur les requêtes est déléguée à Amplify. Ce point simplifie énormément la consommation des services. Tout ce qui est lié à la sécurité et à la scalabilité est managé, nous pouvons donc nous focaliser sur le développement de nos interfaces et du fonctionnement global de notre application.