Django & DRF & Angular 101, partie 3.3 : authentification & accès guards
Tags: angular, typescript, material, django
Published: 2020-11-08 14:13:45

## Sommaire
  * [Introduction](#intro)
  * [Modèles](#modeles)
  * [Utilisateurs et rôles Django](#roles)
  * [Authentification & JWT](#authent)
    * [JWT](#jwt)
    * [Angular](#authent-angular)
  * [Guards](#guards)
  * [Modules](#modules)
    * [Gestion](#gestion)
    * [Consultation](#consultation)
  * [Conclusion et sources](#conclusion)

# Introduction <a name="intro"></a>

Nous verrons dans cette partie de la série Django & DRF & Angular :

* l'**authentification** Angular avec Django en utilisant un _token_ JWT avec les librairies drf-jwt côté django et @auth0/angular-jwt côté front
* l'utilisation des **_guards_** en lien avec les **rôles Django** : Mme. Michue qui souhaite consulter la bibliothèque de livres et **gest1** qui les gère ainsi que leurs auteurs
* d'un **module** pour les gestionnaires qui ne sera chargé que pour les gestionnaires 

# Modèles <a name="modeles"></a>

On va enrichir notre librairie, notamment sur les livres :

* ajouter un résumé
* modifier la contrainte de suppression `on_delete` sur l'auteur : en effet, actuellement, la suppression d'un autre supprime également...tous ses livres, le `models.CASCADE` provoque cette action. On va, à la place, mettre NULL à la `ForeignKey` lorsqu'un auteur est supprimé, on aura des livres orphelins mais au moins on les conserve !

```python
class Book(TimeStampedModel):
    name = models.CharField(max_length=100, null=False, blank=False, db_index=True)
    summary = models.TextField(null=True, blank=True)
    nb_pages = models.IntegerField()

    author = models.ForeignKey(Author,
                               related_name='books',
                               null=True,
                               on_delete=models.SET_NULL)

    enabled = models.BooleanField(default=True, help_text='disponible ou non')

    def __str__(self):
        return '{} : {}'.format(self.name, self.author)
``` 

le [`on_delete`](https://docs.djangoproject.com/en/3.0/ref/models/fields/#django.db.models.ForeignKey.on_delete) peut contenir les valeurs suivantes :

* SET_NULL : remet à NULL la valeur (au lieu de la clé vers l'entité)
* CASCADE : supprime l'entité courant si la clé étrangère est supprimée
* PROTECT : ne fait rien, contrainte forte, des exceptions se léveront dans l'impossibilité telles ou telles entités
* SET_DEFAULT : prend la valeur par défaut
: SET : une autre entité pour remplacer

On effectue un `python3 manage.py makemigrations --name 'book_upgrade_s
ummary_author' && python3 manage.py migrate
` pour appliquer les modifications.

# Utilisateurs et rôles : utilisateurs et groupes Django <a name="roles"></a>

Il s'agir de créer un **groupe** **gestionnaire** ayant les **permissions** suffisantes pour créer / modifier / supprimer / consulter un livre ou auteur, ce groupe aura un rôle "gestionnaire" qui lui permettra de manipuler les livres et auteurs.

Par soucis d'alléger le tutoriel, nous passons par l'**Admin Django** pour gérer toute ceci, il faudrait bien entendu un _backoffice_ sous ...Angular pour ce type de gestion d'utilisateurs, groupes et permissions.

Sous l'admin (plateform/admin), dans les groupes, ajouter un groupe "**gestionnaire**" avec les droits sur les livres et auteurs

Groupes : Ajouter

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/k0shjli5a88sdlwzvmt5.png)

Paramétrage du groupe **gestionnaire** avec les **permissions** "CRUD" sur Book et Author

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/srhj7m783bvpzvezd5yp.png)

puis un utilisateur **gest1** sur ce nouveau groupe ayant les permissions

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/75ntfdbx4yuj8vy7muye.png)

créer aussi un utilisateur **michue** pour notre dame qui souhaite juste consulter voire à termes commenter, sans aucun groupe et permissions.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/19l7jvsygsmtgtem0w21.png)

# Authentification <a name="authent"></a>

La méthode d'authentification qui s'est imposée depuis quelques années avec les SPA ou les applications mobiles est basée sur le token **[JWT](https://jwt.io/)**.

On se basera sur cette méthode côté Angular pour l'obtenir et l'utiliser dans les appels API dont on a besoin.

## JWT <a name="jwt"></a>

L'objectif est d'utilisé l'API d'authentification pour obtenir un jeton [JWT](https://jwt.io/).

 JWT étant un [standard du W3C](https://tools.ietf.org/html/rfc7519), composé de **3 parties** : _header.payload.signature_ 
* le _header_ contient l'algorithme utilisé pour générer le token, 
* le _payload_ les méta données (comme la date d'expiration **exp**) et données ajoutées (**username**) 
* la _signature_ pour vérifier le token (qu'il n'a pas été altéré et que c'est bien...le bon token généré, pour la génération le **SECRET_KEY** du settings Django est utilisé par défaut).

L'extension Django / DRF utilisée est : [drf-jwt](https://pypi.org/project/drf-jwt/) ajouté dans les **requirements.txt**

Par exemple, à partir de http://plateform, étendre la partie **api_token_auth** puis saisir le login et mot de passe défini pour **gest1**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/fycdbftvh0x5coryzxcb.png)

On aura comme résultat, sous forme de JSON, le token JWT ainsi que quelques informations supplémentaire sur l'utilisateur **gest1**

```json
{
  "pk": 1587895563,
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imdlc3QxIiwiaWF0IjoxNTg3ODk1NTYzLCJleHAiOjE1ODc5MDk5NjMsInVzZXJfaWQiOjMsIm9yaWdfaWF0IjoxNTg3ODk1NTYzfQ.zBPgmxxbTKbg6_2cFEdOTg1G3JOk0QIhRlEam8EU7AA",
  "user": {
    "id": 3,
    "username": "gest1",
    "email": "john.do@domain.com",
    "is_superuser": false,
    "is_staff": false
  },
  "refresh_url": "/api-token-refresh/",
  "token_verify_url": "/api-token-verify/"
}
```

Le _flow_ d'une authentification se présente ainsi :

* navigateur : login 
* POST sur api-token-auth du login/passe
* serveur : vérification du login/passe, génération du JWT
* navigateur : retour de la réponse et stockage du token si ok dans le localstorage
* tous les nouveaux appels aux APIs se voient ajouter dans leurs entêtes
 `Authorization: Bearer token` pour potentiellement détecter l'utilisateur côté Django

Pour rappel, une application orientée API est _stateless_ (sans état, revoir la partie consacrée sur [REST](https://dev.to/zorky/django-drf-101-partie-2-1gp5#archirest)), tout est porté par l'url / API. Ici, l'utilisateur authentifié est envoyé à chaque fois dans l'entête Authorization pour vérification côté serveur si on a besoin de l'utilisateur et/ou de ses permissions.

Egalement, le _token_ pourra être stocké côté application sur le navigateur, grâce au localStorage, la librairie **@auth0/angular-jwt** (ci-après) permet de préciser comment peut être stocké via une configuration dans le module principal AppModule, la fonction `tokenGetter()`

```typescript
export function tokenGetter(): string {
  return localStorage.getItem('token');
}

imports: [
    // ...
    JwtModule.forRoot({
      config: {
        tokenGetter,
        whitelistedDomains: ['localhost:4200', 'plateform'],
        blacklistedRoutes: [],
        skipWhenExpired: true,
      }
    }),
   // ...
]
```

Un schéma simple du flux (pris sur cet excellent [article](https://dzone.com/articles/cookies-vs-tokens-the-definitive-guide))

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/07ymt9fj30efph8mya27.png)

Une fois connecte **@auth0/angular-jwt** ajoutera automatiquement à tous les appels API l'entête authorization (via un HttpInterceptor), token qu'interprétera DRF pour déterminer si l'utilisateur a les droits grâce aux `permission_classes`.

En mode anonyme

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/9104ji09au6h3378zxmv.png)

En mode connecté avec le compte **gest1** gestionnaire

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/vk32ooy39exdlvpx6yvu.png)

## Angular authentification <a name="authent-angular"></a>

On utilisera le module [@auth0/angular-jwt](https://www.npmjs.com/package/@auth0/angular-jwt) côté front. Cette librairie fournit un service `JwtHelperService` qui nous aidera dans notre tâche, ainsi que du paramètrage pour retrouver le _token_ JWT, et l'injection automatique de l'entête `Authorization: Bearer token` dans les APIs (merci l'[HttpInterceptor ](https://angular.io/api/common/http/HttpInterceptor) d'Angular qu'utilise la librairie pour ça)

On va créer un service AuthentService qui effectuera le login et le logout, ainsi que quelques fonctions utilitaires (le visiteur était-il connecté ? obtenir le token, l'utilisateur, etc)

### Service AuthentService

L'objectif de ce service d'authentifier l'utilisateur et si le retour est bon, de stocker le **token** ainsi que l'utilisateur **user** en _localstorage_, @auth0/angular-jwt pourra ainsi retrouver ce token grâce à la fonction `tokenGetter` vu précédemment.

`userActivate$` nous servira à émettre ce nouveau connecté aux composants qui écoutent dessus et en ont besoin.

``` typescript

@Injectable({ providedIn: 'root' })
export class AuthService {
  private user: User;
  private url = `${environment.baseUrl}/api-token-auth/`;
  private userSource = new ReplaySubject<User>(1);
  userActivate$ = this.userSource.asObservable();

  constructor(private http: HttpClient,
              private jwtService: JwtHelperService) {
  }
  /**
   * Authentification /api-token-auth/
   * @param {UserAuthent} user : l'utilisateur a connecté
   * @return any dont token
   */ 
  logon(user: UserAuthent): Observable<any> {
    const headers = new HttpHeaders({
      'Content-Type': 'application/json', Accept: 'application/json'
    });
    return this.http
      .post(this.url, { username: user.username, password: user.password }, {headers})
      .pipe(map(dataJwt => this._authenticated(dataJwt)));
  }
  /**
   Déconnexion, on supprime tout ce qui est relatif au connecté et son 
   token 
  */
  logout() {
    localStorage.removeItem('token');
    localStorage.removeItem('user');
  }
  isAuthenticated() {
    return !this.jwtService.isTokenExpired();
  }
  /**
   * Stockage du token et de quelques informations user
   * @param data
   * @private
   */
  private _authenticated(data: any): User {
    localStorage.setItem('token', data.token);
    localStorage.setItem('user', JSON.stringify(data.user));
    this.userSource.next(data.user as User);
    return data;
  }
}
```

### Component bannière de connexion

On va avoir besoin d'un component pour la bannière de connexion, ce dernier, au clic du bouton appellera `logon()` du service AuthentService que l'on a créé, au retour si tout est bon, redirige vers la page d'accueil. En cas de connexion infructueuse, un message est affiché, sinon on le redirige sur la page d'accueil en tant que connecté.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rdnty66e5wadqz9r86h2.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/fesiu5sqx9myiflds7od.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/paxgfpn8lljw5uml8hoq.png)

```typescript
login() {
    this.loading = true;
    this.authService.logon(this.model)
      .pipe(finalize(() => this.loading = false))
      .subscribe(data => {
        if (data.token) {
          this.router.navigate(['/']);
          }
      },
      error => {
        this.loading = false;
        if (error instanceof HttpErrorResponse && error.status === 400) {
          const message =  error.error.non_field_errors[0];
          this.message = {
            message, label: '',
            color: 'red', icon: 'error'
          };
        } else {
          throw error;
        }
      }
    );
  }
```

# Guards <a name="guards"></a>

Les _guards_ vont nous permettre de restreindre l'accès aux routes suivant des règles de gestion sur des rôles, à l'aide de 1 ou 2 interfaces : [CanActivate](https://angular.io/api/router/CanActivate) (si le guard retourne true alors la route continue et charge le component) et potentiellement [CanLoad](https://angular.io/api/router/CanLoad) (est-ce que les enfants peuvent être chargés ? typiquement pour un module à charger). 

Nous allons définir un _guard_ GestionnaireGuard pour les gestionnaires : si tu es gestionnaire alors les components de gestion (édition livres / auteurs) pourront être chargés.

_Guard_ GestionnaireGuard : la fonction `isGestionnaire()` teste si le connecté a un rôle **gestionnaire**, elle renvoie un Observable `true`ou `false`selon le résultat du test. J'utilise le `UserGroupsService` en écoutant le Subject `connecte$`, à chaque connexion et donc le nouveau connecté, je peux tester son groupe d'appartenance.

```typescript
@Injectable({ providedIn: 'root'})
export class GestionnaireGuard implements CanLoad, CanActivate {
  constructor(private userGrpService: UserGroupsService) {
  }

  canLoad(route: Route): Observable<boolean> {
    return this.isGestionnaire();
  }
  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean>  {
    return this.isGestionnaire();
  }

  private isGestionnaire(): Observable<boolean> {
    return new Observable<boolean>(observer => {
      this.userGrpService.connecte$.subscribe(connecte => {
          observer.next(this._gestionnaire(connecte));
          observer.complete();
        }
      );
    });
  }
  private _gestionnaire(connecte: UserGroups) {
    return this.userGrpService.hasRole(connecte, roles.gestionnaire) ||
           this.userGrpService.hasRole(connecte, roles.admin);
  }
}

```

Une fois le _guard_ défini, il suffit de l'utiliser sur les routes qui nous intéressent, ici, celle de la gestion 

app.routing.ts, route pour la gestion

```typescript
// ...
const appRoutes: Routes = [
  {
    path: '',
    children: [
      {path: '', component: BooksListComponent},
      {path: 'gestion', loadChildren: () => import('./gestion/gestion.module').then(m => {
        return m.GestionModule; }), canLoad: [GestionnaireGuard], canActivate: [GestionnaireGuard]}
    ]
  },
  {path: '**', component: NotFoundComponent},
];
// ...
```
et côté module gestion, gestion-routing.module.ts qui contient les routes liés au module gestion, celui-ci sera chargé uniquement si une des routes est appelée (_lazy loading_ !)

```typescript
// ...
const routes: Routes = [
  {
   path: '',
   canLoad: [GestionnaireGuard], canActivate: [GestionnaireGuard],
   children: [
    {path: 'book/edit', component: BookEditComponent},
    {path: 'author/edit', component: AuthorEditComponent}
   ]
 }
];
// ...
```

Les guards permettent un contrôle assez fin des accès aux "vues" via les routes du connecté et de ses permissions.

# Modules <a name="modules"></a>

Les **modules** sous Angular permettent de ne charger les composants associés (et donc les fichiers) que lorsque la route qui pointe dessus est appelée, dans le jargon, on appelle cela du _[lazy loading](https://angular.io/guide/lazy-loading-ngmodules)_ (chargement différé) : économie lors du chargement de l'application pour les utilisateurs lambdas, on ne charge que les "pages" uniquement lors de l'accès à la gestion par exemple. Cela peut servir aussi à partager des composants vers d'autres modules ou applications.

Dans notre application, on aura les modules suivants :

* le module principal, déjà existant, chargé lors du lancement de l'application : **AppModule** (app.module.ts)
* un module **GestionModule** (gestion.module.ts) qui servira uniquement aux gestionnaires lors de leur accès à la gestion des livres et auteurs.
* un module transverse **CommonLibraryModule** qui contiendra potentiellement des composants partagés par les 2 modules précédents 

## Gestion <a name="gestion"></a> 

La gestion consiste pour un gestionnaire à :

* éditer les livres : ajout ou mise à jour (édition / suppression)
* éditer les auteurs : ajout ou mise à jour (édition / suppression)

seuls les gestionnaires peuvent avoir accès à ce type de gestion CRUD.

dans gestion, nous aurons un module **gestion.module.ts** pour la déclaration des composants utilisés dans la gestion de routes **gestion-routing.module.ts** du module Gestion.

Enfin, sur  les routes de l'application principale, il suffit de déclarer les routes vers la gestion (édition livres et auteurs) avec les __guards__ appropriés, nous aurons dans app.routing.ts, remarquez la partie **gestion** : dès qu'une route sur /gestion est appelée (/gestion/author/edit et /gestion/book/edit), le module gestion.module est chargée (ie : les sources et le chargement en mémoire)

```typescript
const appRoutes: Routes = [
  {
    path: '',
    children: [
      {path: '', component: BooksListComponent},
      {path: 'authent', component: LoginComponent},
      {path: 'login', component: LoginComponent},
      {path: 'books', component: BooksListComponent},
      {path: 'authors', component: AuthorsListComponent},
      {path: 'gestion', loadChildren: () => import('./gestion/gestion.module').then(m => {
        return m.GestionModule; })}
    ]
  },
  {path: '**', component: NotFoundComponent},
];

```

## Consultation <a name="consultation"></a>

Les composants de consultations (liste et affichage) font partis du module principal (app.module) car ils sont utilisés soit par les utilisateurs qui consultent mais aussi par les gestionnaire.

Nous aurons alors une déclaration simple pour les routes app.routing.ts, ici la route / et les routes books ou authors

```typescript
const appRoutes: Routes = [
  {
    path: '',
    children: [
      {path: '', component: BooksListComponent},
      {path: 'authent', component: LoginComponent},
      {path: 'login', component: LoginComponent},
      {path: 'books', component: BooksListComponent},
      {path: 'authors', component: AuthorsListComponent},
      {path: 'gestion', loadChildren: () => import('./gestion/gestion.module').then(m => {
        return m.GestionModule; })}
    ]
  },
  {path: '**', component: NotFoundComponent},
];
```
# Conclusion et sources <a name="conclusion"></a>

Dans cet article, nous avons vu

* le principe des rôles et permissions en Django
* l'usage du JWT 
* la restrictions d'accès à certaines routes / vues via les **guards**

Retrouvez les sources sur la branche https://github.com/zorky/library/tree/django-drf-angular-3.3 de cet article.

Dans un prochaine article, nous irons beaucoup plus loin sur le composant "data-table", en proposant des filtres colonnes ou cellules dynamiques, ainsi que certaines améliorations.