parent
26557be67a
commit
af75811436
@ -0,0 +1,23 @@ |
||||
import { AuthConfig } from 'angular-oauth2-oidc'; |
||||
import { environment } from '@env'; |
||||
|
||||
/** |
||||
* Configuration du serveur Keycloak. |
||||
*/ |
||||
export const authConfig: AuthConfig = { |
||||
issuer: environment.keycloakConfig.issuer, |
||||
clientId: environment.keycloakConfig.clientId,
|
||||
dummyClientSecret: environment.keycloakConfig.dummyClientSecret, |
||||
responseType: environment.keycloakConfig.responseType, |
||||
redirectUri: environment.keycloakConfig.redirectUri, |
||||
silentRefreshRedirectUri: environment.keycloakConfig.silentRefreshRedirectUri, |
||||
scope: environment.keycloakConfig.scope,
|
||||
useSilentRefresh: environment.keycloakConfig.useSilentRefresh,
|
||||
silentRefreshTimeout: environment.keycloakConfig.silentRefreshTimeout, |
||||
timeoutFactor: environment.keycloakConfig.timeoutFactor, |
||||
sessionChecksEnabled: environment.keycloakConfig.sessionChecksEnabled, |
||||
showDebugInformation: environment.keycloakConfig.showDebugInformation, |
||||
clearHashAfterLogin: environment.keycloakConfig.clearHashAfterLogin,
|
||||
nonceStateSeparator : environment.keycloakConfig.nonceStateSeparator
|
||||
}; |
||||
|
@ -0,0 +1,11 @@ |
||||
import { OAuthModuleConfig } from 'angular-oauth2-oidc'; |
||||
|
||||
/** |
||||
* Liste des urls qui sont autorisées à recevoir l'acces_token. |
||||
*/ |
||||
export const authModuleConfig: OAuthModuleConfig = { |
||||
resourceServer: { |
||||
allowedUrls: ['https://localhost:44393/api'], |
||||
sendAccessToken: true, |
||||
} |
||||
}; |
@ -0,0 +1,28 @@ |
||||
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(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,61 @@ |
||||
import { HttpClientModule } from '@angular/common/http'; |
||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf, APP_INITIALIZER } from '@angular/core'; |
||||
import { AuthConfig, OAuthModule, OAuthModuleConfig, OAuthStorage } from 'angular-oauth2-oidc'; |
||||
import { authConfig } from './auth-config'; |
||||
import { AuthGuard } from './auth.guard'; |
||||
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) |
||||
*/ |
||||
export function storageFactory(): OAuthStorage { |
||||
return localStorage; |
||||
} |
||||
|
||||
/** |
||||
* Exécute la méthode qui permet d'afficher la page de connexion. |
||||
* @param authService Service d'authentification |
||||
*/ |
||||
export function init_app(authService: AuthService) { |
||||
return () => authService.runInitialLoginSequence(); |
||||
} |
||||
|
||||
/** |
||||
* Module d'authentification. |
||||
*/ |
||||
@NgModule({ |
||||
imports: [ |
||||
HttpClientModule, |
||||
OAuthModule.forRoot(), |
||||
], |
||||
providers: [ |
||||
AuthService, |
||||
AuthGuard, |
||||
], |
||||
}) |
||||
export class AuthModule { |
||||
static forRoot(): ModuleWithProviders<AuthModule> { |
||||
return { |
||||
ngModule: AuthModule, |
||||
providers: [ |
||||
{ provide: AuthConfig, useValue: authConfig }, |
||||
{ 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
|
||||
deps: [ AuthService ], |
||||
multi: true |
||||
} |
||||
] |
||||
}; |
||||
} |
||||
|
||||
constructor (@Optional() @SkipSelf() parentModule: AuthModule) { |
||||
if (parentModule) { |
||||
throw new Error("AuthModule est déjà chargé. Importez-le dans uniquement l'AppModule."); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,6 @@ |
||||
|
||||
describe('AuthService', () => { |
||||
it('should pass', () => { |
||||
expect(true).toBeTruthy(); |
||||
}); |
||||
}); |
@ -0,0 +1,263 @@ |
||||
import { Injectable } from '@angular/core'; |
||||
import { Router } from '@angular/router'; |
||||
import { OAuthErrorEvent, OAuthService } from 'angular-oauth2-oidc'; |
||||
import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs'; |
||||
import { filter, map } from 'rxjs/operators'; |
||||
|
||||
@Injectable({ providedIn: 'root' }) |
||||
export class AuthService { |
||||
|
||||
private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false); |
||||
private isDoneLoadingSubject$ = new ReplaySubject<boolean>(); |
||||
|
||||
/** |
||||
* Observable permettant de savoir si l'utilisateur est authentifié. |
||||
*/ |
||||
public isAuthenticated$ = this.isAuthenticatedSubject$.asObservable(); |
||||
|
||||
/** |
||||
* Observable permettant de savoir si le chargement de la connexion initiale est terminé. |
||||
*/ |
||||
public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable(); |
||||
|
||||
/** |
||||
* Retourne "true" si et seulement si tous les appels asynchrones de la connexion initiale
|
||||
* sont terminés (ou comportent une erreur), et si l'utilisateur |
||||
* a fini par être authentifié. |
||||
* |
||||
* En résumé, il combine: |
||||
* |
||||
* - Le dernier état connu indiquant si l'utilisateur est autorisé |
||||
* - Si les appels ajax pour la connexion initiale ont tous été effectués |
||||
*/ |
||||
public canActivateProtectedRoutes$: Observable<boolean> = combineLatest([ |
||||
this.isAuthenticated$, this.isDoneLoading$ ]).pipe(map(values => values.every(b => b))); |
||||
|
||||
/** |
||||
* Navigue vers la page de connexion. |
||||
*/ |
||||
private navigateToLoginPage() { |
||||
// Pour naviguer vers un composant
|
||||
//this.router.navigateByUrl('/should-login');
|
||||
|
||||
// Pour naviguer vers la page de connexion du server Keycloak
|
||||
this.login(); |
||||
} |
||||
|
||||
/** |
||||
* Constructeur du service d'authentification. |
||||
* @param oauthService Service d'authentification de la librairie angular-oauth2-oidc |
||||
* @param router Service permettant de naviguer et de manipuler les urls |
||||
*/ |
||||
constructor(private oauthService: OAuthService, private router: Router) { |
||||
|
||||
/* |
||||
// Utile pour le débogage:
|
||||
this.oauthService.events.subscribe(event => { |
||||
if (event instanceof OAuthErrorEvent) { |
||||
console.error('OAuthErrorEvent Object:', event); |
||||
} else { |
||||
console.warn('OAuthEvent Object:', event); |
||||
} |
||||
}); |
||||
*/ |
||||
|
||||
// Ajout un évènement afin de gérer l'acces_token dans le cas où l'application est ouverte dans deux onglets différents.
|
||||
// TODO: Pour améliorer cette configuration. Voir: https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/issues/2
|
||||
window.addEventListener('storage', (event) => { |
||||
// La propriété "key" à la valeur "null" si l'événement a été causé par la méthode ".clear()".
|
||||
if (event.key !== 'access_token' && event.key !== null) { |
||||
return; |
||||
} |
||||
|
||||
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
|
||||
if (!this.oauthService.hasValidAccessToken()) { |
||||
this.navigateToLoginPage(); |
||||
} |
||||
}); |
||||
|
||||
// Met à jour l'observable lorsque l'access_token est valide
|
||||
this.oauthService.events |
||||
.subscribe(_ => { |
||||
this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken()); |
||||
}); |
||||
|
||||
// Charge le profil de l'utilisateur lorsque le token est reçu
|
||||
this.oauthService.events |
||||
.pipe(filter(e => ['token_received'].includes(e.type))) |
||||
.subscribe(e => this.oauthService.loadUserProfile()); |
||||
|
||||
// Redirige l'utilisateur vers la page de connexion lorsque la session est terminée ou lorsque qu'il y a une erreur
|
||||
this.oauthService.events |
||||
.pipe(filter(e => ['session_terminated', 'session_error'].includes(e.type))) |
||||
.subscribe(e => this.navigateToLoginPage()); |
||||
|
||||
// Mise en place d'un rafraîchissement silencieux lorsque le jeton est sur le point d'expirer
|
||||
this.oauthService.setupAutomaticSilentRefresh(); |
||||
} |
||||
|
||||
/** |
||||
* Lance la séquence de connexion initiale. |
||||
*/ |
||||
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()) |
||||
|
||||
.then(() => { |
||||
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); |
||||
}); |
||||
}) |
||||
|
||||
.then(() => { |
||||
// Met à jour 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
|
||||
} |
||||
|
||||
/** |
||||
* Affiche la page de connexion. |
||||
* @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); |
||||
} |
||||
|
||||
/** |
||||
* Supprime tous les tokens et déconnecte l'utilisateur. |
||||
*/ |
||||
public logout() { this.oauthService.logOut(); } |
||||
|
||||
/** |
||||
* Permet d'obtenir des nouveaux tokens quand/avant que les tokens existants expirent. |
||||
*/ |
||||
public refresh() { this.oauthService.silentRefresh(); } |
||||
|
||||
/** |
||||
* Vérifie si l'access_token est valide. |
||||
*/ |
||||
public hasValidToken() { return this.oauthService.hasValidAccessToken(); } |
||||
|
||||
/** |
||||
* Premier rôle dans la liste des rôles de l'utilisateur. |
||||
*/ |
||||
public get firstRole() { |
||||
if(this.identityClaims != null) |
||||
return this.oauthService.getIdentityClaims()['roles'][0];
|
||||
else |
||||
return; |
||||
} |
||||
|
||||
/** |
||||
* Liste des rôles de l'utilisateur. |
||||
*/ |
||||
public get roles() {
|
||||
if(this.identityClaims != null) |
||||
return this.oauthService.getIdentityClaims()['roles'];
|
||||
else |
||||
return; |
||||
} |
||||
|
||||
/** |
||||
* Nom et prénom de l'utilisateur. |
||||
*/ |
||||
public get userInfo() {
|
||||
if(this.identityClaims != null) |
||||
return this.oauthService.getIdentityClaims()['name'];
|
||||
else |
||||
return; |
||||
} |
||||
|
||||
|
||||
// Normalement, un service comme celui-ci ne les expose pas,
|
||||
// mais pour le débogage, c'est logique.
|
||||
|
||||
/** |
||||
* Access_token actuel. |
||||
*/ |
||||
public get accessToken() { return this.oauthService.getAccessToken(); } |
||||
|
||||
/** |
||||
* Refresh_Token actuel. |
||||
*/ |
||||
public get refreshToken() { return this.oauthService.getRefreshToken(); } |
||||
|
||||
/** |
||||
* Claims concernant l'utilisateur. |
||||
*/ |
||||
public get identityClaims() { return this.oauthService.getIdentityClaims(); } |
||||
|
||||
/** |
||||
* Id_token actuel. |
||||
*/ |
||||
public get idToken() { return this.oauthService.getIdToken(); } |
||||
|
||||
/** |
||||
* Url de déconnexion. |
||||
*/ |
||||
public get logoutUrl() { return this.oauthService.logoutUrl; } |
||||
} |
@ -1,40 +0,0 @@ |
||||
import { Injectable } from '@angular/core'; |
||||
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; |
||||
import { KeycloakService, KeycloakAuthGuard } from 'keycloak-angular'; |
||||
|
||||
|
||||
/** |
||||
* KeycloakGuard est la classe qui va gérer l'authentification et les droits d'accès en fonction des rôles Keycloak. |
||||
*/ |
||||
@Injectable() |
||||
export class KeycloakGuard extends KeycloakAuthGuard { |
||||
constructor(protected router: Router, protected keycloakAngular: KeycloakService) { |
||||
super(router, keycloakAngular); |
||||
} |
||||
|
||||
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> { |
||||
return new Promise(async (resolve, reject) => { |
||||
if (!this.authenticated) { |
||||
this.keycloakAngular.login(); |
||||
return; |
||||
} |
||||
|
||||
const requiredRoles = route.data.roles; |
||||
if (!requiredRoles || requiredRoles.length === 0) { |
||||
return resolve(true); |
||||
} else { |
||||
if (!this.roles || this.roles.length === 0) { |
||||
resolve(false); |
||||
} |
||||
let granted: boolean = false; |
||||
for (const requiredRole of requiredRoles) { |
||||
if (this.roles.indexOf(requiredRole) > -1) { |
||||
granted = true; |
||||
break; |
||||
} |
||||
} |
||||
resolve(granted); |
||||
} |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,31 @@ |
||||
<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…
Reference in new issue