diff --git a/angular.json b/angular.json index 571e72c..6a2e7da 100644 --- a/angular.json +++ b/angular.json @@ -21,8 +21,7 @@ "aot": true, "assets": [ "src/favicon.ico", - "src/assets", - "src/silent-refresh.html" + "src/assets" ], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index f6bbc70..a792cff 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -5,11 +5,12 @@ import { Routes } from '@angular/router'; import { HomeComponent } from './home/'; -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from '@shared/auth/auth.guard'; import { paths_collaborateurs, paths_demandes_delegation, paths_demandes_formation, paths_ep, paths_saisie_ep, paths_formation, paths_home, paths_referents } from '@shared/utils/paths'; +import { Role } from '@shared/utils/roles'; /** * L'ensemble des routes du client Angular @@ -26,6 +27,7 @@ const routes: Routes = [ path: paths_home.path, component: HomeComponent, canActivate: [AuthGuard], + data: { roles: [Role.assistante, Role.commercial, Role.rh, Role.collaborateur] }, pathMatch: 'full' }, //chargement des chemins du module collaborateur à partir du routing de ce module diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 71b1ef1..565f6bb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -22,7 +22,7 @@ import { DemandesFormationModule } from './demandes-formation'; import { DemandesDelegationModule } from './demandes-delegation'; import { EpSaisieModule } from "./ep-saisie"; import { EpModule } from "./ep" -import { AuthModule } from './auth/auth.module'; +import { AuthModule } from '@shared/auth/auth.module'; diff --git a/src/app/auth/auth.guard.ts b/src/app/auth/auth.guard.ts deleted file mode 100644 index 03539d1..0000000 --- a/src/app/auth/auth.guard.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; - -import { AuthService } from './auth.service'; - -/** - * Guard permettant de gérer les autorisations au niveau des routes. - */ -@Injectable() -export class AuthGuard implements CanActivate { - constructor(private authService: AuthService) { } - - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.authService.canActivateProtectedRoutes$.pipe(tap(isLoggin => this.login(isLoggin))); - } - - /** - * Affiche la page de connexion si l'utilisateur n'est pas connecté. - * @param isLoggin Booléen permettant de savoir si l'utilisateur est connecté ou non - */ - private login(isLoggin: boolean) { - if (!isLoggin) { - this.authService.login(); - } - } -} diff --git a/src/app/collaborateurs/collaborateurs.routing.module.ts b/src/app/collaborateurs/collaborateurs.routing.module.ts index 6296c3b..8c2eda0 100644 --- a/src/app/collaborateurs/collaborateurs.routing.module.ts +++ b/src/app/collaborateurs/collaborateurs.routing.module.ts @@ -9,12 +9,21 @@ import { EditEvaluationComponent } from './formations-collaborateur/edit-evaluat import { paths_collaborateurs } from '@shared/utils/paths'; -import { AuthGuard } from 'app/auth/auth.guard'; +import { AuthGuard } from '@shared/auth/auth.guard'; +import { Role } from '@shared/utils/roles'; + + /** * Routes du module collaborateur */ const routes: Routes = [ - { path:'', component: CollaborateursComponent, pathMatch: 'full', canActivate: [AuthGuard] }, + { + path:'', + component: CollaborateursComponent, + canActivate: [AuthGuard], + data: { roles: [Role.assistante, Role.commercial, Role.rh] }, + pathMatch: 'full' + }, { path:paths_collaborateurs.formations, component: FormationsCollaboateurComponent, canActivate: [AuthGuard] }, { path:paths_collaborateurs.evaluation, component: EvaluationComponent, canActivate: [AuthGuard] }, { path:paths_collaborateurs.edit, component: EditEvaluationComponent, canActivate: [AuthGuard] }, diff --git a/src/app/demandes-delegation/demandes-delegation.routing.module.ts b/src/app/demandes-delegation/demandes-delegation.routing.module.ts index d1009bb..41b0340 100644 --- a/src/app/demandes-delegation/demandes-delegation.routing.module.ts +++ b/src/app/demandes-delegation/demandes-delegation.routing.module.ts @@ -5,7 +5,7 @@ import { Routes, RouterModule } from '@angular/router'; import { DemandesDelegationComponent } from "./demandes-delegation.component"; import { DemandeDelegationComponent } from "./details-demande-delegation/demande-delegation.component"; -import { AuthGuard } from 'app/auth/auth.guard'; +import { AuthGuard } from '@shared/auth/auth.guard'; import { paths_demandes_delegation } from "@shared/utils/paths"; diff --git a/src/app/demandes-formation/demandes-formation.routing.module.ts b/src/app/demandes-formation/demandes-formation.routing.module.ts index 801f8a2..d422b58 100644 --- a/src/app/demandes-formation/demandes-formation.routing.module.ts +++ b/src/app/demandes-formation/demandes-formation.routing.module.ts @@ -6,7 +6,7 @@ import { DemandesFormationComponent } from "./demandes-formation.component"; import { DemandeFormationComponent } from "./details-demande-formation/demande-formation.component"; import { NewDemandeFormationComponent } from "./new-demande-formation/new-demande-formation.component"; -import { AuthGuard } from 'app/auth/auth.guard'; +import { AuthGuard } from '@shared/auth/auth.guard'; import { paths_demandes_formation } from "@shared/utils/paths"; diff --git a/src/app/ep-saisie/ep-saisie.routing.module.ts b/src/app/ep-saisie/ep-saisie.routing.module.ts index f509262..c8bd5c3 100644 --- a/src/app/ep-saisie/ep-saisie.routing.module.ts +++ b/src/app/ep-saisie/ep-saisie.routing.module.ts @@ -7,7 +7,7 @@ import { EpsSaisieComponent } from "./eps-saisie/eps-saisie.component"; import { EpaSaisieComponent } from "./epa-saisie/epa-saisie.component"; import { EpaSixAnsSaisieComponent } from "./epa-six-ans-saisie/epa-six-ans-saisie.component"; -import { AuthGuard } from 'app/auth/auth.guard'; +import { AuthGuard } from '@shared/auth/auth.guard'; import { paths_saisie_ep } from "@shared/utils/paths"; diff --git a/src/app/ep/ep.routing.module.ts b/src/app/ep/ep.routing.module.ts index 80b51bd..df80f4e 100644 --- a/src/app/ep/ep.routing.module.ts +++ b/src/app/ep/ep.routing.module.ts @@ -18,7 +18,7 @@ import { EpCommentaireAssistantComponent } from "./ep-commentaire-assistant/ep-c import { EpCommentaireReferentComponent } from "./ep-commentaire-referent/ep-commentaire-referent.component"; import { NewParticipantComponent } from "./ep-participants/new-participant/new-participant.component"; -import { AuthGuard } from 'app/auth/auth.guard'; +import { AuthGuard } from '@shared/auth/auth.guard'; import { paths_ep } from "@shared/utils/paths"; /** diff --git a/src/app/formations/formations.routing.module.ts b/src/app/formations/formations.routing.module.ts index 7bd2c64..9ba3e8e 100644 --- a/src/app/formations/formations.routing.module.ts +++ b/src/app/formations/formations.routing.module.ts @@ -7,7 +7,7 @@ import { FormationComponent } from "./details-formation/formation.component"; import { NewFormationComponent } from "./new-formation/new-formation.component"; import { EditFormationComponent } from "./edit-formation/edit-formation.component"; -import { AuthGuard } from 'app/auth/auth.guard'; +import { AuthGuard } from '@shared/auth/auth.guard'; import { paths_formation } from "@shared/utils/paths"; /** diff --git a/src/app/home/home-assistante/home-assistante.component.ts b/src/app/home/home-assistante/home-assistante.component.ts index bad3792..9f2aa73 100644 --- a/src/app/home/home-assistante/home-assistante.component.ts +++ b/src/app/home/home-assistante/home-assistante.component.ts @@ -8,7 +8,7 @@ import {MatSort} from '@angular/material/sort'; import { EpInformationDTO, CollaborateurDTO } from "@shared/api-swagger/model/models"; import { EpService } from "@shared/api-swagger/api/api"; -import { AuthService } from 'app/auth/auth.service'; +import { AuthService } from '@shared/auth/auth.service'; /** @@ -96,7 +96,7 @@ export class HomeAssistanteComponent implements OnInit, AfterViewInit { */ chargement = true; - constructor(public authService : AuthService, private service:EpService) { + constructor(private service:EpService) { } /** diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 08f6446..94936ff 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; -import { AuthService } from 'app/auth/auth.service'; +import { AuthService } from '@shared/auth/auth.service'; import { Role } from '@shared/utils/roles'; diff --git a/src/app/referents/referents.routing.module.ts b/src/app/referents/referents.routing.module.ts index 3f59cc7..35af0e6 100644 --- a/src/app/referents/referents.routing.module.ts +++ b/src/app/referents/referents.routing.module.ts @@ -7,7 +7,7 @@ import { DetailsReferentComponent } from "./details-referent/details-referent.co import { paths_referents } from "@shared/utils/paths"; -import { AuthGuard } from 'app/auth/auth.guard'; +import { AuthGuard } from '@shared/auth/auth.guard'; /** * Routes du module référents diff --git a/src/app/auth/auth-config.ts b/src/app/shared/auth/auth-config.ts similarity index 100% rename from src/app/auth/auth-config.ts rename to src/app/shared/auth/auth-config.ts diff --git a/src/app/auth/auth-module-config.ts b/src/app/shared/auth/auth-module-config.ts similarity index 100% rename from src/app/auth/auth-module-config.ts rename to src/app/shared/auth/auth-module-config.ts diff --git a/src/app/shared/auth/auth.guard.ts b/src/app/shared/auth/auth.guard.ts new file mode 100644 index 0000000..80171b3 --- /dev/null +++ b/src/app/shared/auth/auth.guard.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, Router } from '@angular/router'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +import { AuthService } from './auth.service'; + +/** + * Guard permettant de gérer les autorisations au niveau des routes. + */ +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private authService: AuthService, private router: Router) { } + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.authService.canActivateProtectedRoutes$.pipe(tap(isLoggin => this.login(isLoggin,route))); + } + + /** + * Vérifie si l'utilisateur est connecté et si l'utilisateur est autorisé à accéder à la route. + * @param isLoggin Booléen permettant de savoir si l'utilisateur est connecté ou non + * @param route Route à laquelle l'utilisateur souhaite accéder + */ + private login(isLoggin: boolean, route: ActivatedRouteSnapshot): boolean { + if (!isLoggin) { + this.authService.login(); + } + + const firstRole = this.authService.firstRole; + + if (firstRole) { + // Vérifie si la route est restreinte par des rôles + if (route.data.roles && route.data.roles.indexOf(firstRole) === -1) { + + // l'utisateur n'est pas autorisé alors on le redirige vers la page d'accueil + this.router.navigate(['/']); + + return false; + } + + // L'utilisateur est autorisé + return true; + } + } +} diff --git a/src/app/auth/auth.module.ts b/src/app/shared/auth/auth.module.ts similarity index 78% rename from src/app/auth/auth.module.ts rename to src/app/shared/auth/auth.module.ts index 3a26511..a8d5b58 100644 --- a/src/app/auth/auth.module.ts +++ b/src/app/shared/auth/auth.module.ts @@ -7,15 +7,15 @@ import { authModuleConfig } from './auth-module-config'; import { AuthService } from './auth.service'; /** - * Nous avons besoin d'une usine de stockage car le localStorage - * n'est pas disponible au moment de la compilation de l'AOT. (Ahead-of-time) + * Utilisation du localstorage pour stocker le token + * Voir: https://manfredsteyer.github.io/angular-oauth2-oidc/docs/additional-documentation/configure-custom-oauthstorage.html */ export function storageFactory(): OAuthStorage { return localStorage; } /** - * Exécute la méthode qui permet d'afficher la page de connexion. + * Fonction permettant d'appeler la méthode qui va démarrer la séquence de connexion initiale * @param authService Service d'authentification */ export function init_app(authService: AuthService) { @@ -44,8 +44,8 @@ export class AuthModule { { provide: OAuthModuleConfig, useValue: authModuleConfig }, { provide: OAuthStorage, useFactory: storageFactory }, { - provide: APP_INITIALIZER, - useFactory: init_app, // Affiche la page de connexion au démarrage de l'application + provide: APP_INITIALIZER, // Gestion de l'authentification au démarrage de l'application + useFactory: init_app, deps: [ AuthService ], multi: true } diff --git a/src/app/auth/auth.service.spec.ts b/src/app/shared/auth/auth.service.spec.ts similarity index 100% rename from src/app/auth/auth.service.spec.ts rename to src/app/shared/auth/auth.service.spec.ts diff --git a/src/app/auth/auth.service.ts b/src/app/shared/auth/auth.service.ts similarity index 61% rename from src/app/auth/auth.service.ts rename to src/app/shared/auth/auth.service.ts index f32ee00..71d7e9e 100644 --- a/src/app/auth/auth.service.ts +++ b/src/app/shared/auth/auth.service.ts @@ -70,7 +70,7 @@ export class AuthService { return; } - console.warn("Changements remarqués dans l'access_token (très probablement depuis un autre onglet). Mise à jour de l'observable isAuthenticated.") ; + //console.warn("Changements remarqués dans l'access_token (très probablement depuis un autre onglet). Mise à jour de l'observable isAuthenticated.") ; this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken()); // Si l'acces_token n'est pas valide, on redirige l'utilisateur vers la page de connexion @@ -103,78 +103,29 @@ export class AuthService { * Lance la séquence de connexion initiale. */ public runInitialLoginSequence(): Promise { - if (location.hash) { - console.log("Fragment de hachage rencontré, tracé sous forme de tableau...") ; - console.table(location.hash.substr(1).split('&').map(kvp => kvp.split('='))); - } - - // 0. CONFIGURATION DU CHARGEMENT: - // Il faut d'abord vérifier comment le serveur Keycloak est actuellement configuré: - return this.oauthService.loadDiscoveryDocument() - - // 1. HASH LOGIN: - // Essaye de se connecter via le fragment de hachage - // après avoir été redirigé depuis le serveur Keycloak depuis initImplicitFlow : - .then(() => this.oauthService.tryLogin()) - + + // Charge la configuration de Keycloak et tente de se connecter + return this.oauthService.loadDiscoveryDocumentAndTryLogin() .then(() => { + + // Si l'acces token est valide alors tout est ok if (this.oauthService.hasValidAccessToken()) { return Promise.resolve(); } - // 2. SILENT LOGIN: - // Effectue un rafraîchissement silencieux pour le flux implicite. - // Utilise cette méthode pour obtenir des nouveaux tokens quand/avant que les tokens existants expirent. - return this.oauthService.silentRefresh() - .then(() => Promise.resolve()) - .catch(result => { - // Sous-ensemble de situations de https://openid.net/specs/openid-connect-core-1_0.html#AuthError - // Seulement celles où il est raisonnablement certain que l'envoi de l'utilisateur au serveur Keycloak est utile. - const errorResponsesRequiringUserInteraction = [ - 'interaction_required', - 'login_required', - 'account_selection_required', - 'consent_required', - ]; - - if (result - && result.reason - && errorResponsesRequiringUserInteraction.indexOf(result.reason.error) >= 0) { - - // 3. DEMANDE DE CONNEXION: - // A ce stade, nous savons avec certitude que nous devons demander à - // l'utilisateur de se connecter, nous le redirigeons donc vers la page de connexion. - - // Activez cette option pour TOUJOURS forcer un utilisateur à se connecter. - this.login(); - - // Afficher un mesage d'avertissement: - //console.warn("Une interaction de l'utilisateur est nécessaire pour se connecter, nous allons attendre que l'utilisateur se connecte manuellement."); - - return Promise.resolve(); - } - - return Promise.reject(result); - }); - }) + // Renvoie l'utilisateur vers la page de connexion + this.navigateToLoginPage(); + // Afficher un mesage d'avertissement: + //console.warn("Une interaction de l'utilisateur est nécessaire pour se connecter, l'utilisateur doit se connecter manuellement."); + + return Promise.reject(); + }) .then(() => { - // Met à jour l'observable isDoneLoadingSubject + // Met à jour de l'observable isDoneLoadingSubject this.isDoneLoadingSubject$.next(true); - - // Vérifie les valeurs "undefined" et "null" pour être sûr. - // Notre login actuel ne devrait jamais avoir cela, mais au cas - // où quelqu'un appellerait la méthode initImplicitFlow(undefined|null), cela pourrait arriver. - if (this.oauthService.state && this.oauthService.state !== 'undefined' && this.oauthService.state !== 'null') { - let stateUrl = this.oauthService.state; - if (stateUrl.startsWith('/') === false) { - stateUrl = decodeURIComponent(stateUrl); - } - console.log(`Il y a eu l'état de ${this.oauthService.state}, donc nous vous envoyons à ${stateUrl}`) ; - this.router.navigateByUrl(stateUrl); - } }) - .catch(() => this.isDoneLoadingSubject$.next(true)); // Met à jour l'observable isDoneLoadingSubject + .catch(() => this.isDoneLoadingSubject$.next(true)); // Met à jour l'observable isDoneLoadingSubject même si il y a une erreur } /** @@ -182,9 +133,7 @@ export class AuthService { * @param targetUrl Url de destination */ public login(targetUrl?: string) { - // Note : avant la version 9.1.0 de la librairie, il fallait - // passer le composant encodageURIC dans les arguments de la méthode. - this.oauthService.initLoginFlow(targetUrl || this.router.url); + this.oauthService.initCodeFlow(targetUrl || this.router.url); } /** @@ -207,7 +156,7 @@ export class AuthService { */ public get firstRole() { if(this.identityClaims != null) - return this.oauthService.getIdentityClaims()['roles'][0]; + return this.identityClaims['roles'][0]; else return; } @@ -217,7 +166,7 @@ export class AuthService { */ public get roles() { if(this.identityClaims != null) - return this.oauthService.getIdentityClaims()['roles']; + return this.identityClaims['roles']; else return; } @@ -227,14 +176,14 @@ export class AuthService { */ public get userInfo() { if(this.identityClaims != null) - return this.oauthService.getIdentityClaims()['name']; + return this.identityClaims['name']; else return; } - // Normalement, un service comme celui-ci ne les expose pas, - // mais pour le débogage, c'est logique. + // Normalement, ce genre de service ne les expose pas, + // mais pour le débogage, ça peut-être utile. /** * Access_token actuel. diff --git a/src/app/shared/nav-menu/nav-menu.component.ts b/src/app/shared/nav-menu/nav-menu.component.ts index e412255..09a1a0a 100644 --- a/src/app/shared/nav-menu/nav-menu.component.ts +++ b/src/app/shared/nav-menu/nav-menu.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { AuthService } from 'app/auth/auth.service'; +import { AuthService } from '@shared/auth/auth.service'; import { Role } from '@shared/utils/roles'; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 9531770..f325ab0 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -21,21 +21,19 @@ const keycloakConfig: AuthConfig = { //dummyClientSecret: 'f27746f4-e603-441e-a256-3ddd5b19ba54', dummyClientSecret: '201a3d53-7cd5-4613-bcb6-f2e98c1ba2ec', - responseType: 'code', - silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html' , + // To configure your solution for code flow + PKCE you have to set the responseType to code + responseType: 'code', // set the scope for the permissions the client should request // The first four are defined by OIDC. // Important: Request offline_access to get a refresh token // The api scope is a usecase specific one scope: 'openid profile email', - showDebugInformation: true, - useSilentRefresh: true, // Needed for Code Flow to suggest using iframe-based refreshes - silentRefreshTimeout: 5000, // For faster testing - timeoutFactor: 0.25, // For faster testing + useSilentRefresh: true, // Activate silent refresh for code flow + silentRefreshTimeout: 5000, + timeoutFactor: 0.25, sessionChecksEnabled: true, - clearHashAfterLogin: false, // https://github.com/manfredsteyer/angular-oauth2-oidc/issues/457#issuecomment-431807040, - nonceStateSeparator : 'semicolon' // Real semicolon gets mangled by IdentityServer's URI encoding + nonceStateSeparator : 'semicolon' // Real semicolon gets mangled by Keycloak's URI encoding }; export const environment = { diff --git a/src/silent-refresh.html b/src/silent-refresh.html deleted file mode 100644 index 6adbc4e..0000000 --- a/src/silent-refresh.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - -