Ionic 2 partie 3 : L’authentification
Dans cette troisième partie nous allons voir un exemple de mise en place d’une authentification au sein d’une application Ionic 2. Comme pour les deux articles précédents nous allons prendre comme exemple notre application mobile NFC. Pour rappel, le premier article détaillait la structure du projet et le second article le menu et la navigation de l’application.
Workflow
Le processus d’authentification de notre application est très proche de celui évoqué dans notre article sur la sécurité HMAC puisqu’il s’agit ici aussi d’utiliser un Json Web Token. Je ne vais pas redétailler le processus, je vous invite à relire l’article. L’idée, dans cette troisième partie, étant de voir comment stocker le JWT et comment l’envoyer automatiquement en header HTTP de chacune des requêtes de notre application Ionic 2.
Mais commençons par le commencement et voyons tout d’abord la page de login.
Page de login
Dans nos sources, on peut retrouver les différentes classes et templates liés au login dans le dossier app/pages/login.
Template
Le formulaire de login se trouve dans le fichier login.html et son contenu est relativement classique:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<form [ngFormModel]="loginForm" (submit)="login($event,username,password,rememberMe)" style="padding-top: 50px"> <ion-list no-lines> <ion-item> <ion-label stacked-label>{{'login.username' | translate}}</ion-label> <ion-input type="text" [(ngModel)]="username" placeholder="admin" ngControl="username"></ion-input> </ion-item> <ion-item> <ion-label stacked-label>{{'login.password' | translate}}</ion-label> <ion-input type="password" [(ngModel)]="password" placeholder="admin" ngControl="password"></ion-input> </ion-item> <ion-item> <ion-label stacked-label>{{'login.remember-me' | translate}}</ion-label> <ion-checkbox dark [(ngModel)]="rememberMe" ngControl="rememberMe"></ion-checkbox> </ion-item> </ion-list> <div> <button block type="submit" [disabled]="!loginForm.valid">{{'buttons.login' | translate}}</button> </div> </form> |
- Notre formulaire contient trois champs: username, password et rememberMe et bien entendu on retrouve sur chacun d’entre eux la directive [(ngModel)] pour le binding bidirectionnel.
- La validation du formulaire est possible grâce aux directives [ngFormModel] et ngControl (sera détaillé dans le chapitre suivant).
- A la soumission du formulaire la fonction login est appelée grâce à la directive (submit) sur le tag form .
Composant
Voyons maintenant le composant @Page associé au formulaire et qui est déclaré dans le fichier login.ts :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Page({ templateUrl: 'build/pages/login/login.html', providers:[LoginService], (...) }) export class LoginPage { loginForm:ControlGroup; rememberMe = false; constructor(private loginService:LoginService,(....)){} login(event:Event,username:string,password:string,rememberMe:boolean):void { (...) this.loginService.login(username,password,rememberMe).subscribe(() => { (...) this.nav.setRoot(NFCPage); },(alert:Alert) => { this.nav.present(alert); }); } } |
- L’attribut templateUrl nous permet de faire le lien avec notre fichier de template et providers crée un injecteur pour le service LoginService, déclaré dans la fichier login.service.ts, ce qui nous permettra de faire une injection de dépendance comme c’est le cas dans le constructeur.
- Deux variables sont créées sur la classe :
- loginForm: Du type ControlGroup qui va nous permettre de faire la validation (voir plus bas).
- rememberMe: Liée à la checkbox du template par la directive ngModel et est initialisée à false. La case sera donc décochée par défaut.
- On retrouve notre fonction login appelée par l’événement (submit) du formulaire. Comme on peut s’en douter la fonction login fait appel au service LoginService et notamment à sa méthode doLogin qui va se charger de l’authentification à proprement parler.
- La méthode
this.loginService.login retourne un Observable, ainsi :
- En cas de succès de l’authentification, on redirige l’utilisateur vers la page d’accueil : NfcPage
- En cas d’échec on reste sur la page de login et on affiche une alerte d’erreur.
- this.nav est une instance de NavController (Voir l’article sur la navigation au sein d’ionic 2)
Service login
Authentification
1 2 3 4 5 6 7 8 |
const CONTENT_TYPE_HEADER:string = 'Content-Type'; const APPLICATION_JSON:string = 'application/json'; const BACKEND_URL:string = 'http://demo2726806.mockable.io/login'; @Injectable() export class LoginService { constructor(private http:Http) {} (...) } |
Comme on peut s’y attendre, le service fait une injection de dépendance sur Http ce qui nous permettra de faire un appel au backend. L’url de login appelée est un mock créé sur la plateforme mockable.io et qui permet de retourner des données et des headers http statiques.
Tout se passe dans la méthode login du service qui prend en paramètre les identifiants, le « remember me » et qui retourne un Observable:
1 |
login(username:string,password:string,rememberMe:boolean):Observable<any>{} |
Dans un premier temps on vérifie, de manière statique, si les identifiants saisis sont corrects. Si ils ne le sont pas, une modal d’erreur est affichée et une exception est levée via la méthode Observable.throw.
1 2 3 4 5 6 7 8 |
if(username.toLowerCase() !== 'admin' || password.toLowerCase() !== 'admin') { let alert = Alert.create({ title: 'Invalid credentials', subTitle: 'You entered invalid credentials !', buttons: ['Ok'] }); return Observable.throw(alert); } |
Si les informations saisies sont correctes, une requête http de type POST est faîte grâce au service http d’Angular 2 dont toutes les méthodes retourne un Observable .
1 |
return this.http.post(BACKEND_URL,JSON.stringify({login:username,password:password}),{headers:headers}).map((res:Response) => {}); |
Json Web Token
Structure
Mais la partie qui nous intéresse vraiment est la récupération des données renvoyées par le webservice d’authentification et notamment le JWT dont voici la donnée brute :
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxIiwiaWF0IjoxN DU4NzM1Njc5LCJleHAiOjE0NTg3MzcxMTQsImF1ZCI6Ind3dy5yZWRmcm9nZ3kuZnIiLCJzdW IiOiJtaWNoYWVsLmRlc2lnYXVkQHJlZGZyb2dneS5mciIsImZpcnN0TmFtZSI6Ik1pY2hhZWw iLCJsYXN0TmFtZSI6IkRlc2lnYXVkIiwicm9sZSI6WyJNYW5hZ2VyIiwiVXNlciJdfQ.ZAeN- thKID2PMEvjT9nwRRf5gsHlwCuKQEOCZIMBv-Q"}
Il s’agit d’un objet au format JSON avec une seule propriété « token » ayant pour valeur un jwt.
Pour rappel un jwt est constitué de trois parties. Chacune est encodée en base64 et c’est la deuxième qui nous intéresse. On appelle le « payload » et la donnée décodée contient :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "iss": "1", "iat": 1458735679, "exp": 1458737114, "aud": "www.redfroggy.fr", "sub": "michael.desigaud@redfroggy.fr", "firstName": "Michael", "lastName": "Desigaud", "role": [ "Manager", "User" ] } |
On retrouve principalement des données utilisateurs et c’est ce que l’on cherche à récupérer et à stocker localement en plus du jwt lui même.
Récupération et stockage
On parse tout d’abord la réponse en objet json puis on récupère le token :
1 2 3 4 |
return this.http.post(BACKEND_URL,JSON.stringify({login:username,password:password}),{headers:headers}).map((res:Response) => { let loginData:any = res.json(); let user:User = this.readJwt(loginData.token); }); |
La méthode readJwt de la classe LoginService va lire les données du payload qui sont ensuite affectées à une instance de la classe User. Le token JWT est ensuite stocké en local storage via la classe utilitaire StorageUtils et sera utilisé pour toute les futures requêtes http dans le header Authorization .
Si l’utilisateur a coché la case rememberMe on stocke les informations de l’utilisateur et le jwt en local storage. On pourrait évidemment ne stocker que le jwt étant donnée que la variable user est instanciée à partir de données extraites du payload jwt mais pour des raisons pratiques et pour ne pas avoir à extraire ces informations systématiquement on les stocke également.
Ces données seront notamment affichées dans la page « Mon compte« .
1 2 3 4 |
if (rememberMe) { StorageUtils.setAccount(user); StorageUtils.setToken(loginData.token); } |
Lancement de l’application
Ce que l’on souhaite c’est qu’au lancement de l’application la page de login soit affichée si l’utilisateur ne s’est jamais connecté ou s’il n’a pas coché la case « se souvenir de moi ». Dans le cas contraire on ne présente pas l’écran d’authentification et on affiche directement la page d’accueil.
C’est naturellement dans le fichier app.ts (qui est notre point d’entrée) que cela va se jouer :
1 2 3 4 5 |
if (StorageUtils.hasAccount()) { this.rootPage = NFCPage; } else { this.rootPage = LoginPage; } |
Rien de tres compliqué ici, on teste simplement la présence des données utilisateurs en local storage. Il nous reste désormais à voir comment envoyer le jwt dans chaque requête http via notamment le header Authorization .
Surcharge HTTP
En effet une authentification par token nécessite que le client envoi systématiquement le jeton afin qu’il puisse s’identifier auprès du serveur. Coté Ionic 2 /Angular 2 l’astuce est de surcharger la classe http offerte par le Framework Angular pour notamment ajouter globalement notre jeton dans un header http comme nous l’avions expliquer dans notre article sur Angular2, Spring et HMAC.
La classe JwtHttp répond à ce besoin et ajoute le header « Authorization » pour chacune des différents type de requêtes : get, post, put delete, etc..
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export class JwtHttp extends Http { (....) setAuthorizationHeader(options?: RequestOptionsArgs):void { let jwt:string = StorageUtils.getToken(); if(options && jwt) { options.headers.set(AUTHORIZATION_TYPE_HEADER,'Bearer '+jwt); } } get(url: string, options?: RequestOptionsArgs): Observable<Response> { this.setAuthorizationHeader(options); return super.get(url, options); } } |
Malheureusement pour que Angular utilise notre classe cela ne suffit pas. Il nous faut créer la factory correspondante sur l’annotation @App dans le fichier app.ts :
1 2 3 4 5 6 7 8 9 10 11 12 |
@App({ (...) provide(Http,{ useFactory:(xhrBackend: XHRBackend, requestOptions: RequestOptions) => { return new JwtHttp(xhrBackend, requestOptions); }, deps: [XHRBackend, RequestOptions], multi:false }) ], (..) }) |
Ainsi à chaque injection de dépendance sur la classe http, l’instance de JwtHttp créée dans la fonction useFactory sera utilisée.
Nous verrons dans notre prochain article un exemple d’utilisation d’un plugin cordova.
Salut Michael,
Tout d’abord grand merci pour ces articles, notamment pour la petite découverte du framework Ionic suivi d’AngularJs VERSION 2. très bon tuto.
Depuis un certain temps je commence réellement à apprécier le développement pour application mobile, mais voilà j’avais encore du mal à faire un choix avec tous les services et outils qui nous permettent justement à réaliser à tout cela.
Puis en lisant tes posts, il y a pas photo, en plus du TypeScript qui fait ça place sur ce Framework.
Je crois être convaincu de cette vague, merci Michael pour ces tutos à la fois détaillés et instructifs.
Je vous avoue que c’est la première fois que je tombe sur votre site, et je pense que ça ne sera pas la dernière. ^^
Très bon courage pour la suite et continuez ainsi
Cordialement,
Kivivi