Yanaël GRETTE 4 years ago
commit 1692adaf42
  1. 3
      angular.json
  2. 4
      src/app/app-routing.module.ts
  3. 2
      src/app/app.module.ts
  4. 28
      src/app/auth/auth.guard.ts
  5. 13
      src/app/collaborateurs/collaborateurs.routing.module.ts
  6. 2
      src/app/demandes-delegation/demandes-delegation.routing.module.ts
  7. 2
      src/app/demandes-formation/demandes-formation.routing.module.ts
  8. 2
      src/app/ep-saisie/ep-saisie.routing.module.ts
  9. 2
      src/app/ep/ep.routing.module.ts
  10. 2
      src/app/formations/formations.routing.module.ts
  11. 4
      src/app/home/home-assistante/home-assistante.component.ts
  12. 2
      src/app/home/home.component.ts
  13. 2
      src/app/referents/referents.routing.module.ts
  14. 0
      src/app/shared/auth/auth-config.ts
  15. 0
      src/app/shared/auth/auth-module-config.ts
  16. 45
      src/app/shared/auth/auth.guard.ts
  17. 10
      src/app/shared/auth/auth.module.ts
  18. 0
      src/app/shared/auth/auth.service.spec.ts
  19. 85
      src/app/shared/auth/auth.service.ts
  20. 2
      src/app/shared/nav-menu/nav-menu.component.ts
  21. 12
      src/environments/environment.ts
  22. 31
      src/silent-refresh.html

@ -21,8 +21,7 @@
"aot": true, "aot": true,
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets", "src/assets"
"src/silent-refresh.html"
], ],
"styles": [ "styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",

@ -5,11 +5,12 @@ import { Routes } from '@angular/router';
import { HomeComponent } from './home/'; 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, import { paths_collaborateurs, paths_demandes_delegation, paths_demandes_formation,
paths_ep, paths_saisie_ep, paths_formation, paths_home, paths_referents } from '@shared/utils/paths'; 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 * L'ensemble des routes du client Angular
@ -26,6 +27,7 @@ const routes: Routes = [
path: paths_home.path, path: paths_home.path,
component: HomeComponent, component: HomeComponent,
canActivate: [AuthGuard], canActivate: [AuthGuard],
data: { roles: [Role.assistante, Role.commercial, Role.rh, Role.collaborateur] },
pathMatch: 'full' pathMatch: 'full'
}, },
//chargement des chemins du module collaborateur à partir du routing de ce module //chargement des chemins du module collaborateur à partir du routing de ce module

@ -22,7 +22,7 @@ import { DemandesFormationModule } from './demandes-formation';
import { DemandesDelegationModule } from './demandes-delegation'; import { DemandesDelegationModule } from './demandes-delegation';
import { EpSaisieModule } from "./ep-saisie"; import { EpSaisieModule } from "./ep-saisie";
import { EpModule } from "./ep" import { EpModule } from "./ep"
import { AuthModule } from './auth/auth.module'; import { AuthModule } from '@shared/auth/auth.module';

@ -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<boolean> {
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();
}
}
}

@ -9,12 +9,21 @@ import { EditEvaluationComponent } from './formations-collaborateur/edit-evaluat
import { paths_collaborateurs } from '@shared/utils/paths'; 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 * Routes du module collaborateur
*/ */
const routes: Routes = [ 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.formations, component: FormationsCollaboateurComponent, canActivate: [AuthGuard] },
{ path:paths_collaborateurs.evaluation, component: EvaluationComponent, canActivate: [AuthGuard] }, { path:paths_collaborateurs.evaluation, component: EvaluationComponent, canActivate: [AuthGuard] },
{ path:paths_collaborateurs.edit, component: EditEvaluationComponent, canActivate: [AuthGuard] }, { path:paths_collaborateurs.edit, component: EditEvaluationComponent, canActivate: [AuthGuard] },

@ -5,7 +5,7 @@ import { Routes, RouterModule } from '@angular/router';
import { DemandesDelegationComponent } from "./demandes-delegation.component"; import { DemandesDelegationComponent } from "./demandes-delegation.component";
import { DemandeDelegationComponent } from "./details-demande-delegation/demande-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"; import { paths_demandes_delegation } from "@shared/utils/paths";

@ -6,7 +6,7 @@ import { DemandesFormationComponent } from "./demandes-formation.component";
import { DemandeFormationComponent } from "./details-demande-formation/demande-formation.component"; import { DemandeFormationComponent } from "./details-demande-formation/demande-formation.component";
import { NewDemandeFormationComponent } from "./new-demande-formation/new-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"; import { paths_demandes_formation } from "@shared/utils/paths";

