From ffff2ec6d58ee02d72e7dd250dd4acc9b8e046b9 Mon Sep 17 00:00:00 2001 From: Homada Boumedane <homada.boumedane@unige.ch> Date: Mon, 8 Jul 2019 15:16:12 +0200 Subject: [PATCH] refactor(dlcm-portal): refactor oauth2 service refactor oauth2 service + remove app_initilize because and use ngxsOnInit instead --- src/app/app.component.ts | 16 +- src/app/app.module.ts | 33 +-- src/app/auth.config.ts | 27 -- src/app/core/auth/auth.config.ts | 234 ------------------ src/app/core/auth/oauth-module.config.ts | 13 - src/app/core/auth/oauth2.service.ts | 165 ++++-------- src/app/core/auth/types.ts | 12 - src/app/core/config/app-config.service.ts | 13 +- .../core/http/oauth2-interceptor.service.ts | 18 +- .../paginator/paginator.component.ts | 6 +- src/app/stores/app.state.ts | 13 +- src/environments/environment.defaults.ts | 32 ++- src/environments/environment.prod.ts | 16 +- 13 files changed, 112 insertions(+), 486 deletions(-) delete mode 100644 src/app/auth.config.ts delete mode 100644 src/app/core/auth/auth.config.ts delete mode 100644 src/app/core/auth/oauth-module.config.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b637442b8..88e5d863e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -13,7 +13,7 @@ import {Navigate} from "@ngxs/router-plugin"; import {Select, Store} from "@ngxs/store"; import {Observable} from "rxjs"; import {tap} from "rxjs/internal/operators/tap"; -import {environment} from "../environments/environment"; +import {AppConfigService} from "@app/core/config/app-config.service"; @Component({ selector: "dlcm-root", @@ -21,8 +21,8 @@ import {environment} from "../environments/environment"; styleUrls: ["./app.component.scss"], }) export class AppComponent extends AbstractComponent implements OnInit { - title: string = environment.appTitle; - theme: string = environment.theme; + title: string = ""; + theme: string = ""; currentModule: string; logo: string; @@ -33,18 +33,20 @@ export class AppComponent extends AbstractComponent implements OnInit { constructor(private store: Store, private oauthService: OAuth2Service, private router: Router, - private translate: TranslateService) { + private translate: TranslateService, + private appConfig: AppConfigService) { super(); this.setLogo(); this.setCurrentModule(); this.languageSwitcher(); + this.title = this.appConfig.config.appTitle; + this.theme = this.appConfig.config.theme; } ngOnInit(): void { } useLanguage(language: LanguagesEnum): void { - // this.translate.use(language); this.store.dispatch(new ChangeAppLanguage(language)); } @@ -54,11 +56,11 @@ export class AppComponent extends AbstractComponent implements OnInit { private setLogo(): string { const basePath = "assets/images/"; - if (environment.theme === ThemeEnum.yareta) { + if (this.theme === ThemeEnum.yareta) { this.logo = basePath + "Yareta-v.svg"; return; } - if (environment.theme === ThemeEnum.dlcm) { + if (this.theme === ThemeEnum.dlcm) { this.logo = basePath + "DLCM2.svg"; return; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4e32186d0..d60140119 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,5 +1,5 @@ import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from "@angular/common/http"; -import {APP_INITIALIZER, ErrorHandler, NgModule} from "@angular/core"; +import {ErrorHandler, NgModule} from "@angular/core"; import {BrowserModule} from "@angular/platform-browser"; import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; import {AppRoutingModule} from "@app/app-routing.module"; @@ -7,9 +7,6 @@ import {AppState} from "@app/stores/app.state"; import {MainToolbarMobileComponent} from "@app/components/main-toolbar/main-toolbar-mobile/main-toolbar-mobile.component"; import {ApiModule} from "@app/generated-api/api.module"; import {GlobalErrorsHandlerService} from "@app/core/services/global-errors-handler.service"; - -import {OAuthModuleConfig} from "@app/core/auth/oauth-module.config"; - import {SharedModule} from "@app/shared/shared.module"; import {TranslateLoader, TranslateModule} from "@ngx-translate/core"; import {TranslateHttpLoader} from "@ngx-translate/http-loader"; @@ -19,20 +16,16 @@ import {NgxsRouterPluginModule} from "@ngxs/router-plugin"; import {NgxsModule} from "@ngxs/store"; import {environment} from "../environments/environment"; import {AppComponent} from "./app.component"; -import {authConfig, authModuleConfig} from "./auth.config"; import {FooterComponent} from "./components/footer/footer.component"; import {HomeComponent} from "./components/home/home.component"; import {LanguageSelectorComponent} from "./components/language-selector/language-selector.component"; import {LoginComponent} from "./components/login/login.component"; import {MainToolbarDesktopComponent} from "./components/main-toolbar/main-toolbar-desktop/main-toolbar-desktop.component"; import {PageNotFoundComponent} from "./components/page-not-found/page-not-found.component"; -import {AbstractMainToolbarComponent} from "./components/main-toolbar/abstract-main-toolbar/abstract-main-toolbar.component"; -import {AppConfigService} from "@app/core/config/app-config.service"; import {OAuth2Interceptor} from "@app/core/http/oauth2-interceptor.service"; import {MatPaginatorIntl} from "@angular/material"; import {CustomMatPaginatorIntlService} from "@app/core/services/custom-mat-paginator-intl.service"; import {OAuthStorage} from "@app/core/auth/types"; -import {AuthConfig} from "@app/core/auth/auth.config"; const components = [ AppComponent, @@ -45,12 +38,6 @@ const components = [ PageNotFoundComponent, ]; -const appInitializerFn = (appConfig: AppConfigService) => { - return () => { - return appConfig.mergeConfig(environment).toPromise(); - }; -}; - export function createDefaultStorage(): Storage | null { return typeof sessionStorage !== "undefined" ? sessionStorage : null; } @@ -104,12 +91,6 @@ export function createDefaultStorage(): Storage | null { AppRoutingModule, ], providers: [ - { - provide: APP_INITIALIZER, - useFactory: appInitializerFn, - multi: true, - deps: [AppConfigService] - }, { provide: HTTP_INTERCEPTORS, useClass: OAuth2Interceptor, @@ -125,18 +106,6 @@ export function createDefaultStorage(): Storage | null { { provide: ErrorHandler, useClass: GlobalErrorsHandlerService, - }, - { - provide: OAuthModuleConfig, - useValue: authModuleConfig, - }, - { - provide: OAuthStorage, - useValue: sessionStorage, - }, - { - provide: AuthConfig, - useValue: authConfig, } ], bootstrap: [AppComponent], diff --git a/src/app/auth.config.ts b/src/app/auth.config.ts deleted file mode 100644 index 15ef5e342..000000000 --- a/src/app/auth.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {OAuthModuleConfig} from "@app/core/auth/oauth-module.config"; -import {environment} from "../environments/environment"; -import {AuthConfig} from "@app/core/auth/auth.config"; - -export const authConfig: AuthConfig = { - oidc: false, - loginUrl: environment.oauthLoginUrl, - redirectUri: environment.oauthRedirectUrl, - responseType: "token", - dummyClientSecret: environment.oauthDummyClientSecret, - clientId: environment.oauthClientId, - scope: environment.oauthScope, - requireHttps: environment.oauthRequireHttps, - tokenEndpoint: environment.oauthTokenEndpoint, - requestAccessToken: true, - showDebugInformation: environment.oauthShowDebugInformation, - useIdTokenHintForSilentRefresh: true, - skipIssuerCheck: true, -}; - -export const authModuleConfig: OAuthModuleConfig = { - // Inject "Authorization: Bearer ..." header for these APIs: - resourceServer: { - allowedUrls: [environment.oauthAllowedUrl], - sendAccessToken: true, - }, -}; diff --git a/src/app/core/auth/auth.config.ts b/src/app/core/auth/auth.config.ts deleted file mode 100644 index e9ff5a971..000000000 --- a/src/app/core/auth/auth.config.ts +++ /dev/null @@ -1,234 +0,0 @@ -export class AuthConfig { - /** - * The client's id as registered with the auth server - */ - public clientId?: string | undefined = ""; - - /** - * The client's redirectUri as registered with the auth server - */ - public redirectUri?: string | undefined = ""; - - /** - * An optional second redirectUri where the auth server - * redirects the user to after logging out. - */ - public postLogoutRedirectUri?: string | undefined = ""; - - /** - * The auth server's endpoint that allows to log - * the user in when using implicit flow. - */ - public loginUrl?: string | undefined = ""; - - /** - * The requested scopes - */ - public scope?: string | undefined = "openid profile"; - - public resource?: string | undefined = ""; - - public rngUrl?: string | undefined = ""; - - /** - * Defines whether to use OpenId Connect during - * implicit flow. - */ - public oidc?: boolean | undefined = true; - - /** - * Defines whether to request a access token during - * implicit flow. - */ - public requestAccessToken?: boolean | undefined = true; - - public options?: any = null; - - /** - * The issuer's uri. - */ - public issuer?: string | undefined = ""; - - /** - * The logout url. - */ - public logoutUrl?: string | undefined = ""; - - /** - * Defines whether to clear the hash fragment after logging in. - */ - public clearHashAfterLogin?: boolean | undefined = true; - - /** - * Url of the token endpoint as defined by OpenId Connect and OAuth 2. - */ - public tokenEndpoint?: string = null; - - /** - * Url of the userinfo endpoint as defined by OpenId Connect. - * - */ - public userinfoEndpoint?: string = null; - - public responseType?: string | undefined = "token"; - - /** - * Defines whether additional debug information should - * be shown at the console. - */ - public showDebugInformation?: boolean | undefined = false; - - /** - * The redirect uri used when doing silent refresh. - */ - public silentRefreshRedirectUri?: string | undefined = ""; - - public silentRefreshMessagePrefix?: string | undefined = ""; - - /** - * Set this to true to display the iframe used for - * silent refresh for debugging. - */ - public silentRefreshShowIFrame?: boolean | undefined = false; - - /** - * Timeout for silent refresh. - * @internal - * depreacted b/c of typo, see silentRefreshTimeout - */ - public siletRefreshTimeout?: number = 1000 * 20; - - /** - * Timeout for silent refresh. - */ - public silentRefreshTimeout?: number = 1000 * 20; - - /** - * Some auth servers don't allow using password flow - * w/o a client secreat while the standards do not - * demand for it. In this case, you can set a password - * here. As this passwort is exposed to the public - * it does not bring additional security and is therefore - * as good as using no password. - */ - public dummyClientSecret?: string = null; - - /** - * Defines whether https is required. - * The default value is remoteOnly which only allows - * http for localhost, while every other domains need - * to be used with https. - */ - public requireHttps?: boolean | "remoteOnly" = "remoteOnly"; - - /** - * Defines whether every url provided by the discovery - * document has to start with the issuer's url. - */ - public strictDiscoveryDocumentValidation?: boolean | undefined = true; - - /** - * JSON Web Key Set (https://tools.ietf.org/html/rfc7517) - * with keys used to validate received id_tokens. - * This is taken out of the disovery document. Can be set manually too. - */ - public jwks?: object = null; - - /** - * Map with additional query parameter that are appended to - * the request when initializing implicit flow. - */ - public customQueryParams?: object = null; - - public silentRefreshIFrameName?: string | undefined = "angular-oauth-oidc-silent-refresh-iframe"; - - /** - * Defines when the token_timeout event should be raised. - * If you set this to the default value 0.75, the event - * is triggered after 75% of the token's life time. - */ - public timeoutFactor?: number | undefined = 0.75; - - /** - * If true, the lib will try to check whether the user - * is still logged in on a regular basis as described - * in http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification - */ - public sessionChecksEnabled?: boolean | undefined = false; - - /** - * Intervall in msec for checking the session - * according to http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification - */ - public sessionCheckIntervall?: number | undefined = 3 * 1000; - - /** - * Url for the iframe used for session checks - */ - public sessionCheckIFrameUrl?: string = null; - - /** - * Name of the iframe to use for session checks - */ - public sessionCheckIFrameName?: string | undefined = "angular-oauth-oidc-check-session-iframe"; - - /** - * This property has been introduced to disable at_hash checks - * and is indented for Identity Provider that does not deliver - * an at_hash EVEN THOUGH its recommended by the OIDC specs. - * Of course, when disabling these checks the we are bypassing - * a security check which means we are more vulnerable. - */ - public disableAtHashCheck?: boolean | undefined = false; - - /* - * Defines wether to check the subject of a refreshed token after silent refresh. - * Normally, it should be the same as before. - */ - public skipSubjectCheck?: boolean | undefined = false; - - public useIdTokenHintForSilentRefresh?: boolean | undefined = false; - - /* - * Defined whether to skip the validation of the issuer in the discovery document. - * Normally, the discovey document's url starts with the url of the issuer. - */ - public skipIssuerCheck?: boolean | undefined = false; - - /** - * According to rfc6749 it is recommended (but not required) that the auth - * server exposes the access_token's life time in seconds. - * This is a fallback value for the case this value is not exposed. - */ - public fallbackAccessTokenExpirationTimeInSec?: number; - - /* - * final state sent to issuer is built as follows: - * state = nonce + nonceStateSeparator + additional state - * Default separator is ';' (encoded %3B). - * In rare cases, this character might be forbidden or inconvenient to use by the issuer so it can be customized. - */ - public nonceStateSeparator?: string | undefined = ";"; - - /* - * set this to true to use HTTP BASIC auth for password flow - */ - public useHttpBasicAuthForPasswordFlow?: boolean | undefined = false; - - public disableNonceCheck?: boolean | undefined = false; - - constructor(json?: Partial<AuthConfig>) { - if (json) { - Object.assign(this, json); - } - } - - /** - * This property allows you to override the method that is used to open the login url, - * allowing a way for implementations to specify their own method of routing to new - * urls. - */ - public openUri?: ((uri: string) => void) = uri => { - location.href = uri; - } -} diff --git a/src/app/core/auth/oauth-module.config.ts b/src/app/core/auth/oauth-module.config.ts deleted file mode 100644 index da0fb896d..000000000 --- a/src/app/core/auth/oauth-module.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -export abstract class OAuthModuleConfig { - resourceServer: OAuthResourceServerConfig; -} - -export abstract class OAuthResourceServerConfig { - /** - * Urls for which calls should be intercepted. - * If there is an ResourceServerErrorHandler registered, it is used for them. - * If sendAccessToken is set to true, the access_token is send to them too. - */ - allowedUrls?: string[]; - sendAccessToken: boolean; -} diff --git a/src/app/core/auth/oauth2.service.ts b/src/app/core/auth/oauth2.service.ts index 1994ad5c6..44270381c 100644 --- a/src/app/core/auth/oauth2.service.ts +++ b/src/app/core/auth/oauth2.service.ts @@ -1,10 +1,10 @@ import {HttpClient, HttpHeaders, HttpParams} from "@angular/common/http"; import {Injectable, NgZone, Optional} from "@angular/core"; -import {AuthConfig} from "@app/core/auth/auth.config"; import {OAuthErrorEvent, OAuthEvent, OAuthInfoEvent, OAuthSuccessEvent} from "@app/core/auth/events"; import {LoginOptions, OAuthStorage, TokenResponse} from "@app/core/auth/types"; import {Observable, of, Subject, Subscription} from "rxjs"; import {delay, filter} from "rxjs/operators"; +import {AppConfigService} from "@app/core/config/app-config.service"; @Injectable({ @@ -14,9 +14,7 @@ import {delay, filter} from "rxjs/operators"; * Service for logging in and logging out with * OIDC and OAuth2. Supports code flow. */ -export class OAuth2Service extends AuthConfig { - // extending AuthConfig ist just for LEGACY reasons - // to not break existing code +export class OAuth2Service { /** * Informs about events, like token_received or token_expires. @@ -31,13 +29,10 @@ export class OAuth2Service extends AuthConfig { constructor(private ngZone: NgZone, private http: HttpClient, - @Optional() storage: OAuthStorage, - @Optional() private config: AuthConfig) { - super(); + private appConfig: AppConfigService, + @Optional() storage: OAuthStorage) { - if (config) { - this.configure(config); - } + this.events = this.eventsSubject.asObservable(); try { if (storage) { @@ -52,18 +47,6 @@ export class OAuth2Service extends AuthConfig { this.setupRefreshTimer(); } - /** - * Use this method to configure the service - * @param config the configuration - */ - public configure(config: AuthConfig): void { - // For the sake of downward compatibility with - // original configuration API - Object.assign(this, new AuthConfig(), config); - - this.config = Object.assign({} as AuthConfig, new AuthConfig(), config); - } - /** * Sets a custom storage used to store the received * tokens on client side. By default, the browser's @@ -84,9 +67,9 @@ export class OAuth2Service extends AuthConfig { let params = new HttpParams() .set("grant_type", "refresh_token") .set("refresh_token", this._storage.getItem("refresh_token")) - .set("scope", this.scope); - if (this.dummyClientSecret) { - params = params.set("client_secret", this.dummyClientSecret); + .set("scope", this.appConfig.config.scope); + if (this.appConfig.config.dummyClientSecret) { + params = params.set("client_secret", this.appConfig.config.dummyClientSecret); } this.fetchToken(params); } @@ -109,11 +92,10 @@ export class OAuth2Service extends AuthConfig { */ public initAuthorizationCodeFlow(): void { - if (!this.validateUrlForHttps(this.loginUrl)) { + if (!this.validateUrlForHttps(this.appConfig.config.loginUrl)) { throw new Error("loginUrl must use Http. Also check property requireHttps."); } - - this.createLoginUrl("", "", null, false, {}) + this.createLoginUrl("", "") .subscribe(url => { location.href = url; }, error => { @@ -131,8 +113,8 @@ export class OAuth2Service extends AuthConfig { * @param options Optinal options. */ public tryLogin(options: LoginOptions = null): Observable<boolean> { - if (!this.requestAccessToken && !this.oidc) { - this.debug("Either requestAccessToken or oidc or both must be true."); + if (!this.appConfig.config.requestAccessToken && !this.appConfig.config.oidc) { + console.warn("Either requestAccessToken or oidc or both must be true."); return of(false); } @@ -216,7 +198,7 @@ export class OAuth2Service extends AuthConfig { this.eventsSubject.next(new OAuthInfoEvent("logout")); - if (!this.logoutUrl) { + if (!this.appConfig.config.logoutUrl) { return; } if (noRedirectToLogoutUrl) { @@ -224,22 +206,22 @@ export class OAuth2Service extends AuthConfig { } let logoutUrl: string; - if (!this.validateUrlForHttps(this.logoutUrl)) { + if (!this.validateUrlForHttps(this.appConfig.config.logoutUrl)) { throw new Error("logoutUrl must use Http. Also check property requireHttps."); } // For backward compatibility - if (this.logoutUrl.indexOf("{{") > -1) { - logoutUrl = this.logoutUrl - .replace(/\{\{client_id\}\}/, this.clientId); + if (this.appConfig.config.logoutUrl.indexOf("{{") > -1) { + logoutUrl = this.appConfig.config.logoutUrl + .replace(/\{\{client_id\}\}/, this.appConfig.config.clientId); } else { logoutUrl = - this.logoutUrl + - (this.logoutUrl.indexOf("?") > -1 ? "&" : "?") + + this.appConfig.config.logoutUrl + + (this.appConfig.config.logoutUrl.indexOf("?") > -1 ? "&" : "?") + "id_token_hint=" + encodeURIComponent("") + "&post_logout_redirect_uri=" + - encodeURIComponent(this.postLogoutRedirectUri || this.redirectUri); + encodeURIComponent(this.appConfig.config.postLogoutRedirectUri || this.appConfig.config.redirectUrl); } location.href = logoutUrl; } @@ -254,19 +236,14 @@ export class OAuth2Service extends AuthConfig { } protected createNonce(): string { + let text = ""; + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - if (this.rngUrl) { - throw new Error("createNonce with rng-web-api has not been implemented so far"); - } else { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (let i = 0; i < 40; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - - return text; + for (let i = 0; i < 40; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); } + + return text; } private setupRefreshTokenEventListener(): void { @@ -290,49 +267,43 @@ export class OAuth2Service extends AuthConfig { } private createLoginUrl(state: string = "", - loginHint: string = "", - customRedirectUri: string = "", - noPrompt: boolean = false, - params: object = {}): Observable<string> { - const that = this; + customRedirectUri: string = ""): Observable<string> { let redirectUri: string; if (customRedirectUri) { redirectUri = customRedirectUri; } else { - redirectUri = this.redirectUri; + redirectUri = this.appConfig.config.redirectUrl; } let nonce = null; - if (!this.disableNonceCheck) { + if (!this.appConfig.config.disableNonceCheck) { nonce = this.createAndSaveNonce(); if (state) { - state = nonce + this.config.nonceStateSeparator + state; + state = nonce + this.appConfig.config.nonceStateSeparator + state; } else { state = nonce; } } - if (!this.requestAccessToken && !this.oidc) { - throw new Error( - "Either requestAccessToken or oidc or both must be true", - ); + if (!this.appConfig.config.requestAccessToken && !this.appConfig.config.oidc) { + throw new Error("Either requestAccessToken or oidc or both must be true"); } - this.responseType = this.RESPONSE_TYPE; + this.appConfig.config.responseType = this.RESPONSE_TYPE; - const separationChar = that.loginUrl.indexOf("?") > -1 ? "&" : "?"; + const separationChar = this.appConfig.config.logoutUrl.indexOf("?") > -1 ? "&" : "?"; - const scope = that.scope; + const scope = this.appConfig.config.logoutUrl; let url = - that.loginUrl + + this.appConfig.config.loginUrl + separationChar + "response_type=" + - encodeURIComponent(that.responseType) + + encodeURIComponent(this.appConfig.config.responseType) + "&client_id=" + - encodeURIComponent(that.clientId) + + encodeURIComponent(this.appConfig.config.clientId) + "&state=" + encodeURIComponent(state) + "&redirect_uri=" + @@ -340,34 +311,10 @@ export class OAuth2Service extends AuthConfig { "&scope=" + encodeURIComponent(scope); - if (loginHint) { - url += "&login_hint=" + encodeURIComponent(loginHint); - } - - if (that.resource) { - url += "&resource=" + encodeURIComponent(that.resource); - } - - if (nonce && this.oidc) { + if (nonce && this.appConfig.config.oidc) { url += "&nonce=" + encodeURIComponent(nonce); } - if (noPrompt) { - url += "&prompt=none"; - } - - for (const key of Object.keys(params)) { - url += - "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); - } - - if (this.customQueryParams) { - for (const key of Object.getOwnPropertyNames(this.customQueryParams)) { - url += - "&" + key + "=" + encodeURIComponent(this.customQueryParams[key]); - } - } - return of(url); } @@ -389,12 +336,6 @@ export class OAuth2Service extends AuthConfig { } } - private debug(...args: any[]): void { - if (this.showDebugInformation) { - console.log.apply(console, args); - } - } - private validateUrlForHttps(url: string): boolean { if (!url) { return true; @@ -402,13 +343,7 @@ export class OAuth2Service extends AuthConfig { const lcUrl = url.toLowerCase(); - if (this.requireHttps === false) { - return true; - } - - if ((lcUrl.match(/^http:\/\/localhost($|[:\/])/) - || lcUrl.match(/^http:\/\/localhost($|[:\/])/)) - && this.requireHttps === "remoteOnly") { + if (this.appConfig.config.requireHttps === false) { return true; } @@ -417,7 +352,7 @@ export class OAuth2Service extends AuthConfig { private setupRefreshTimer(): void { if (typeof window === "undefined") { - this.debug("timer not supported on this plattform"); + console.warn("timer not supported on this plattform"); return; } @@ -457,7 +392,7 @@ export class OAuth2Service extends AuthConfig { } private calcTimeout(storedAt: number, expiration: number): number { - return (expiration - storedAt) * this.timeoutFactor; + return (expiration - storedAt) * this.appConfig.config.timeoutFactor; } /** @@ -467,32 +402,26 @@ export class OAuth2Service extends AuthConfig { const params = new HttpParams() .set("grant_type", "authorization_code") .set("code", code) - .set("redirect_uri", this.redirectUri); + .set("redirect_uri", this.appConfig.config.redirectUrl); this.fetchToken(params); } private fetchToken(params: HttpParams): void { - if (!this.validateUrlForHttps(this.tokenEndpoint)) { + if (!this.validateUrlForHttps(this.appConfig.config.tokenEndpoint)) { throw new Error("tokenEndpoint must use Http. Also check property requireHttps."); } - params = params.set("client_id", this.clientId); + params = params.set("client_id", this.appConfig.config.clientId); - if (this.customQueryParams) { - for (const key of Object.getOwnPropertyNames(this.customQueryParams)) { - params = params.set(key, this.customQueryParams[key]); - } - } - const authData = window.btoa(this.config.clientId + ":" + this.config.dummyClientSecret); + const authData = window.btoa(this.appConfig.config.clientId + ":" + this.appConfig.config.dummyClientSecret); const headers = new HttpHeaders() .set("Content-Type", "application/x-www-form-urlencoded") .set("Authorization", "Basic " + authData); - this.http.post<TokenResponse>(this.tokenEndpoint, params, {headers}) + this.http.post<TokenResponse>(this.appConfig.config.tokenEndpoint, params, {headers}) .subscribe((tokenResp) => { - this.debug("refresh tokenResponse", tokenResp); this.storeAccessTokenResponse(tokenResp.access_token, tokenResp.refresh_token, tokenResp.expires_in, tokenResp.scope); this.eventsSubject.next(new OAuthSuccessEvent("token_received")); this.eventsSubject.next(new OAuthSuccessEvent("token_refreshed")); diff --git a/src/app/core/auth/types.ts b/src/app/core/auth/types.ts index f228dbe61..cc00c9bb9 100644 --- a/src/app/core/auth/types.ts +++ b/src/app/core/auth/types.ts @@ -72,18 +72,6 @@ export class ReceivedTokens { state?: string; } -/** - * Represents the parsed and validated id_token. - */ -export interface ParsedIdToken { - idToken: string; - idTokenClaims: object; - idTokenHeader: object; - idTokenClaimsJson: string; - idTokenHeaderJson: string; - idTokenExpiresAt: number; -} - /** * Represents the response from the token endpoint * http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint diff --git a/src/app/core/config/app-config.service.ts b/src/app/core/config/app-config.service.ts index 277bee296..2b9234467 100644 --- a/src/app/core/config/app-config.service.ts +++ b/src/app/core/config/app-config.service.ts @@ -3,13 +3,14 @@ import {Injectable, Injector} from "@angular/core"; import _merge from "lodash.merge"; import {Observable, of} from "rxjs"; import {catchError, map, tap} from "rxjs/operators"; +import {defaultEnvironment} from "../../../environments/environment.defaults"; @Injectable({ providedIn: "root", }) export class AppConfigService { - public config: any; + public config: typeof defaultEnvironment ; public readonly CONSOLE_WARN_NO_ENVIRONMENT_RUNTIME_AVAILABLE: string = "No runtime config available, using the compiled environment config"; public readonly CONSOLE_ERROR_LOADING_RUNTIME_CONFIG: string = "Error loading runtime config. An empty file named \"environment.runtime.json\" " + "must be available in the application assets/ folder"; @@ -18,8 +19,8 @@ export class AppConfigService { constructor(private injector: Injector) { } - mergeConfig(environment: any): Observable<any> { - if (environment.production === true) { + mergeConfig(env: typeof defaultEnvironment): Observable<any> { + if (env.production === true) { const http = this.injector.get(HttpClient); return http.get(this.CONFIG_FILE_URL) @@ -28,12 +29,12 @@ export class AppConfigService { tap(runtimeConfig => { // Merge runtime environment config into compiled environment. The result is also store // in the config variable - this.config = _merge(environment, runtimeConfig); + this.config = _merge(env, runtimeConfig); }), catchError(err => { if (err.status === 404) { console.warn(this.CONSOLE_WARN_NO_ENVIRONMENT_RUNTIME_AVAILABLE); - this.config = environment; + this.config = env; } else { console.error(this.CONSOLE_ERROR_LOADING_RUNTIME_CONFIG); } @@ -41,7 +42,7 @@ export class AppConfigService { }), ); } else { - this.config = environment; + this.config = env; return of(this.config); } } diff --git a/src/app/core/http/oauth2-interceptor.service.ts b/src/app/core/http/oauth2-interceptor.service.ts index 4d91d7b09..745c4b933 100644 --- a/src/app/core/http/oauth2-interceptor.service.ts +++ b/src/app/core/http/oauth2-interceptor.service.ts @@ -1,9 +1,9 @@ import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http"; -import {Injectable, Optional} from "@angular/core"; -import {OAuthModuleConfig} from "@app/core/auth/oauth-module.config"; +import {Injectable} from "@angular/core"; import {OAuthStorage} from "@app/core/auth/types"; import {Observable, throwError} from "rxjs"; import {catchError} from "rxjs/operators"; +import {AppConfigService} from "@app/core/config/app-config.service"; @Injectable({ providedIn: "root", @@ -11,11 +11,11 @@ import {catchError} from "rxjs/operators"; export class OAuth2Interceptor implements HttpInterceptor { constructor(private authStorage: OAuthStorage, - @Optional() private moduleConfig: OAuthModuleConfig) { + private appConfig: AppConfigService) { } private checkUrl(url: string): boolean { - const found = this.moduleConfig.resourceServer.allowedUrls.find(u => url.startsWith(u)); + const found = this.appConfig.config.allowedUrls.find(u => url.startsWith(u)); return !!found; } @@ -23,17 +23,11 @@ export class OAuth2Interceptor implements HttpInterceptor { const url = req.url.toLowerCase(); - if (!this.moduleConfig) { - return next.handle(req); - } - if (!this.moduleConfig.resourceServer) { - return next.handle(req); - } - if (this.moduleConfig.resourceServer.allowedUrls && !this.checkUrl(url)) { + if (this.appConfig.config.allowedUrls && !this.checkUrl(url)) { return next.handle(req); } - const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken; + const sendAccessToken = this.appConfig.config.sendAccessToken; if (sendAccessToken && this.authStorage.getItem("access_token")) { const token = this.authStorage.getItem("access_token"); diff --git a/src/app/shared/components/paginator/paginator.component.ts b/src/app/shared/components/paginator/paginator.component.ts index e2cbc30a8..00425484d 100644 --- a/src/app/shared/components/paginator/paginator.component.ts +++ b/src/app/shared/components/paginator/paginator.component.ts @@ -1,7 +1,7 @@ import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild} from "@angular/core"; import {MatPaginator, PageEvent} from "@angular/material"; import {PagingModel} from "@app/shared/models/paging.model"; -import {environment} from "../../../../environments/environment"; +import {AppConfigService} from "@app/core/config/app-config.service"; @Component({ selector: "dlcm-paginator", @@ -22,13 +22,13 @@ export class PaginatorComponent { @ViewChild("paginator") paginator: MatPaginator; - constructor() { + constructor(private appConfig: AppConfigService) { this.pageEvent = new EventEmitter<PagingModel>(); this.getDefaultValue(); } private getDefaultValue(): void { - this.pageSizeOptions = environment.pageSizeOptions; + this.pageSizeOptions = this.appConfig.config.pageSizeOptions; } onPageEvent(pageEventMaterial: PageEvent): void { diff --git a/src/app/stores/app.state.ts b/src/app/stores/app.state.ts index 22d549a51..88f706577 100644 --- a/src/app/stores/app.state.ts +++ b/src/app/stores/app.state.ts @@ -1,6 +1,5 @@ -import {ChangeAppLanguage, Login, LoginFail, LoginSuccess, Logout, SetDefaultLanguage, SetMomentLocal} from "@app/stores/app.action"; -import {authConfig} from "@app/auth.config"; -import {BaseState} from "@app/core/models/stores/base.state"; +import {ChangeAppLanguage, Login, LoginFail, LoginSuccess, Logout, SetDefaultLanguage, SetMomentLocal} from "@app/app.action"; +import {BaseStateModel} from "@app/base.state"; import {LanguagesEnum} from "@app/shared/enums/languages.enum"; import {RoutesEnum} from "@app/shared/enums/routes.enum"; import {SessionStorageEnum} from "@app/shared/enums/session-storage.enum"; @@ -14,6 +13,8 @@ import {Observable} from "rxjs"; import {tap} from "rxjs/operators"; import {environment} from "../../environments/environment"; import {OAuth2Service} from "@app/core/auth/oauth2.service"; +import {environment} from "../environments/environment"; +import {AppConfigService} from "@app/core/config/app-config.service"; export interface AppStateModel extends BaseState { appLanguage: LanguagesEnum; @@ -31,14 +32,16 @@ export interface AppStateModel extends BaseState { export class AppState { constructor(private store: Store, private translate: TranslateService, - private oauthService: OAuth2Service) { - this.oauthService.configure(authConfig); + private oauthService: OAuth2Service, + private appConfig: AppConfigService) { this.oauthService.setupAutomaticRefreshToken(); } @Action(InitState) initApplication(ctx: StateContext<AppStateModel>): void { + // TODO put this in separate action + this.appConfig.mergeConfig(environment).subscribe(); ctx.dispatch([ new Login(), new SetDefaultLanguage(), diff --git a/src/environments/environment.defaults.ts b/src/environments/environment.defaults.ts index 69f496f20..c240187cb 100644 --- a/src/environments/environment.defaults.ts +++ b/src/environments/environment.defaults.ts @@ -12,19 +12,33 @@ export const defaultEnvironment = { INGEST_APPLICATION: "https://to-set/ingestion", PREINGEST_APPLICATION: "https://to-set/accession", STORAGION_APPLICATION: "https://to-set/storagion", + theme: ThemeEnum.dlcm, + defaultLanguage: LanguagesEnum.en, pageSizeOptions: [5, 10, 25, 50, 100], defaultPageSizeOption: 5, - oauthTokenEndpoint: "http://localhost/dlcm/authorisation/token", - oauthDummyClientSecret: "123abc", - oauthClientId: "dlcm-test-angular", - oauthScope: "READ", - oauthRequireHttps: false, - oauthLoginUrl: "http://localhost/dlcm/authorisation/authorize", - oauthAllowedUrl: "/api/", - oauthRedirectUrl: window.location.origin, - oauthShowDebugInformation: true, + + // OAuth2 properties + oidc: false, + requestAccessToken: true, + tokenEndpoint: "http://localhost/dlcm/authorisation/token", + dummyClientSecret: "123abc", + clientId: "local-dev-angular", + responseType: "token", + scope: "READ", + requireHttps: false, + loginUrl: "http://localhost/dlcm/authorisation/authorize", + allowedUrls: ["/api/"], + redirectUrl: window.location.origin, + timeoutFactor: 0.75, + postLogoutRedirectUri: "", + logoutUrl: "", + nonceStateSeparator: ";", + disableNonceCheck: false, + sendAccessToken: true, + + // Others defaultNotificationErrorDurationInSeconds: 5, defaultNotificationSuccessDurationInSeconds: 3, defaultNotificationWarningDurationInSeconds: 3, diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index ae327f9f2..106c48dd7 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -10,12 +10,12 @@ export const environment = { INGEST_APPLICATION: "https://test.dlcm.ch/ingestion", PREINGEST_APPLICATION: "https://test.dlcm.ch/accession", STORAGION_APPLICATION: "https://test.dlcm.ch/storagion", - oauthTokenEndpoint: "https://test.dlcm.ch/authorization/oauth/token", - oauthDummyClientSecret: "123abc", - oauthClientId: "dlcm-test-angular", - oauthScope: "READ", - oauthRequireHttps: false, - oauthLoginUrl: "https://test.dlcm.ch/authorization/oauth/authorize", - oauthShowDebugInformation: false, - oauthRedirectUrl: "https://test.dlcm.ch/portal/", + + tokenEndpoint: "https://test.dlcm.ch/authorization/oauth/token", + dummyClientSecret: "123abc", + clientId: "dlcm-test-angular", + scope: "READ", + requireHttps: false, + loginUrl: "https://test.dlcm.ch/authorization/oauth/authorize", + redirectUrl: "https://test.dlcm.ch/portal/", }; -- GitLab