diff --git a/package.json b/package.json index 063d8be87186c03b825b4a5215a9917bf716546d..543a3f1fd4ec3938052c46cc8b954aaa421a4903 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,11 @@ "@ngrx/router-store": "^10.0.1", "@ngrx/store": "^10.0.1", "@ngrx/store-devtools": "^10.0.1", - "angular-auth-oidc-client": "^11.2.2", "bootstrap": "^4.5.3", "d3": "^5.15.1", "jwt-decode": "^3.1.2", + "keycloak-angular": "^8.0.1", + "keycloak-js": "^11.0.3", "ng-inline-svg": "^11.0.1", "ngx-bootstrap": "^6.1.0", "ngx-json-viewer": "^2.4.0", diff --git a/src/app/auth/auth.action.spec.ts b/src/app/auth/auth.action.spec.ts deleted file mode 100644 index 1751c246acd3b93bf86d1fa8214bc4f7e32901ed..0000000000000000000000000000000000000000 --- a/src/app/auth/auth.action.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as authActions from './auth.action'; - -describe('[Auth] Action', () => { - it('should create CheckAuthAction', () => { - const action = new authActions.CheckAuthAction(); - expect(action.type).toEqual(authActions.CHECK_AUTH); - }); - - it('should create CheckAuthCompleteAction', () => { - const action = new authActions.CheckAuthCompleteAction(true); - expect(action.type).toEqual(authActions.CHECK_AUTH_COMPLETE); - expect(action.payload).toBeTruthy(); - }); - - it('should create LoginAction', () => { - const action = new authActions.LoginAction(); - expect(action.type).toEqual(authActions.LOGIN); - }); - - it('should create LoginCompleteAction', () => { - const action = new authActions.LoginCompleteAction('toto'); - expect(action.type).toEqual(authActions.LOGIN_COMPLETE); - expect(action.payload).toEqual('toto'); - }); - - it('should create LogoutAction', () => { - const action = new authActions.LogoutAction(); - expect(action.type).toEqual(authActions.LOGOUT); - }); - - it('should create OpenEditProfileAction', () => { - const action = new authActions.OpenEditProfileAction(); - expect(action.type).toEqual(authActions.OPEN_EDIT_PROFILE); - }); - - it('should create ParseJwtAction', () => { - const action = new authActions.ParseJwtAction('toto'); - expect(action.type).toEqual(authActions.PARSE_JWT); - expect(action.payload).toEqual('toto'); - }); - - it('should create LoadUserRolesAction', () => { - const action = new authActions.LoadUserRolesAction(['toto']); - expect(action.type).toEqual(authActions.LOAD_USER_ROLES); - expect(action.payload).toEqual(['toto']); - }); -}); diff --git a/src/app/auth/auth.action.ts b/src/app/auth/auth.action.ts index fc07c2862bce940dc66604c0b7221c9325d3899a..481b97a2ffcf383dbddec3ac8f6b70c8d762629f 100644 --- a/src/app/auth/auth.action.ts +++ b/src/app/auth/auth.action.ts @@ -1,117 +1,54 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - import { Action } from '@ngrx/store'; -export const CHECK_AUTH = '[Auth] Check Auth'; -export const CHECK_AUTH_COMPLETE = '[Auth] Check Auth Complete'; +import { UserProfile } from './user-profile.model'; + export const LOGIN = '[Auth] Login'; -export const LOGIN_COMPLETE = '[Auth] Login Complete'; export const LOGOUT = '[Auth] Logout'; +export const AUTH_SUCCESS = '[Auth] Auth Success'; +export const LOAD_USER_PROFILE_SUCCESS = '[Auth] Load User Profile Success'; +export const LOAD_USER_ROLES_SUCCESS = '[Auth] Load User Roles Success'; export const OPEN_EDIT_PROFILE = '[Auth] Edit Profile'; -export const PARSE_JWT = '[Auth] Parse JWT'; -export const LOAD_USER_ROLES = '[Auth] Load User Roles'; - -/** - * @class - * @classdesc CheckAuthAction action. - * @readonly - */ -export class CheckAuthAction implements Action { - readonly type = CHECK_AUTH; - - constructor(public payload: {} = null) { } -} -/** - * @class - * @classdesc CheckAuthCompleteAction action. - * @readonly - */ -export class CheckAuthCompleteAction implements Action { - readonly type = CHECK_AUTH_COMPLETE; - - constructor(public payload: boolean) { } -} - -/** - * @class - * @classdesc LoginAction action. - * @readonly - */ export class LoginAction implements Action { readonly type = LOGIN; - constructor(public payload: string = null) { } -} - -/** - * @class - * @classdesc LoginCompleteAction action. - * @readonly - */ -export class LoginCompleteAction implements Action { - readonly type = LOGIN_COMPLETE; - - constructor(public payload: any) { } + constructor(public payload: {} = null) { } } -/** - * @class - * @classdesc LogoutAction action. - * @readonly - */ export class LogoutAction implements Action { readonly type = LOGOUT; constructor(public payload: {} = null) { } } -/** - * @class - * @classdesc OpenEditProfileAction action. - * @readonly - */ -export class OpenEditProfileAction implements Action { - readonly type = OPEN_EDIT_PROFILE; +export class AuthSuccessAction implements Action { + readonly type = AUTH_SUCCESS; constructor(public payload: {} = null) { } } -/** - * @class - * @classdesc ParseJwtAction action. - * @readonly - */ -export class ParseJwtAction implements Action { - readonly type = PARSE_JWT; +export class LoadUserProfileSuccessAction implements Action { + readonly type = LOAD_USER_PROFILE_SUCCESS; - constructor(public payload: string) { } + constructor(public payload: UserProfile) { } } -/** - * @class - * @classdesc LoadUserRolesAction action. - * @readonly - */ -export class LoadUserRolesAction implements Action { - readonly type = LOAD_USER_ROLES; +export class LoadUserRolesSuccessAction implements Action { + readonly type = LOAD_USER_ROLES_SUCCESS; constructor(public payload: string[]) { } } +export class OpenEditProfileAction implements Action { + readonly type = OPEN_EDIT_PROFILE; + + constructor(public payload: {} = null) { } +} + export type Actions - = CheckAuthAction - | CheckAuthCompleteAction - | LoginAction - | LoginCompleteAction + = LoginAction | LogoutAction - | OpenEditProfileAction - | ParseJwtAction - | LoadUserRolesAction; + | AuthSuccessAction + | LoadUserProfileSuccessAction + | LoadUserRolesSuccessAction + | OpenEditProfileAction; diff --git a/src/app/auth/auth.effects.ts b/src/app/auth/auth.effects.ts index b84de91045583ae069de33451992b314eac4b149..7f492302140914a38199cc0cb3a47375e3560357 100644 --- a/src/app/auth/auth.effects.ts +++ b/src/app/auth/auth.effects.ts @@ -1,134 +1,48 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - import { Injectable } from '@angular/core'; -import { Router } from '@angular/router'; +import { from } from 'rxjs'; +import { switchMap, tap } from 'rxjs/operators'; import { Effect, Actions, ofType } from '@ngrx/effects'; -import { of } from 'rxjs'; -import { switchMap, map, tap } from 'rxjs/operators'; -import jwt_decode from 'jwt-decode'; +import { KeycloakService } from 'keycloak-angular'; import * as authActions from './auth.action'; -import { AuthService } from './auth.service'; import { environment } from '../../environments/environment'; @Injectable() -/** - * @class - * @classdesc Authentication effects. - */ export class AuthEffects { constructor( private actions$: Actions, - private authService: AuthService, - private router: Router + private keycloak: KeycloakService ) { } - /** - * Executes log in (OIDC Login). - */ @Effect({ dispatch: false }) loginAction$ = this.actions$.pipe( ofType(authActions.LOGIN), - tap((action: authActions.LoginAction) => { - const redirectUrl = action.payload; - if (redirectUrl !== null) { - sessionStorage.setItem(environment.ssoClientId + '_redirectUrl', redirectUrl); - } - return this.authService.doLogin() - }) - ); - - /** - * Checks if user is authenticated. - */ - @Effect() - checkAuthAction$ = this.actions$.pipe( - ofType(authActions.CHECK_AUTH), - switchMap(() => - this.authService - .checkAuth() - .pipe( - map((isAuthenticated) => new authActions.CheckAuthCompleteAction(isAuthenticated)) - ) - ) - ); - - /** - * Completes authentication if not complete. - */ - @Effect() - checkAuthCompleteAction$ = this.actions$.pipe( - ofType(authActions.CHECK_AUTH_COMPLETE), - switchMap((action: authActions.CheckAuthCompleteAction) => { - const isAuthenticated = action.payload; - - if (isAuthenticated) { - return this.authService.userData.pipe( - switchMap((profile) => [ - new authActions.LoginCompleteAction(profile), - new authActions.ParseJwtAction(this.authService.token) - ]) - ); - } else { - return of({ type: '[No Action] ' + authActions.CHECK_AUTH_COMPLETE }); - } - }) + tap(_ => this.keycloak.login()) ); - /** - * Redirects user after login completes. - */ @Effect({ dispatch: false }) - loginCompleteAction$ = this.actions$.pipe( - ofType(authActions.LOGIN_COMPLETE), - tap(_ => { - const redirectUrl = sessionStorage.getItem(environment.ssoClientId + '_redirectUrl'); - if (redirectUrl !== null) { - sessionStorage.removeItem(environment.ssoClientId + '_redirectUrl'); - this.router.navigateByUrl(redirectUrl); - } - }) + logoutAction$ = this.actions$.pipe( + ofType(authActions.LOGOUT), + tap(_ => this.keycloak.logout()) ); - /** - * Parses token to get user roles. - */ @Effect() - parseJWTAction$ = this.actions$.pipe( - ofType(authActions.PARSE_JWT), - switchMap((action: authActions.ParseJwtAction) => { - const jwt: any = jwt_decode(action.payload); - if (environment.ssoName === 'auth0') { - return of({ type: '[No Action] ' + authActions.PARSE_JWT }); - } else { - return of(new authActions.LoadUserRolesAction(jwt.realm_access.roles)); - } - }) - ); - - /** - * Executes log out. - */ - @Effect({ dispatch: false }) - logoutAction$ = this.actions$.pipe( - ofType(authActions.LOGOUT), - tap(_ => this.authService.signOut()) + authSuccessAction$ = this.actions$.pipe( + ofType(authActions.AUTH_SUCCESS), + switchMap(_ => + from(this.keycloak.loadUserProfile()).pipe( + switchMap(userProfile => [ + new authActions.LoadUserProfileSuccessAction(userProfile), + new authActions.LoadUserRolesSuccessAction(this.keycloak.getUserRoles()) + ]) + ) + ) ); - /** - * Opens edit profile page. - */ @Effect({ dispatch: false }) - openEditProfileAction$ = this.actions$.pipe( + OpenEditProfileAction$ = this.actions$.pipe( ofType(authActions.OPEN_EDIT_PROFILE), - tap(_ => window.open(environment.ssoAuthUrl + '/account', '_blank')) + tap(_ => window.open(environment.ssoAuthUrl + '/realms/' + environment.ssoRealm + '/account', '_blank')) ); } diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts index 67188544aca69fccb0b473fd0fef82add26b7b13..8d7d58afc16229fe4ca6add1af004262bf99dd60 100644 --- a/src/app/auth/auth.module.ts +++ b/src/app/auth/auth.module.ts @@ -1,49 +1,21 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { NgModule, APP_INITIALIZER } from '@angular/core'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { KeycloakAngularModule } from 'keycloak-angular'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; -import { AuthModule as AuthOIDCModule, OidcConfigService } from 'angular-auth-oidc-client'; +import { initializeKeycloakAnis } from './init.keycloak'; import { reducer } from './auth.reducer'; import { AuthEffects } from './auth.effects'; -import { AuthService } from './auth.service'; -import { configureAuth } from './config-auth'; -import { TokenInterceptor } from './token-interceptor'; @NgModule({ imports: [ - AuthOIDCModule.forRoot(), + KeycloakAngularModule, StoreModule.forFeature('auth', reducer), EffectsModule.forFeature([ AuthEffects ]) ], providers: [ - OidcConfigService, - { - provide: APP_INITIALIZER, - useFactory: configureAuth, - deps: [OidcConfigService], - multi: true, - }, - AuthService, - { - provide: HTTP_INTERCEPTORS, - useClass: TokenInterceptor, - multi: true - } + initializeKeycloakAnis ] }) -/** - * @class - * @classdesc Authentication module. - */ export class AuthModule { } diff --git a/src/app/auth/auth.reducer.spec.ts b/src/app/auth/auth.reducer.spec.ts deleted file mode 100644 index 4cf2dae8e0f45ecbfa6bc049275ba777d7ad1681..0000000000000000000000000000000000000000 --- a/src/app/auth/auth.reducer.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as fromAuth from './auth.reducer'; -import * as authActions from './auth.action'; - -describe('[Auth] Reducer', () => { - it('should return init state', () => { - const { initialState } = fromAuth; - const action = {} as authActions.Actions; - const state = fromAuth.reducer(undefined, action); - - expect(state).toBe(initialState); - }); - - it('should set isAuthenticated to true and set userProfile', () => { - const { initialState } = fromAuth; - const action = new authActions.LoginCompleteAction('toto'); - const state = fromAuth.reducer(initialState, action); - - expect(state.isAuthenticated).toBeTruthy(); - expect(state.userProfile).toEqual('toto'); - expect(state.userRoles.length).toEqual(0); - expect(state).not.toEqual(initialState); - }); - - it('should set userRoles', () => { - const { initialState } = fromAuth; - const action = new authActions.LoadUserRolesAction(['toto']); - const state = fromAuth.reducer(initialState, action); - - expect(state.isAuthenticated).toBeFalsy(); - expect(state.userProfile).toBeNull(); - expect(state.userRoles.length).toEqual(1); - expect(state.userRoles[0]).toEqual('toto'); - expect(state).not.toEqual(initialState); - }); - - it('should set isAuthenticated to false and unset userRoles', () => { - const initialState = { ...fromAuth.initialState, isAuthenticated: true, userProfile: 'toto' }; - const action = new authActions.LogoutAction(); - const state = fromAuth.reducer(initialState, action); - - expect(state.isAuthenticated).toBeFalsy(); - expect(state.userProfile).toBeNull(); - expect(state.userRoles.length).toEqual(0); - expect(state).not.toEqual(initialState); - }); - - it('should get isAuthenticated', () => { - const action = {} as authActions.Actions; - const state = fromAuth.reducer(undefined, action); - - expect(fromAuth.isAuthenticated(state)).toBeFalsy(); - }); - - it('should get userProfile', () => { - const action = {} as authActions.Actions; - const state = fromAuth.reducer(undefined, action); - - expect(fromAuth.getUserProfile(state)).toBeNull(); - }); - - it('should get userRoles', () => { - const action = {} as authActions.Actions; - const state = fromAuth.reducer(undefined, action); - - expect(fromAuth.getUserRoles(state).length).toEqual(0); - }); -}); diff --git a/src/app/auth/auth.reducer.ts b/src/app/auth/auth.reducer.ts index 87d6637a09f2bf173732e78ea547ed3ef7da8d00..576ca0100cf210e755ca825a5195a957a58617c1 100644 --- a/src/app/auth/auth.reducer.ts +++ b/src/app/auth/auth.reducer.ts @@ -1,22 +1,10 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - import * as actions from './auth.action'; -/** - * Interface for authentication state. - * - * @interface State - */ +import { UserProfile } from './user-profile.model'; + export interface State { isAuthenticated: boolean; - userProfile: any; + userProfile: UserProfile; userRoles: string[]; } @@ -26,35 +14,29 @@ export const initialState: State = { userRoles: [] }; -/** - * Reduces state. - * - * @param {State} state - The state. - * @param {actions} action - The action. - * - * @return State - */ export function reducer(state: State = initialState, action: actions.Actions): State { switch (action.type) { - case actions.LOGIN_COMPLETE: + case actions.AUTH_SUCCESS: return { ...state, - isAuthenticated: true, - userProfile: action.payload + isAuthenticated: true }; - case actions.LOAD_USER_ROLES: + case actions.LOAD_USER_PROFILE_SUCCESS: + const userProfile = action.payload; + return { ...state, - userRoles: action.payload - }; + userProfile + }; + + case actions.LOAD_USER_ROLES_SUCCESS: + const userRoles = action.payload; - case actions.LOGOUT: return { ...state, - isAuthenticated: false, - userProfile: null - } + userRoles + }; default: return state; diff --git a/src/app/auth/auth.selector.spec.ts b/src/app/auth/auth.selector.spec.ts deleted file mode 100644 index 64a4e089015f4ce1b61f70ef8b0ac4389565de2e..0000000000000000000000000000000000000000 --- a/src/app/auth/auth.selector.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as authSelector from './auth.selector'; -import * as fromAuth from './auth.reducer'; - -describe('[Auth] Selector', () => { - it('should get isAuthenticated', () => { - const state = { auth: { ...fromAuth.initialState }}; - expect(authSelector.isAuthenticated(state)).toBeFalsy(); - }); - - it('should get isAuthenticated', () => { - const state = { auth: { ...fromAuth.initialState }}; - expect(authSelector.isAuthenticated(state)).toBeFalsy(); - }); - - it('should get userProfile', () => { - const state = { auth: { ...fromAuth.initialState }}; - expect(authSelector.getUserProfile(state)).toBeNull(); - }); - - it('should get userRoles', () => { - const state = { auth: { ...fromAuth.initialState }}; - expect(authSelector.getUserRoles(state).length).toEqual(0); - }); -}); \ No newline at end of file diff --git a/src/app/auth/auth.selector.ts b/src/app/auth/auth.selector.ts index db6ccddec34494926d889d520eecaa85a3918e54..a58dfd67eb2ee9295cbcd18d4df0aa76ac438733 100644 --- a/src/app/auth/auth.selector.ts +++ b/src/app/auth/auth.selector.ts @@ -1,12 +1,3 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - import { createSelector, createFeatureSelector } from '@ngrx/store'; import * as auth from './auth.reducer'; diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts deleted file mode 100644 index d7f8c45a595fceb7fbb612565620203db0614e68..0000000000000000000000000000000000000000 --- a/src/app/auth/auth.service.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Injectable } from '@angular/core'; - -import { Observable, of } from 'rxjs'; -import { OidcSecurityService } from 'angular-auth-oidc-client'; - -import { environment } from '../../environments/environment'; - -@Injectable({ providedIn: 'root' }) -/** - * @class - * @classdesc Authentication service. - */ -export class AuthService { - constructor(private oidcSecurityService: OidcSecurityService) { } - - /** - * Gets user data. - * - * @return Observable<any> - */ - get token() { - if (environment.ssoName === 'auth0') { - return this.oidcSecurityService.getIdToken(); - } else { - return this.oidcSecurityService.getToken(); - } - } - - /** - * Gets user data. - * - * @return Observable<any> - */ - get userData(): Observable<any> { - return this.oidcSecurityService.userData$; - } - - /** - * Checks authentication. - * - * @return Observable<boolean> - */ - checkAuth(): Observable<boolean> { - return this.oidcSecurityService.checkAuthIncludingServer(); - } - - /** - * Logs in user. - */ - doLogin() { - this.oidcSecurityService.authorize(); - } - - /** - * Logs out user. - */ - signOut(): void { - this.oidcSecurityService.logoffAndRevokeTokens(); - } -} diff --git a/src/app/auth/config-auth.ts b/src/app/auth/config-auth.ts deleted file mode 100644 index 309326bb1362692e753129dcec23d97c187e3ffe..0000000000000000000000000000000000000000 --- a/src/app/auth/config-auth.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { OidcConfigService, LogLevel } from 'angular-auth-oidc-client'; - -import { environment } from '../../environments/environment'; - -/** - * Adds user token to the request. - * - * @param {OidcConfigService} oidcConfigService - The single sign on service. - * - * @return any - */ -export function configureAuth(oidcConfigService: OidcConfigService): any { - return () => oidcConfigService.withConfig({ - stsServer: environment.ssoAuthUrl, - redirectUrl: window.location.origin, - postLogoutRedirectUri: window.location.origin, - clientId: environment.ssoClientId, - scope: 'openid profile email offline_access', - responseType: 'code', - silentRenew: true, - silentRenewUrl: `${window.location.origin}/silent-renew.html`, - renewTimeBeforeTokenExpiresInSeconds: 10, - logLevel: LogLevel.None, - }); -} diff --git a/src/app/auth/init.keycloak.ts b/src/app/auth/init.keycloak.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b9e98db17f3c6381b1aeabece40054d1196fe5f --- /dev/null +++ b/src/app/auth/init.keycloak.ts @@ -0,0 +1,40 @@ +import { APP_INITIALIZER } from '@angular/core'; +import { from } from 'rxjs'; + +import { KeycloakService, KeycloakEventType } from 'keycloak-angular'; +import { Store } from '@ngrx/store'; + +import * as keycloakActions from './auth.action'; +import * as fromKeycloak from './auth.reducer'; +import { environment } from '../../environments/environment'; + +function initializeKeycloak(keycloak: KeycloakService, store: Store<{ keycloak: fromKeycloak.State }>) { + return async () => { + from(keycloak.keycloakEvents$).subscribe(event => { + if (event.type === KeycloakEventType.OnAuthSuccess) { + store.dispatch(new keycloakActions.AuthSuccessAction()); + } + }) + + return keycloak.init({ + config: { + url: environment.ssoAuthUrl, + realm: environment.ssoRealm, + clientId: environment.ssoClientId, + }, + initOptions: { + onLoad: 'check-sso', + silentCheckSsoRedirectUri: + window.location.origin + '/assets/silent-check-sso.html' + }, + loadUserProfileAtStartUp: true + }); + } +} + +export const initializeKeycloakAnis = { + provide: APP_INITIALIZER, + useFactory: initializeKeycloak, + multi: true, + deps: [ KeycloakService, Store ], +}; diff --git a/src/app/auth/token-interceptor.ts b/src/app/auth/token-interceptor.ts deleted file mode 100644 index d54c36f7e7e1ce25a62dfd68a7d13645ef2a4b14..0000000000000000000000000000000000000000 --- a/src/app/auth/token-interceptor.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Injectable } from '@angular/core'; -import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; - -import { Observable } from 'rxjs'; - -import { AuthService } from './auth.service'; -import { environment } from '../../environments/environment'; - -@Injectable() -/** - * @class - * @classdesc Token interceptor. - * - * @implements HttpInterceptor - */ -export class TokenInterceptor implements HttpInterceptor { - constructor(private authService: AuthService) { } - - /** - * Adds user token to the request. - * - * @param {HttpRequest<any>} request - The outgoing request. - * @param {HttpHandler} next - The next interceptor. - * - * @return Observable<HttpEvent<any>> - */ - intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { - let requestToForward = request; - const token = this.authService.token; - const isApiUrl = request.url.startsWith(environment.apiUrl); - if (token !== '' && isApiUrl) { - requestToForward = request.clone({ - setHeaders: { - Authorization: `Bearer ${token}` - } - }); - } - return next.handle(requestToForward); - } -} diff --git a/src/app/auth/user-profile.model.ts b/src/app/auth/user-profile.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2addf7e0613e83e9015c843bef7ad7131352113 --- /dev/null +++ b/src/app/auth/user-profile.model.ts @@ -0,0 +1,11 @@ +export interface UserProfile { + id?: string; + username?: string; + email?: string; + firstName?: string; + lastName?: string; + enabled?: boolean; + emailVerified?: boolean; + totp?: boolean; + createdTimestamp?: number; +} diff --git a/src/app/core/components/nav.component.html b/src/app/core/components/nav.component.html index 5d0a999de620bb71e477db72cfd7424e8aecfc64..8161ec8052c17293c5444745d43fbae0cd7b8296 100644 --- a/src/app/core/components/nav.component.html +++ b/src/app/core/components/nav.component.html @@ -49,12 +49,12 @@ <span class="dropdown-item font-italic">{{ userProfile.email }}</span> </li> <li class="divider dropdown-divider"></li> - <li *ngIf="ssoName != 'auth0'" role="menuitem"> + <li role="menuitem"> <button class="dropdown-item pointer" (click)="openEditProfile.emit()"> <span class="fas fa-id-card"></span> Edit profile </button> </li> - <li *ngIf="ssoName != 'auth0'" class="divider dropdown-divider"></li> + <li class="divider dropdown-divider"></li> <li role="menuitem"> <button class="dropdown-item text-danger pointer" (click)="logout.emit()"> <span class="fas fa-sign-out-alt fa-fw"></span> Sign Out @@ -96,12 +96,12 @@ </a> </li> <li *ngIf="getConfig('authentication', 'allowed') && isAuthenticated" class="divider dropdown-divider"></li> - <li *ngIf="getConfig('authentication', 'allowed') && isAuthenticated && ssoName != 'auth0'" role="menuitem"> + <li *ngIf="getConfig('authentication', 'allowed') && isAuthenticated" role="menuitem"> <button class="dropdown-item pointer" (click)="openEditProfile.emit()"> <span class="fas fa-id-card"></span> Edit profile </button> </li> - <li *ngIf="getConfig('authentication', 'allowed') && ssoName != 'auth0'" class="divider dropdown-divider"></li> + <li *ngIf="getConfig('authentication', 'allowed')" class="divider dropdown-divider"></li> <li role="menuitem"> <button *ngIf="getConfig('authentication', 'allowed') && !isAuthenticated" class="dropdown-item text-success" diff --git a/src/app/core/components/nav.component.ts b/src/app/core/components/nav.component.ts index e65122ab9c8a423931bbcc069e0abbdd91e654bb..a714d2ef5801aef1df3e77f247c762c062a84c43 100644 --- a/src/app/core/components/nav.component.ts +++ b/src/app/core/components/nav.component.ts @@ -10,6 +10,7 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; import { Instance } from '../../metamodel/model'; +import { UserProfile } from '../../auth/user-profile.model'; import { environment } from '../../../environments/environment' @Component({ @@ -24,14 +25,13 @@ import { environment } from '../../../environments/environment' */ export class NavComponent { @Input() isAuthenticated: boolean; - @Input() userProfile: any; + @Input() userProfile: UserProfile; @Input() instance: Instance; @Output() login: EventEmitter<any> = new EventEmitter(); @Output() logout: EventEmitter<any> = new EventEmitter(); @Output() openEditProfile: EventEmitter<any> = new EventEmitter(); baseHref: string = environment.baseHref; - ssoName: string = environment.ssoName; /** * Checks if given key for the given configuration propriety is allowed. diff --git a/src/app/core/containers/app.component.spec.ts b/src/app/core/containers/app.component.spec.ts index a53e5eb3b1e31e38f11efc784104b5eecdb097c9..de13a0b9d58ab26776fa3d16ae7c748f25aeeb0b 100644 --- a/src/app/core/containers/app.component.spec.ts +++ b/src/app/core/containers/app.component.spec.ts @@ -46,16 +46,14 @@ describe('[Core] Container: AppComponent', () => { it('should execute ngOnInit lifecycle', () => { const loadInstanceMetaAction = new instanceActions.LoadInstanceMetaAction(); - const checkAuthAction = new authActions.CheckAuthAction(); const spy = spyOn(store, 'dispatch'); component.ngOnInit(); - expect(spy).toHaveBeenCalledTimes(2); + expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith(loadInstanceMetaAction); - expect(spy).toHaveBeenCalledWith(checkAuthAction); }); it('#login() should dispatch LoginAction', () => { - const loginAction = new authActions.LoginAction('/'); + const loginAction = new authActions.LoginAction(); const spy = spyOn(store, 'dispatch'); component.login(); expect(spy).toHaveBeenCalledTimes(1); diff --git a/src/app/core/containers/app.component.ts b/src/app/core/containers/app.component.ts index e556cb08353fff9caca9189832926ffdfd712f03..ef662f5a0b9752132646e35555e20d37be40fbe7 100644 --- a/src/app/core/containers/app.component.ts +++ b/src/app/core/containers/app.component.ts @@ -8,7 +8,6 @@ */ import { Component, ViewEncapsulation, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; @@ -19,6 +18,7 @@ import * as metamodelSelector from '../../metamodel/selectors'; import * as fromAuth from '../../auth/auth.reducer'; import * as authActions from '../../auth/auth.action'; import * as authSelector from '../../auth/auth.selector'; +import { UserProfile } from '../../auth/user-profile.model'; import { Instance } from '../../metamodel/model'; import { VERSIONS } from '../../../settings/settings'; @@ -48,10 +48,10 @@ export class AppComponent implements OnInit { public anisClientVersion: string = VERSIONS.anisClient; public year = (new Date()).getFullYear(); public isAuthenticated: Observable<boolean>; - public userProfile: Observable<any>; + public userProfile: Observable<UserProfile>; public instance: Observable<Instance>; - constructor(private store: Store<StoreState>, private router: Router) { + constructor(private store: Store<StoreState>) { this.isAuthenticated = store.select(authSelector.isAuthenticated); this.userProfile = store.select(authSelector.getUserProfile); this.instance = store.select(metamodelSelector.getInstance); @@ -59,14 +59,13 @@ export class AppComponent implements OnInit { ngOnInit() { this.store.dispatch(new instanceActions.LoadInstanceMetaAction()); - this.store.dispatch(new authActions.CheckAuthAction()); } /** * Dispatches action to log in. */ login(): void { - this.store.dispatch(new authActions.LoginAction(this.router.url)); + this.store.dispatch(new authActions.LoginAction()); } /** diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index bcf0250547b4046a4c50fd0a5993e816805efc51..c3fdee522f6b8f094acd22f39e1c4a5f92ce3fe3 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -14,7 +14,7 @@ export const environment = { spectraUrl: 'https://cesam.lam.fr/anis-tools/spectra_to_csv/?file=/dataproject/SPECTRA/', instanceName: 'default', baseHref: '/', - ssoName: 'keycloak', - ssoAuthUrl: 'https://anis-dev.lam.fr/auth/realms/anis', + ssoAuthUrl: 'https://anis-dev.lam.fr/auth', + ssoRealm: 'anis', ssoClientId: 'anis-client' }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index a92b10b39a42a48ce382e81358e25c9307bdd6c7..239025093d1bcbfab536bddbba8e3ef5cd674e4c 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -18,7 +18,7 @@ export const environment = { spectraUrl: 'https://cesam.lam.fr/anis-tools/spectra_to_csv/?file=/dataproject/SPECTRA/', instanceName: 'default', baseHref: '/', - ssoName: 'keycloak', - ssoAuthUrl: 'http://localhost:8180/auth/realms/anis', + ssoAuthUrl: 'http://localhost:8180/auth', + ssoRealm: 'anis', ssoClientId: 'anis-client' }; diff --git a/yarn.lock b/yarn.lock index 9d7951e3c1f80cc83990fad1ea84a11e7db02aa5..d34144c342012542c2b90ac111a9e2ee9b511660 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2014,14 +2014,6 @@ alphanum-sort@^1.0.0: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= -angular-auth-oidc-client@^11.2.2: - version "11.2.2" - resolved "https://registry.yarnpkg.com/angular-auth-oidc-client/-/angular-auth-oidc-client-11.2.2.tgz#59c3875042478e64736cb286cb7095b8126d1cf1" - integrity sha512-eBt6iuEF2mC2AL3IINXXrb0zKGPeJ8LXrYav4cnum6vNr0b8XbD1guFtu+/j699qge4rPDWoncPJzYk/VuZ0LA== - dependencies: - common-tags "^1.8.0" - jsrsasign-reduced "^8.0.15" - ansi-colors@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -2339,6 +2331,11 @@ base64-arraybuffer@0.1.5: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= +base64-js@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + base64-js@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" @@ -3050,11 +3047,6 @@ commander@^2.11.0, commander@^2.12.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -common-tags@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" - integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -5846,6 +5838,11 @@ jest-worker@^26.3.0: merge-stream "^2.0.0" supports-color "^7.0.0" +js-sha256@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -5947,11 +5944,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsrsasign-reduced@^8.0.15: - version "8.0.15" - resolved "https://registry.yarnpkg.com/jsrsasign-reduced/-/jsrsasign-reduced-8.0.15.tgz#a1b110cdbb83bc0af91e9c66bee6f62ce0c9719d" - integrity sha512-Ig4W69nXCIUedzOSk3nqJWUr2DmSDENYfsmCqVK33GPETtPcjwREGQc92hV5jcJ6zavMvGD4tjhZ+T7JIiaSLA== - jszip@^3.1.3: version "3.2.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.1.tgz#c5d32df7274042282b157efb16e522b43435e01a" @@ -6041,6 +6033,21 @@ karma@~5.0.0: ua-parser-js "0.7.21" yargs "^15.3.1" +keycloak-angular@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/keycloak-angular/-/keycloak-angular-8.0.1.tgz#29851e7aded21925faa051c69dfa5872bda6661f" + integrity sha512-q68vcaFiSYNjbzPM1v+6LohMpWUyus9hcQBi2rNz06xOtWuRU4U6t5vQgoim6bDhtkhWpR5+a3SYl0lzUJKyrw== + dependencies: + tslib "^2.0.0" + +keycloak-js@^11.0.3: + version "11.0.3" + resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-11.0.3.tgz#5f22f22662211e2bfa5327d3d2eb83020a5baa23" + integrity sha512-e2OVyCiru25UhJz3aPj5irf//+vJzvAhHdcsCIWAcvF8Te22iUoZqEdNFji8D3zNzDehX4VpuIJwQOYCj6rqTA== + dependencies: + base64-js "1.3.1" + js-sha256 "0.9.0" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"