Tutoriel Angular2 & Symfony3 - apprendre à créer rapidement un système d'authentification

Grégory Lanau

Après avoir travaillé sur AngularJS, j'ai voulu tester la seconde version du framework : Angular2. Pour me faire la main, j'ai choisi de travailler sur un système d'authentification par token en utilisant Symfony pour la partie back-end, en incluant le bundle Lexik JWT Authentication.

Je vous propose de voir ensemble un exemple très simple et les différentes étapes à suivre pour la mise en place d'une API sécurisée servant une liste de publications (…très privées) à notre application Angular2.

Commentez

- Titre : Tutoriel Angular2 & Symfony3 - apprendre à créer rapidement un système d'authentification

- Auteur : Grégory Lanau

- Parution : 20 février 2018

- Licence : Copyright ® 2018 Grégory Lanau. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Mise en place de la partie Back-end

- I-A. Installation d'une application Symfony3

- I-B. Installation des bundles nécessaires

- I-B-1. FOSRestBundle

- I-B-2. NelmioCorsBundle

- I-B-3. JMSSerializerBundle

- I-B-4. LexikJWTAuthenticationBundle

- I-C. Création d'utilisateurs

- I-D. Création d'un jeu de données

- II. Mise en place de la partie front-end

- II-A. Création de l'application Angular2 via Angular CLI

- II-B. Création des différents composants

- II-C. Mise en place d'un système de routing

- II-D. Protéger les routes authentifiées avec AuthGuard

- II-E. Authentifier ses requêtes avec Angular2-jwt

- III. Conclusion

- IV. Remerciements

I - Mise en place de la partie Back-end

I-A - Installation d'une application Symfony3

Installons tout d'abord la dernière version de Symfony3 via l'installeur prévu à cet effet sur le site officiel :

symfony new api-lab

Puis lançons le serveur PHP interne via cette commande à la racine du projet :

bin/console server:start

I-B - Installation des bundles nécessaires

Viennent ensuite l'installation et la configuration de certains bundles incontournables lorsque l'on veut créer une API. Nous sauterons volontairement l'étape du « composer require » et de la déclaration des bundles dans le Kernel de Symfony pour passer directement à la configuration.

I-B-1 - FOSRestBundle

Ce bundle va nous permettre d'utiliser des routes API automatiques ainsi que de retourner des réponses au format Json à notre client Angular2 avec un minimum de configuration :

app/config/config.yml

1. fos_rest:

2. routing_loader:

3. default_format: json

4. view:

5. view_response_listener: true

app/config/routing.yml

1. app:

2. resource: "@AppBundle/Controller/"

3. type: rest

4. prefix: /api

+ d'information sur la documentation

I-B-2 - NelmioCorsBundle

Continuons ensuite avec le Bundle, qui va nous permettre de faire des requêtes Ajax sur l'API, étant donné que nos deux applications se trouvent sur deux domaines différents :

1. nelmio_cors:

2. paths:

3. '^/api/':

4. allow_origin: ['http://localhost:4200']

5. allow_headers: ['origin', 'content-type', 'authorization']

6. allow_methods: ['POST', 'PUT', 'GET', 'DELETE']

7. max_age: 3600

Nous avons ici autorisé notre future application Angular2 ainsi que le header « authorization » qui nous servira à nous authentifier. Patience, on y est bientôt.

I-B-3 - JMSSerializerBundle

Ce bundle va nous permettre de sérialiser les données renvoyées par notre API. Aucune configuration n'est nécessaire dans le cadre de ce tutoriel. Nous utiliserons JMSSerializer plus tard, directement dans notre PostController.

I-B-4 - LexikJWTAuthenticationBundle

Enfin, dernier, mais pas le moindre, le bundle qui va nous servir à sécuriser l'accès à nos données Symfony via un token d'authentification. Je vous laisse lire la documentation officielle qui est très claire. Il vous suffit vraiment de suivre les étapes point par point.