@ -7,7 +7,7 @@ import { EpsSaisieComponent } from "./eps-saisie/eps-saisie.component";
import { EpaSaisieComponent } from "./epa-saisie/epa-saisie.component"; import { EpaSaisieComponent } from "./epa-saisie/epa-saisie.component";
import { EpaSixAnsSaisieComponent } from "./epa-six-ans-saisie/epa-six-ans-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"; import { paths_saisie_ep } from "@shared/utils/paths";

@ -18,7 +18,7 @@ import { EpCommentaireAssistantComponent } from "./ep-commentaire-assistant/ep-c
import { EpCommentaireReferentComponent } from "./ep-commentaire-referent/ep-commentaire-referent.component"; import { EpCommentaireReferentComponent } from "./ep-commentaire-referent/ep-commentaire-referent.component";
import { NewParticipantComponent } from "./ep-participants/new-participant/new-participant.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"; import { paths_ep } from "@shared/utils/paths";
/** /**

@ -7,7 +7,7 @@ import { FormationComponent } from "./details-formation/formation.component";
import { NewFormationComponent } from "./new-formation/new-formation.component"; import { NewFormationComponent } from "./new-formation/new-formation.component";
import { EditFormationComponent } from "./edit-formation/edit-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"; import { paths_formation } from "@shared/utils/paths";
/** /**

@ -8,7 +8,7 @@ import {MatSort} from '@angular/material/sort';
import { EpInformationDTO, CollaborateurDTO } from "@shared/api-swagger/model/models"; import { EpInformationDTO, CollaborateurDTO } from "@shared/api-swagger/model/models";
import { EpService } from "@shared/api-swagger/api/api"; 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; chargement = true;
constructor(public authService : AuthService, private service:EpService) { constructor(private service:EpService) {
} }
/** /**

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core'; 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'; import { Role } from '@shared/utils/roles';

@ -7,7 +7,7 @@ import { DetailsReferentComponent } from "./details-referent/details-referent.co
import { paths_referents } from "@shared/utils/paths"; 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 * Routes du module référents

@ -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<boolean> {
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;
}
}
}

@ -7,15 +7,15 @@ import { authModuleConfig } from './auth-module-config';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
/** /**
* Nous avons besoin d'une usine de stockage car le localStorage * Utilisation du localstorage pour stocker le token
* n'est pas disponible au moment de la compilation de l'AOT. (Ahead-of-time) * Voir: https://manfredsteyer.github.io/angular-oauth2-oidc/docs/additional-documentation/configure-custom-oauthstorage.html
*/ */
export function storageFactory(): OAuthStorage { export function storageFactory(): OAuthStorage {
return localStorage; 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 * @param authService Service d'authentification
*/ */
export function init_app(authService: AuthService) { export function init_app(authService: AuthService) {
@ -44,8 +44,8 @@ export class AuthModule {
{ provide: OAuthModuleConfig, useValue: authModuleConfig }, { provide: OAuthModuleConfig, useValue: authModuleConfig },
{ provide: OAuthStorage, useFactory: storageFactory }, { provide: OAuthStorage, useFactory: storageFactory },
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER, // Gestion de l'authentification au démarrage de l'application
useFactory: init_app, // Affiche la page de connexion au démarrage de l'application useFactory: init_app,
deps: [ AuthService ], deps: [ AuthService ],
multi: true multi: true
} }

@ -70,7 +70,7 @@ export class AuthService {
return; 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()); this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());
// Si l'acces_token n'est pas valide, on redirige l'utilisateur vers la page de connexion // 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. * Lance la séquence de connexion initiale.
*/ */
public runInitialLoginSequence(): Promise<void> { public runInitialLoginSequence(): Promise<void> {
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(() => { .then(() => {
// Si l'acces token est valide alors tout est ok
if (this.oauthService.hasValidAccessToken()) { if (this.oauthService.hasValidAccessToken()) {
return Promise.resolve(); return Promise.resolve();
} }
// 2. SILENT LOGIN: // Renvoie l'utilisateur vers la page de connexion
// Effectue un rafraîchissement silencieux pour le flux implicite. this.navigateToLoginPage();
// 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: // 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."); //console.warn("Une interaction de l'utilisateur est nécessaire pour se connecter, l'utilisateur doit se connecter manuellement.");
return Promise.resolve();
}
return Promise.reject(result); return Promise.reject();
});
}) })
.then(() => { .then(() => {
// Met à jour l'observable isDoneLoadingSubject // Met à jour de l'observable isDoneLoadingSubject
this.isDoneLoadingSubject$.next(true); 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 * @param targetUrl Url de destination
*/ */
public login(targetUrl?: string) { public login(targetUrl?: string) {
// Note : avant la version 9.1.0 de la librairie, il fallait this.oauthService.initCodeFlow(targetUrl || this.router.url);
// passer le composant encodageURIC dans les arguments de la méthode.
this.oauthService.initLoginFlow(targetUrl || this.router.url);
} }
/** /**
@ -207,7 +156,7 @@ export class AuthService {
*/ */
public get firstRole() { public get firstRole() {
if(this.identityClaims != null) if(this.identityClaims != null)
return this.oauthService.getIdentityClaims()['roles'][0]; return this.identityClaims['roles'][0];
else else
return; return;
} }
@ -217,7 +166,7 @@ export class AuthService {
*/ */
public get roles() { public get roles() {
if(this.identityClaims != null) if(this.identityClaims != null)
return this.oauthService.getIdentityClaims()['roles']; return this.identityClaims['roles'];
else else
return; return;
} }
@ -227,14 +176,14 @@ export class AuthService {
*/ */
public get userInfo() { public get userInfo() {
if(this.identityClaims != null) if(this.identityClaims != null)
return this.oauthService.getIdentityClaims()['name']; return this.identityClaims['name'];
else else
return; return;
} }
// Normalement, un service comme celui-ci ne les expose pas, // Normalement, ce genre de service ne les expose pas,
// mais pour le débogage, c'est logique. // mais pour le débogage, ça peut-être utile.
/** /**
* Access_token actuel. * Access_token actuel.

@ -1,5 +1,5 @@
import { Component } from '@angular/core'; 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'; import { Role } from '@shared/utils/roles';

@ -21,21 +21,19 @@ const keycloakConfig: AuthConfig = {
//dummyClientSecret: 'f27746f4-e603-441e-a256-3ddd5b19ba54', //dummyClientSecret: 'f27746f4-e603-441e-a256-3ddd5b19ba54',
dummyClientSecret: '201a3d53-7cd5-4613-bcb6-f2e98c1ba2ec', dummyClientSecret: '201a3d53-7cd5-4613-bcb6-f2e98c1ba2ec',
// To configure your solution for code flow + PKCE you have to set the responseType to code
responseType: 'code', responseType: 'code',
silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html' ,
// set the scope for the permissions the client should request // set the scope for the permissions the client should request
// The first four are defined by OIDC. // The first four are defined by OIDC.
// Important: Request offline_access to get a refresh token // Important: Request offline_access to get a refresh token
// The api scope is a usecase specific one // The api scope is a usecase specific one
scope: 'openid profile email', scope: 'openid profile email',
showDebugInformation: true, useSilentRefresh: true, // Activate silent refresh for code flow
useSilentRefresh: true, // Needed for Code Flow to suggest using iframe-based refreshes silentRefreshTimeout: 5000,
silentRefreshTimeout: 5000, // For faster testing timeoutFactor: 0.25,
timeoutFactor: 0.25, // For faster testing
sessionChecksEnabled: true, sessionChecksEnabled: true,
clearHashAfterLogin: false, // https://github.com/manfredsteyer/angular-oauth2-oidc/issues/457#issuecomment-431807040, nonceStateSeparator : 'semicolon' // Real semicolon gets mangled by Keycloak's URI encoding
nonceStateSeparator : 'semicolon' // Real semicolon gets mangled by IdentityServer's URI encoding
}; };
export const environment = { export const environment = {

@ -1,31 +0,0 @@
<html>
<body>
<script>
// Based on: https://manfredsteyer.github.io/angular-oauth2-oidc/docs/additional-documentation/silent-refresh.html
const checks = [/[\?|&|#]code=/, /[\?|&|#]error=/, /[\?|&|#]token=/, /[\?|&|#]id_token=/];
function isResponse(str) {
let count = 0;
if (!str) {
return false;
}
for (let i = 0; i < checks.length; i++) {
if (str.match(checks[i])) return true;
}
return false;
}
let message = isResponse(location.hash) ? location.hash : '#' + location.search;
console.log("Rafraîchissement silencieux de l'iframe affichée dans l'application parente, message: ", message);
(window.opener || window.parent).postMessage(message, location.origin);
</script>
</body>
</html>
Loading…
Cancel
Save