diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index da16ff037..b8fe0354e 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -39,14 +39,12 @@ import { ServiceWorkerModule } from '@angular/service-worker'; import { environment } from '../environments/environment'; import { ProjectDetailsDialogComponent } from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component'; import { AuthModule } from './auth/auth.module'; -import { AuthGuard } from './auth/auth.guard'; import { FileUploadModule } from './upload/file-upload.module'; import { FullPageLoadingIndicatorComponent } from './utils/full-page-loading-indicator/full-page-loading-indicator.component'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { InitialsAvatarComponent } from './common/initials-avatar/initials-avatar.component'; import { StatusBarComponent } from './components/status-bar/status-bar.component'; import { LogoComponent } from './logo/logo.component'; -import { AuthInterceptorService } from './interceptor/auth-interceptor.service'; import { CompositeRouteGuard } from './utils/composite-route.guard'; import { AppStateGuard } from './state/app-state.guard'; import { SimpleDoughnutChartComponent } from './components/simple-doughnut-chart/simple-doughnut-chart.component'; @@ -54,6 +52,7 @@ import { ManualRedactionDialogComponent } from './screens/file/manual-redaction- import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { AnnotationIconComponent } from './components/annotation-icon/annotation-icon.component'; +import { AuthGuard } from "./auth/auth.guard"; export function HttpLoaderFactory(httpClient: HttpClient) { return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json'); @@ -157,10 +156,6 @@ export function HttpLoaderFactory(httpClient: HttpClient) { provide: HTTP_INTERCEPTORS, multi: true, useClass: ApiPathInterceptorService - }, { - provide: HTTP_INTERCEPTORS, - multi: true, - useClass: AuthInterceptorService }, { provide: APP_INITIALIZER, multi: true, diff --git a/apps/red-ui/src/app/auth/auth.guard.ts b/apps/red-ui/src/app/auth/auth.guard.ts index 2d92b7633..b01f1d182 100644 --- a/apps/red-ui/src/app/auth/auth.guard.ts +++ b/apps/red-ui/src/app/auth/auth.guard.ts @@ -1,78 +1,30 @@ import {Injectable} from "@angular/core"; -import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree} from "@angular/router"; -import {Observable} from "rxjs"; -import {AuthConfig, OAuthService} from "angular-oauth2-oidc"; -import {AppConfigKey, AppConfigService} from "../app-config/app-config.service"; -import {map} from "rxjs/operators"; +import {ActivatedRouteSnapshot, Router, RouterStateSnapshot} from "@angular/router"; +import {KeycloakAuthGuard, KeycloakService} from "keycloak-angular"; import {UserService} from "../user/user.service"; - @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class AuthGuard implements CanActivate { - - private _configured = false; - - constructor(private readonly _oauthService: OAuthService, - private readonly _userService: UserService, - private readonly _appConfigService: AppConfigService) { +export class AuthGuard extends KeycloakAuthGuard { + constructor( + protected readonly _router: Router, + protected readonly _keycloak: KeycloakService, + private readonly _userService: UserService + ) { + super(_router, _keycloak); } - private async _configure() { - this._configured = true; - const authConfig = await this._createConfiguration().toPromise(); - this._oauthService.configure(authConfig); - this._oauthService.setupAutomaticSilentRefresh(); + public async isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { - - window['silentRefresh'] =() =>{ - this - ._oauthService - .silentRefresh() - .then(info => console.log('refresh ok', info)) - .catch(err => console.log('refresh error', err)); - }; - - return this._oauthService.loadDiscoveryDocumentAndTryLogin(); - } - - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { - if (!this._configured) { - return this._configure().then(() => this._checkToken()); + if (!this.authenticated) { + await this._keycloak.login({ + redirectUri: window.location.origin + state.url, + }); } - return this._checkToken(); - } + await this._userService.loadCurrentUser(); - private async _checkToken() { - const timeLeft= ((this._oauthService.getAccessTokenExpiration() - new Date().getTime()) / 1000); - const expired = timeLeft < 60; - - if (!this._oauthService.getAccessToken() || expired) { - this._oauthService.initLoginFlow(); - return false; - } - if (!this._userService.user) { - await this._userService.loadCurrentUser(); - return true; - } return true; } - - private _createConfiguration(): Observable { - return this._appConfigService.loadAppConfig().pipe(map(config => { - return { - issuer: config[AppConfigKey.OAUTH_URL], - redirectUri: window.location.origin, - clientId: config[AppConfigKey.OAUTH_CLIENT_ID], - scope: 'openid profile email offline_access', - responseType: 'code', - showDebugInformation: true, - silentRefreshRedirectUri: window.location.origin + '/assets/oauth/silent-refresh.html', - useSilentRefresh: true, - } - })); - } - } diff --git a/apps/red-ui/src/app/auth/auth.module.ts b/apps/red-ui/src/app/auth/auth.module.ts index 1d30338c4..15b9de25d 100644 --- a/apps/red-ui/src/app/auth/auth.module.ts +++ b/apps/red-ui/src/app/auth/auth.module.ts @@ -1,28 +1,55 @@ -import {NgModule} from "@angular/core"; +import {APP_INITIALIZER, NgModule} from "@angular/core"; import {CommonModule} from "@angular/common"; import {HttpClientModule} from "@angular/common/http"; -import {OAuthModule} from "angular-oauth2-oidc"; -import {AuthGuard} from "./auth.guard"; import {AppConfigModule} from "../app-config/app-config.module"; +import {KeycloakService} from "keycloak-angular"; +import {AppConfigKey, AppConfigService} from "../app-config/app-config.service"; + + +export function keycloakInitializer(keycloak: KeycloakService, appConfigService: AppConfigService) { + return () => { + return appConfigService.loadAppConfig().toPromise().then(() => { + + let url = appConfigService.getConfig(AppConfigKey.OAUTH_URL); + url = url.replace(/\/$/, ""); // remove trailing slash + const realm = url.substring(url.lastIndexOf("/") + 1, url.length); + url = url.substr(0, url.lastIndexOf("/")); + return keycloak.init({ + config: { + url: url, + realm: realm, + clientId: appConfigService.getConfig(AppConfigKey.OAUTH_CLIENT_ID) + }, + initOptions: { + checkLoginIframe: false, + onLoad: 'check-sso', + silentCheckSsoRedirectUri: + window.location.origin + '/assets/keycloak/silent-check-sso.html', + flow: 'standard' + }, + enableBearerInterceptor: true, + }) + }); + } +} + + @NgModule({ declarations: [], imports: [ CommonModule, HttpClientModule, AppConfigModule, - OAuthModule.forRoot( - { - resourceServer: { - allowedUrls: ['/'], - sendAccessToken: true - } - } - ) ], providers: [ - AuthGuard + { + provide: APP_INITIALIZER, + useFactory: keycloakInitializer, + multi: true, + deps: [KeycloakService, AppConfigService], + }, ], }) export class AuthModule { diff --git a/apps/red-ui/src/app/interceptor/auth-interceptor.service.ts b/apps/red-ui/src/app/interceptor/auth-interceptor.service.ts deleted file mode 100644 index 32bee4a6f..000000000 --- a/apps/red-ui/src/app/interceptor/auth-interceptor.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; -import {Injectable} from '@angular/core'; -import {Observable, of} from 'rxjs'; -import {AppConfigService} from "../app-config/app-config.service"; -import {catchError} from "rxjs/operators"; -import {OAuthService} from "angular-oauth2-oidc"; - -@Injectable() -export class AuthInterceptorService implements HttpInterceptor { - - constructor(private readonly _appConfigService: AppConfigService, private readonly _oauthService: OAuthService) { - } - - intercept(req: HttpRequest, next: HttpHandler): Observable> { - return next.handle(req).pipe(catchError((err: any) => { - if (err instanceof HttpErrorResponse) { - if (err.status === 401) { - this._oauthService.initLoginFlow(); - } - } - return of(err); - })); - } -} diff --git a/apps/red-ui/src/app/user/user.service.ts b/apps/red-ui/src/app/user/user.service.ts index 8da70bd68..a961b79cf 100644 --- a/apps/red-ui/src/app/user/user.service.ts +++ b/apps/red-ui/src/app/user/user.service.ts @@ -1,22 +1,37 @@ import {Injectable} from "@angular/core"; -import {OAuthService, UserInfo} from "angular-oauth2-oidc"; +import {KeycloakService} from "keycloak-angular"; +import {KeycloakProfile} from "keycloak-js"; + + +export class UserWrapper { + + constructor(private _currentUser: KeycloakProfile, public roles: string[]) { + } + + get name() { + return this._currentUser.firstName + " " + this._currentUser.lastName; + } + + +} @Injectable({ providedIn: 'root' }) export class UserService { - private _currentUser: UserInfo; + private _currentUser: UserWrapper; - constructor(private _oauthService: OAuthService) { + constructor(private _keycloakService: KeycloakService) { } logout() { - this._oauthService.logOut(); + this._keycloakService.logout(); } async loadCurrentUser() { - this._currentUser = await this._oauthService.loadUserProfile(); + this._currentUser = new UserWrapper(await this._keycloakService.loadUserProfile(false),this._keycloakService.getUserRoles(true)); + console.log(this._currentUser); } get user() { diff --git a/package.json b/package.json index d64736ef3..109ab4de9 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,9 @@ "@ngx-translate/http-loader": "^6.0.0", "@nrwl/angular": "^10.2.0", "@pdftron/webviewer": "^7.0.1", - "angular-oauth2-oidc": "^10.0.3", - "angular-oauth2-oidc-jwks": "^9.0.0", "file-saver": "^2.0.2", + "keycloak-angular": "^8.0.1", + "keycloak-js": "^11.0.2", "ng2-file-upload": "^1.4.0", "ngp-sort-pipe": "^0.0.4", "ngx-dropzone": "^2.2.2", diff --git a/redaction.iml b/redaction.iml index 8021953ed..da8860ce3 100644 --- a/redaction.iml +++ b/redaction.iml @@ -2,7 +2,10 @@ - + + + + diff --git a/yarn.lock b/yarn.lock index 5d8b34b0e..12529778a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2263,20 +2263,6 @@ alphanum-sort@^1.0.0: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= -angular-oauth2-oidc-jwks@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/angular-oauth2-oidc-jwks/-/angular-oauth2-oidc-jwks-9.0.0.tgz#f11e4e561ff423928ab63ca2cca84703a00ff85d" - integrity sha512-3hTJc7vEI/ka/nnliMcCQuDnszzL3AhGInBBbn96BO+ZOdvP/4PbEumUsDto2WRpPMPxD6HAmExwYeQWljcc5A== - dependencies: - jsrsasign "^8.0.12" - -angular-oauth2-oidc@^10.0.3: - version "10.0.3" - resolved "https://registry.yarnpkg.com/angular-oauth2-oidc/-/angular-oauth2-oidc-10.0.3.tgz#612ef75c2e07b56592d2506f9618ee6a61857ad9" - integrity sha512-9wC8I3e3cN6rMBOlo5JB2y3Fd2erp8pJ67t4vEVzyPbnRG6BJ4rreSOznSL9zw/2SjhC9kRV2OfFie29CUCzEg== - dependencies: - tslib "^2.0.0" - ansi-colors@4.1.1, ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -2694,7 +2680,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-js@^1.0.2: +base64-js@1.3.1, base64-js@^1.0.2: 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== @@ -6787,6 +6773,11 @@ jest@26.2.2: import-local "^3.0.2" jest-cli "^26.2.2" +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" @@ -6937,11 +6928,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsrsasign@^8.0.12: - version "8.0.24" - resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.24.tgz#fc26bac45494caac3dd8f69c1f95847c4bda6c83" - integrity sha512-u45jAyusqUpyGbFc2IbHoeE4rSkoBWQgLe/w99temHenX+GyCz4nflU5sjK7ajU1ffZTezl6le7u43Yjr/lkQg== - karma-source-map-support@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" @@ -6949,6 +6935,21 @@ karma-source-map-support@1.4.0: dependencies: source-map-support "^0.5.5" +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.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-11.0.2.tgz#e981c5270e72066e38b2a1bd98f1138d6cd560c1" + integrity sha512-dnvzgTetovu3eTjJtvBQQhxRN4jqvd/DaA2wFaE4aWIFXhwRcoPpZT8ZJ7MwlICDPdCgzbCsOsBjpL8CbYOZsg== + 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"