J'ai ajouté deux petites lignes sous l'index « form_login » du security.yml de façon à pouvoir envoyer username & password au lieu de _username et _password pour nous authentifier auprès de notre API. Je vous invite à en faire de même.

username_parameter: username
password_parameter: password

Nous allons ensuite devoir générer des données de bases pour pouvoir tester notre système.

I-C - Création d'utilisateurs

Nous avons besoin d'un utilisateur. Il s'appellera « gary » et aura comme password « pass » (très original…). Pour ce faire, nous n'allons pas mettre en place un système de gestion d'utilisateurs, car ce n'est pas le but de ce tutoriel. Nous allons utiliser le système « user in memory » de Symfony. Je vous propose donc de rajouter un peu de configuration :

app/config/security.yml

1. security:

2. encoders:

3. Symfony\Component\Security\Core\User\User: plaintext

5. providers:

6. in_memory:

7. memory:

8. users:

9. gary:

10. password: pass

11. roles: 'ROLE_USER'

I-D - Création d'un jeu de données

Nous allons avoir besoin de publications à renvoyer à notre client Angular2. Nous devons créer une entity « Post » qui sera la représentation de nos données.

Nous n'ajouterons qu'une seule propriété « title » à cette entity pour les besoins de ce tutoriel ; même s'il était utile que nos publications aient aussi un auteur, un contenu, une date de création, etc., etc.

1. <?php

3. namespace AppBundle\Entity;

5. use Doctrine\ORM\Mapping as ORM;

6. use JMS\Serializer\Annotation as Serializer;

8. /**

9. * Post

10. *

11. * @ORM\Table(name="posts")

12. * @ORM\Entity

13. *

14. * @Serializer\ExclusionPolicy("all")

15. */

16. class Post

17. {

18. /**

19. * @var int

20. *

21. * @ORM\Column(name="id", type="integer")

22. * @ORM\Id

23. * @ORM\GeneratedValue(strategy="AUTO")

24. */

25. private $id;

27. /**

28. * @var string

29. *

30. * @ORM\Column(type="string", length=255)

31. *

32. * @Serializer\Expose

33. */

34. private $title;

36. /**

37. * Get id

38. *

39. * @return int

40. */

41. public function getId()

42. {

43. return $this->id;

44. }

46. /**

47. * Set title

48. *

49. * @param string $title

50. *

51. * @return Post

52. */

53. public function setTitle($title)

54. {

55. $this->title = $title;

57. return $this;

58. }

60. /**

61. * Get title

62. *

63. * @return string

64. */

65. public function getTitle()

66. {

67. return $this->title;

68. }

69. }

Utilisons ensuite le DoctrineFixturesBundle (après l'avoir installé bien sûr !) pour générer deux publications en créant une classe LoadPostData :

1. <?php

3. namespace AppBundle\DataFixtures\ORM;

5. use Doctrine\Common\DataFixtures\FixtureInterface;

6. use Doctrine\Common\Persistence\ObjectManager;

7. use AppBundle\Entity\Post;

9. class LoadPostData implements FixtureInterface

10. {

11. public function load(ObjectManager $manager)

12. {

13. $post = new Post();

14. $post->setTitle('post1');

16. $post2 = new Post();

17. $post2->setTitle('post2');

19. $manager->persist($post);

20. $manager->persist($post2);

21. $manager->flush();

22. }

23. }

Créons ensuite la base de données, le schéma, et chargeons les données via ces commandes :

1. # Création de la base

2. bin/console do:da:cr

4. # Création du schéma de données

5. bin/console do:sc:cr

7. # Générations des fixtures

8. bin/console doctrine:fixtures:load

Enfin, créons notre PostController avec la méthode qui sera le endpoint de notre micro API :

1. <?php

3. namespace AppBundle\Controller;

5. use FOS\RestBundle\Controller\FOSRestController;

6. use FOS\RestBundle\Routing\ClassResourceInterface;

7. use FOS\RestBundle\Controller\Annotations as Rest;

9. class PostController extends FOSRestController implements ClassResourceInterface

10. {

11. /**

12. * @Rest\View()

13. */

14. public function cgetAction()

15. {

16. return $this->getDoctrine()->getRepository('AppBundle:Post')->findAll();

17. }

18. }

Nous voilà parés ! Vous pouvez dès lors tester la partie back-end en faisant une requête POST vers notre endpoint :

curl -X POST http://localhost:8000/api/login_check -d username=gary -d password=pass

Si tout va bien, vous devriez recevoir un token d'authentification.

C'est le cas ? Très bien, nous allons pouvoir commencer la partie front-end.

II - Mise en place de la partie front-end

II-A - Création de l'application Angular2 via Angular CLI

Installons tout d'abord Angular CLI globalement sur notre machine. Cet outil va nous servir à générer la structure de notre application via une simple commande et à recompiler à la volée nos modifications :

npm install -g @angular/cli

Créons ensuite notre application :

ng new api-lab

Puis lançons notre serveur interne :

cd api-lab && ng serve

Maintenant que notre application est lancée, vous pouvez vous rendre sur l'URL indiquée dans votre console pour accéder à votre application :

http://localhost:4200

Vous devriez alors voir apparaître : app works!

II-B - Création des différents composants

Nous allons ensuite générer trois composants principaux supplémentaires :

- homepage ;

- authentication ;

- post.

Commençons notre composant « homepage » en lançant la commande suivante :

ng g c homepage

g pour generate et c pour component.

homepage/homepage.component.ts

1. import { Component } from '@angular/core';

3. @Component({

4. selector: 'app-homepage',

5. templateUrl: './homepage.component.html',

6. })

7. export class HomepageComponent {}

Vous remarquerez que nous avons enlevé la déclaration du fichier CSS. En effet, nous inclurons bootstrap pour styliser rapidement notre application.

homepage/homepage.html

<h1>Home</h1>

Créons ensuite le composant « authentication » de la même manière :

ng g c authentication

authentication/authentication.component.ts

1. import { Component } from '@angular/core';

2. import { FormBuilder, FormGroup, Validators } from '@angular/forms';

3. import { Router } from '@angular/router';

5. import { AuthenticationService } from './authentication.service';

7. @Component({

8. selector: 'app-authentication',

9. templateUrl: './authentication.component.html',

10. })

11. export class AuthenticationComponent {

12. loginForm: FormGroup;

13. error: string = '';

15. constructor(

16. private formBuilder: FormBuilder,

17. private authenticationService: AuthenticationService,

18. private router: Router

19. ) {

20. this.loginForm = formBuilder.group({

21. 'username': ['', Validators.required],

22. 'password': ['', Validators.required]

23. });

24. }

26. onSubmit() {

27. this.authenticationService

28. .authenticate(this.loginForm.value)

29. .subscribe(

30. data => {

31. localStorage.setItem('id_token', data.token);

32. this.router.navigate(['post']);

33. },

34. error => this.error = error.message

35. );

36. }

37. }

authentication/authentication.component.html

1. <h1>Login</h1>

2. <div>

3. <div [hidden]="!error" class="alert alert-danger" role="alert">

4. <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>

5. <span class="sr-only">Error:</span>

6. </div>

7. <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">

8. <div class="form-group">

9. <input type="text" class="form-control" placeholder="Username*" formControlName="username">

10. </div>

11. <div class="form-group">

12. <input type="password" class="form-control" placeholder="Password*" formControlName="password">

13. </div>

14. <button type="submit" class="btn btn-success pull-right" [disabled]="!loginForm.valid">Submit</button>

15. </form>

16. </div>

Ce composant sera notre formulaire d'authentification vers notre API. Nous utiliserons le module ReactiveFormsModule de Angular2 pour une mise en place plus simple sans utiliser la directive [(ngModel)] très gourmande en ressources et pour pouvoir lui attribuer des validateurs.

Passons ensuite à la création du composant « post » :

ng g c post

post/post.component.ts

1. import { Component, OnInit } from '@angular/core';

3. import { PostRepository } from './post-repository.service';

5. @Component({

6. selector: 'app-post',

7. templateUrl: './post.component.html'

8. })

9. export class PostComponent implements OnInit {

10. posts: any[] = [];

11. error: string = '';

13. constructor(private postRepository: PostRepository) {}

15. ngOnInit() {

16. this.postRepository

17. .getList()

18. .subscribe(

19. data => this.posts = data,

20. error => this.error = error.message

21. );

22. }

23. }

post/post.component.html

1. <h1>Posts</h1>

2. <div [hidden]="!error" class="alert alert-danger" role="alert">

3. <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>

4. <span class="sr-only">Error:</span>

6. </div>

7. <ul>

8. <li *ngFor="let post of posts">

9. <a href="#"></a>

10. </li>

11. </ul>

Pour finir, nous allons ajouter deux services à notre application :

- un service qui nous servira à nous authentifier sur notre API et à gérer le login et le logout ;

- un service qui nous servira à requêter notre endpoint et qui nous renverra notre liste de publications.

Pour ce faire, lancez les commandes suivantes :

ng g s authentication/authentication --flat

g pour generate, s pour service (je sais que vous le saviez !) et -flat pour créer des composants sans dossiers.

authentication/authentication.service.ts

1. import { Injectable } from '@angular/core';

2. import { Http, Response, Headers, RequestOptions, URLSearchParams } from '@angular/http';

3. import { tokenNotExpired } from 'angular2-jwt';

5. import 'rxjs/add/operator/map';

7. @Injectable()

8. export class AuthenticationService {

10. constructor(private http: Http) {}

12. authenticate(user: any) {

13. let url     = 'http://127.0.0.1:8000/api/login_check';

14. let body     = new URLSearchParams();

15. body.append('username', user.username);

16. body.append('password', user.password);

17. let headers = new Headers({'Content-Type': 'application/x-www-form-urlencoded'});

18. let options = new RequestOptions({headers: headers});

20. return this.http

21. .post(url, body.toString(), options)

22. .map((data: Response) => data.json());

23. }

25. logout() {

26. localStorage.removeItem('id_token');

27. }

29. loggedIn() {

30. return tokenNotExpired();

31. }

32. }

ng g s post/post-repository --flat

post/post-repository.service.ts

1. import { Injectable } from '@angular/core';

2. import { Response } from '@angular/http';

3. import { AuthHttp } from 'angular2-jwt';

5. @Injectable()

6. export class PostRepository {

8. constructor(private authHttp: AuthHttp) {}

10. getList() {

11. let url = 'http://127.0.0.1:8000/api/posts';

13. return this.authHttp

14. .get(url)

15. .map((data: Response) => data.json());

16. }

17. }

Nous remarquerons que nous utilisons Http dans l'authentication service alors que nous utilisons AuthHttp dans le post-repository service. Il y a une très bonne raison à cela. En effet, comme il est écrit dans la documentation de la librairie Angular2-jwt :

This library does not have any functionality for (or opinion about) implementing user authentication and retrieving JWTs to begin with. Those details will vary depending on your setup, but in most cases, you will use a regular HTTP request to authenticate your users and then save their JWTs in local storage or in a cookie if successful.

En d'autres termes, cette librairie n'est pas faite pour s'authentifier et stocker notre token. Pour cette étape, il vaut mieux privilégier l'utilisation du module Http basique livré avec Angular2.

II-C - Mise en place d'un système de routing

Nous allons maintenant nous occuper du routing. En effet, nous n'avons pour l'instant aucun moyen d'afficher le contenu qui se trouve dans les fichiers HTML de nos composants. Pour configurer le routing de votre application, c'est très simple. Nous allons créer un fichier app.routing.ts à la racine de notre application et indiquer les trois routes de nos composants principaux, ainsi que la route de redirection au cas où nous entrerions une URL qui ne correspond à aucune route :

app.routing.ts

1. import { Routes, RouterModule } from '@angular/router';

3. import { HomepageComponent } from './homepage/homepage.component';

4. import { AuthenticationComponent } from './authentication/authentication.component';

5. import { PostComponent } from './post/post.component';

6. import { AuthGuard } from './_guard/index';

8. const APP_ROUTES: Routes = [

9. {

10. path: '',

11. component: HomepageComponent

12. },

13. {

14. path: 'login',

15. component: AuthenticationComponent

16. },

17. {

18. path: 'post',

19. component: PostComponent,

20. canActivate: [AuthGuard]

21. },

22. { path: '**', redirectTo: '' }

23. ];

25. export const Routing = RouterModule.forRoot(APP_ROUTES);

II-D - Protéger les routes authentifiées avec AuthGuard

Pour finir, nous allons mettre en place un système permettant de protéger nos routes sécurisées via Guard. Créons un dossier « _guard » dans le dossier « app » contenant deux fichiers :

_guard/auth.guard.ts

1. import { Injectable } from '@angular/core';

2. import { Router } from '@angular/router';

3. import { CanActivate } from '@angular/router';

4. import { AuthenticationService } from '../authentication/authentication.service';

6. @Injectable()

7. export class AuthGuard implements CanActivate {

9. constructor(private authentication: AuthenticationService, private router: Router) {}

11. canActivate() {

12. if(this.authentication.loggedIn()) {

13. return true;

14. } else {

15. this.router.navigate(['login']);

17. return false;

18. }

19. }

20. }

_guard/index.ts

export * from './auth.guard';

Nous importerons le fichier index.ts dans notre fichier app.module.ts et nous déclarerons AuthGuard en tant que provider, puis nous l'importerons également dans notre fichier app.routing.ts pour protéger notre route « post » via la propriété « canActivate ».

II-E - Authentifier ses requêtes avec Angular2-jwt

Pourquoi ai-je choisi d'utiliser cette bibliothèque ? Et bien tout d'abord pour l'essayer. Et puis parce qu'elle va nous simplifier la vie. Enfin du moins l'envoi des requêtes vers notre API dans un premier temps.

En effet, ce wrapper du module Http natif d'Angular2 permet d'inclure directement l'id_token contenu dans le localStorage, dans un header « authorization » compatible avec le format utilisé par notre LexikJwtAuthenticationBundle.

Le deuxième avantage de cette librairie est qu'elle va automatiquement vérifier si le token est valide. Ce qui n'est pas du tout négligeable.

Installons angular2-jwt via npm à la racine du projet en lançant cette commande :

npm install angular2-jwt

Maintenant que nous avons structuré notre application, il nous faut mettre à jour notre composant « app » ainsi que notre fichier app.module.ts avec les imports nécessaires :

app.component.ts

1. import { Component } from '@angular/core';

2. import { Router } from '@angular/router';

4. import { AuthenticationService } from './authentication/authentication.service';

6. @Component({

7. selector: 'app-root',

8. templateUrl: './app.component.html'

9. })

10. export class AppComponent {

12. constructor(private authenticationService: AuthenticationService, private router: Router) {}

14. hasAuthToken() {

15. return localStorage.getItem('id_token') !== null;

16. }

18. logout() {

19. this.authenticationService.logout();

20. this.router.navigate(['home']);

21. }

22. }

app.component.html

1. <nav class="navbar navbar-inverse navbar-fixed-top">

2. <div class="container">

3. <div class="navbar-header">

4. <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">

5. <span class="sr-only">Toggle navigation</span>

6. <span class="icon-bar"></span>

7. <span class="icon-bar"></span>

8. <span class="icon-bar"></span>

9. </button>

10. <img class="pull-left logo" src="../assets/images/ng-xs.png" alt="Angular2">

11. <a class="navbar-brand" [routerLink]="['']">Api Lab</a>

12. </div>

13. <div id="navbar" class="collapse navbar-collapse">

14. <ul class="nav navbar-nav">

15. <li><a [routerLink]="['']">Home</a></li>

16. <li><a [routerLink]="['post']">Posts</a></li>

17. </ul>

18. <ul class="nav navbar-nav pull-right">

19. <li *ngIf="!hasAuthToken()"><a [routerLink]="['login']">Login</a></li>

20. <li *ngIf="hasAuthToken()"><a (click)="logout()" href="#">Logout</a></li>

21. </ul>

22. </div>

23. </div>

24. </nav>

26. <router-outlet></router-outlet>

app.module.ts

1. import { BrowserModule } from '@angular/platform-browser';

2. import { NgModule } from '@angular/core';

3. import { ReactiveFormsModule } from '@angular/forms';

4. import { Http, HttpModule, RequestOptions } from '@angular/http';

5. import { AuthHttp, AuthConfig } from 'angular2-jwt';

7. import { AppComponent } from './app.component';

8. import { Routing } from './app.routing';

9. import { AuthGuard } from './_guard/index';

11. import { AuthenticationComponent } from './authentication/authentication.component';

12. import { AuthenticationService } from './authentication/authentication.service';

13. import { HomepageComponent } from './homepage/homepage.component';

14. import { PostComponent } from './post/post.component';

15. import { PostRepository } from './post/post-repository.service';

17. export function authHttpServiceFactory(http: Http, options: RequestOptions) {

18. return new AuthHttp( new AuthConfig({}), http, options);

19. }

21. @NgModule({

22. declarations: [

23. AppComponent,

24. AuthenticationComponent,

25. HomepageComponent,

26. PostComponent

27. ],

28. imports: [

29. BrowserModule,

30. ReactiveFormsModule,

31. HttpModule,

32. Routing

33. ],

34. providers: [

35. {

36. provide: AuthHttp,

37. useFactory: authHttpServiceFactory,

38. deps: [ Http, RequestOptions ]

39. },

40. AuthGuard,

41. AuthenticationService,

42. PostRepository

43. ],

44. bootstrap: [AppComponent]

45. })

46. export class AppModule { }

J'ai délibérément inclus la factory authHttpServiceFactory directement dans ce fichier contrairement à ce que dit la documentation, car il y a un bug connu de la team Angular2-jwt lors de la compilation qui peut être résolu de cette manière.

Enfin, pour finaliser notre application, il ne reste plus qu'à lui appliquer un peu de style :

index.html

1. <!doctype html>

2. <html>

3. <head>

4. <meta charset="utf-8">

5. <title>Api Lab</title>

6. <base href="/">

8. <meta name="viewport" content="width=device-width, initial-scale=1">

10. <link rel="icon" type="image/x-icon" href="favicon.ico">

11. <script src="https://code.jquery.com/jquery-3.1.1.min.js"

12. integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="

13. crossorigin="anonymous">

14. </script>

15. <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">

16. </head>

17. <body>

18. <div class="container">

19. <app-root><div class="text-center loading">Loading...</div></app-root>

20. </div>

22. <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

23. </body>

24. </html>

style.css

1. /* You can add global styles to this file, and also import other style files */

2. body {

3. padding-top: 50px;

4. }

6. .loading {

7. font-weight: bold;

8. margin-top: 150px;

9. }

11. .logo {

12. margin: 5px;

13. }

Et voilà le travail !

III - Conclusion

Pour conclure, je dirais que cette expérience s'est avérée très enrichissante. Angular2 propose une nouvelle perception du framework front-end en comparaison à la première version. Une façon de développer qui se rapproche plus de la programmation orientée objet et rappelle étrangement les frameworks PHP au niveau de la structure des fichiers. Enfin le CLI, bien que toujours en version bêta, reste un outil très pratique dans la lignée de la console de Symfony. Une très bonne première expérience.

IV - Remerciements

Nous remercions Elevenlabs qui nous a autorisés à publier ce tutoriel.

Nous tenons également à remercier Winjerome pour la mise au gabarit et Ced pour la correction orthographique.