@dbg-riskit/angular-auth
Version:
1 lines • 81.8 kB
Source Map (JSON)
{"version":3,"file":"dbg-riskit-angular-auth.mjs","sources":["../../../../pkg/dbg-riskit/angular-auth/src/lib/auth.config.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/jwt.helper.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/auth.storage.service.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/auth.http.interceptor.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/request.utils.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/nonce.generator.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/well.known.service.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/auth.service.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/auth.routing.flow.service.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/auth.routing.guard.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/auth.module.ts","../../../../pkg/dbg-riskit/angular-auth/src/lib/config.reader.ts","../../../../pkg/dbg-riskit/angular-auth/src/dbg-riskit-angular-auth.ts"],"sourcesContent":["import {InjectionToken} from '@angular/core';\n\nexport type AuthFlowNames =\n 'openid-connect/authorization-code' |\n 'openid-connect/implicit' |\n 'openid-connect/hybrid' |\n 'openid-connect/direct';\n\nexport class AuthFlow {\n private static readonly CONSTANTS_MAP: { [key: string]: AuthFlow } = {};\n public static readonly AUTHORIZATION_CODE = new AuthFlow('openid-connect/authorization-code');\n public static readonly IMPLICIT = new AuthFlow('openid-connect/implicit');\n public static readonly HYBRID = new AuthFlow('openid-connect/hybrid');\n public static readonly DIRECT = new AuthFlow('openid-connect/direct');\n\n private constructor(private readonly _type: string) {\n AuthFlow.CONSTANTS_MAP[_type] = this;\n }\n\n public get type(): string {\n return this._type;\n }\n\n public static byType(type: AuthFlowNames): AuthFlow {\n return AuthFlow.CONSTANTS_MAP[type];\n }\n}\n\nexport type AuthSpecScopes = 'profile' | 'group' | 'email' | 'address' | 'phone';\n\nexport interface AuthConfig {\n loginRoute: string;\n afterLoginRedirectRoute: string;\n wellKnown: string;\n clientID: string;\n clientSecret?: string;\n flow: AuthFlow;\n useNonce?: boolean;\n scope?: Array<AuthSpecScopes | string>;\n}\n\nexport const AUTH_CONFIG = new InjectionToken<AuthConfig>('risk.authConfig');\n","/**\n * Helper class to decode and find JWT expiration.\n */\nimport {base64decode, base64encode} from '@dbg-riskit/common';\nimport {TokenData} from './token.responses';\n\n// @dynamic\nexport class JwtHelper {\n\n public static decodeToken(token: string): TokenData {\n const parts = token.split('.');\n\n if (parts.length !== 3) {\n throw new Error('JWT must have 3 parts');\n }\n\n const decoded = base64decode(parts[1]);\n if (!decoded) {\n throw new Error('Cannot decode the token');\n }\n\n return JSON.parse(decoded);\n }\n\n public static getTokenExpirationDate(token: string,\n property: keyof Pick<TokenData, 'exp' | 'iat'> = 'exp'\n ): Date | null {\n const decoded = JwtHelper.decodeToken(token);\n\n if (!decoded.hasOwnProperty(property)) {\n return null;\n }\n\n const date = new Date(0); // The 0 here is the key, which sets the date to the epoch\n date.setUTCSeconds(decoded[property]);\n\n return date;\n }\n\n public static isTokenExpired(token: string, offsetSeconds?: number): boolean {\n const date = JwtHelper.getTokenExpirationDate(token);\n return JwtHelper.isBefore(date, offsetSeconds);\n }\n\n public static isBefore(date: Date | null, offsetSeconds?: number): boolean {\n offsetSeconds = offsetSeconds || 0;\n\n if (date == null) {\n return false;\n }\n\n // Token expired?\n return date.valueOf() < (new Date().valueOf() + (offsetSeconds * 1000));\n }\n}\n\nexport function encodeTestToken(payload: TokenData): string {\n // Don't actually check or care about the header or signature in angular2-jwt\n return `.${base64encode(JSON.stringify(payload))}.`;\n}\n","import {Injectable} from '@angular/core';\n\nimport {LocalStorage, Logger, Maybe} from '@dbg-riskit/common';\n\nimport {JwtHelper} from './jwt.helper';\n\n@Injectable()\nexport class AuthStorageService {\n\n @LocalStorage('id_token', 'auth')\n public idToken?: string | null;\n\n @LocalStorage('access_token', 'auth')\n public accessToken?: string | null;\n\n @LocalStorage('refresh_token', 'auth')\n public refreshToken?: string | null;\n\n @LocalStorage('sync_diff', 'auth')\n private _timeSyncDiff?: number | null;\n\n @LocalStorage('nonce', 'auth')\n public nonce?: string | null;\n\n @LocalStorage('req_path', 'auth')\n public authRequestedPath?: string | null;\n\n public constructor(private readonly logger: Logger) {\n }\n\n public syncTime(): void {\n const now = Math.floor((new Date().valueOf()) / 1000);\n const computeDiff = (token: Maybe<string>, currDiff = 0): number => {\n if (token) {\n const tokenData = JwtHelper.decodeToken(token);\n if (tokenData.iat) {\n const diff = tokenData.iat - now;\n if (Math.abs(diff) > Math.abs(currDiff)) {\n return diff;\n }\n }\n }\n return currDiff;\n };\n\n this._timeSyncDiff = computeDiff(this.idToken);\n this._timeSyncDiff = computeDiff(this.accessToken, this._timeSyncDiff);\n this._timeSyncDiff = computeDiff(this.refreshToken, this._timeSyncDiff);\n this._timeSyncDiff = Math.floor(this._timeSyncDiff || 0);\n }\n\n private get exp(): Date | null {\n const computeExpiration = (token: Maybe<string>, date: Date | null = null): Date | null => {\n if (token) {\n const tokenExp = JwtHelper.getTokenExpirationDate(token);\n if (date == null || tokenExp == null) {\n return tokenExp;\n }\n\n if (date > tokenExp) {\n return tokenExp;\n }\n }\n return date;\n };\n\n let exp = computeExpiration(this.idToken);\n exp = computeExpiration(this.accessToken, exp);\n return computeExpiration(this.refreshToken, exp);\n }\n\n public get refresh_in(): number {\n const exp = this.exp;\n const now = new Date();\n if (!exp) {\n return 1;\n }\n\n const syncDiff = this._timeSyncDiff || 0;\n\n const expFromNow = Math.floor((exp.valueOf() - now.valueOf()) / 1000) - syncDiff;\n const refreshIn = expFromNow - 60; // Refresh at least a minute before\n this.logger.info(`Tokens will refresh in ${refreshIn}s.`);\n\n return refreshIn > 0 ? (refreshIn * 1000) : 1;\n }\n\n public clear() {\n this.idToken = null;\n this.accessToken = null;\n this.refreshToken = null;\n this._timeSyncDiff = null;\n this.nonce = null;\n }\n}\n","import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';\nimport {Injectable} from '@angular/core';\nimport {HttpRequestHeaders} from '@dbg-riskit/angular-http';\nimport {AuthTokenType, Maybe} from '@dbg-riskit/common';\nimport {defer, Observable} from 'rxjs';\nimport {AuthStorageService} from './auth.storage.service';\n\nexport class AuthConfigConsts {\n public static DEFAULT_HEADER_NAME = 'Authorization';\n public static HEADER_PREFIX_BEARER = 'Bearer ';\n}\n\n@Injectable()\nexport class AuthHttpInterceptor implements HttpInterceptor {\n\n public constructor(private readonly authStorage: AuthStorageService) {\n }\n\n public intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {\n return defer(() => {\n if (request.headers instanceof HttpRequestHeaders) {\n const tokenType = request.headers.tokenType;\n switch (tokenType) {\n case AuthTokenType.API:\n return handleInterception(this.authStorage.idToken, request, next);\n case AuthTokenType.AUTH:\n return handleInterception(this.authStorage.accessToken, request, next);\n default:\n return next.handle(request);\n }\n }\n return next.handle(request);\n });\n }\n}\n\nfunction handleInterception(token: Maybe<string>, request: HttpRequest<unknown>,\n next: HttpHandler): Observable<HttpEvent<unknown>> {\n if (token) {\n request = request.clone({\n setHeaders: {\n [AuthConfigConsts.DEFAULT_HEADER_NAME]: `${AuthConfigConsts.HEADER_PREFIX_BEARER}${token}`\n }\n });\n }\n return next.handle(request);\n}\n","import {Location} from '@angular/common';\n\nexport interface OpenIDQueryParams {\n error: string | null;\n errorDesc: string | null;\n code: string | null;\n id_token: string | null;\n access_token: string | null;\n}\n\n// @dynamic\nexport class RequestUtils {\n\n public static get locationHref(): string {\n return window.location.href;\n }\n\n public static set locationHref(href: string) {\n window.location.href = href;\n }\n\n private static getURLPath(href: string) {\n const link: HTMLAnchorElement = document.createElement('a');\n link.href = href;\n return link.pathname;\n }\n\n public static getOrigin(): string {\n const port = window.location.port ? `:${window.location.port}` : '';\n return window.location.origin || (`${window.location.protocol}//${window.location.hostname}${port}`);\n }\n\n public static getBaseURL(location: Location, relativeURL = '/'): string {\n let baseURL = '';\n if (location) {\n baseURL = RequestUtils.getURLPath(location.prepareExternalUrl('/'));\n }\n if (!baseURL.startsWith('/')) {\n baseURL = '/' + baseURL;\n }\n if (!baseURL.endsWith('/')) {\n baseURL += '/';\n }\n if (relativeURL.startsWith('/')) {\n relativeURL = relativeURL.substring(1);\n }\n\n const origin = RequestUtils.getOrigin();\n return origin + baseURL + relativeURL;\n }\n\n public static getQueryParam(name: string): string | null {\n const url = RequestUtils.locationHref;\n name = name.replace(/[\\[\\]]/g, '\\\\$&');\n const regex = new RegExp(`[?#&]${name}(=([^&#]*)|&|#|$)`);\n const results = regex.exec(url);\n if (!results) {\n return null;\n }\n if (!results[2]) {\n return '';\n }\n return decodeURIComponent(results[2].replace(/\\+/g, ' '));\n }\n\n public static getOpenIDQueryParams(): OpenIDQueryParams {\n const error = RequestUtils.getQueryParam('error');\n const errorDesc = RequestUtils.getQueryParam('error_description');\n const code = RequestUtils.getQueryParam('code');\n const idToken = RequestUtils.getQueryParam('id_token');\n const accessToken = RequestUtils.getQueryParam('access_token');\n return {\n error,\n errorDesc,\n code,\n id_token : idToken,\n access_token: accessToken\n };\n }\n\n public static hasOpenIDQueryParams(): boolean {\n const params = RequestUtils.getOpenIDQueryParams();\n return params.error != null\n || params.errorDesc != null\n || params.code != null\n || params.id_token != null\n || params.access_token != null;\n }\n}\n","import {secureRandom} from '@dbg-riskit/common';\nimport {defer, Observable, of} from 'rxjs';\n\n// @dynamic\nexport class NonceGenerator {\n\n public static generateNonce(): Observable<string> {\n return defer(() => {\n const guidHolder = 'xxx-xxyxxxxx4xxxyxxxxxx-xxxx4xxx';\n const hex = '0123456789abcdef';\n let r = 0;\n let guidResponse = '';\n for (const placeHolder of guidHolder) {\n if (placeHolder !== '-' && placeHolder !== '4') {\n // each x and y needs to be random\n r = Math.floor(secureRandom() * 16);\n }\n\n if (placeHolder === 'x') {\n guidResponse += hex[r];\n } else if (placeHolder === 'y') {\n\n /* eslint-disable no-bitwise */\n\n // clock-seq-and-reserved first hex is filtered and remaining hex values are random\n r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0??\n r |= 0x8; // set pos 3 to 1 as 1???\n\n /* eslint-enable */\n\n guidResponse += hex[r];\n } else {\n guidResponse += placeHolder;\n }\n }\n return of(guidResponse);\n });\n }\n}\n","import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';\nimport {Inject, Injectable} from '@angular/core';\nimport {CONTENT_TYPE} from '@dbg-riskit/common';\nimport {Observable} from 'rxjs';\nimport {map, shareReplay} from 'rxjs/operators';\nimport {AUTH_CONFIG, AuthConfig} from './auth.config';\n\nexport interface WellKnown {\n endpoints: {\n auth: string;\n token: string;\n logout: string;\n };\n issuer: string;\n}\n\n@Injectable()\nexport class WellKnownService {\n\n private readonly _wellKnown: Observable<WellKnown>;\n\n public constructor(private readonly http: HttpClient,\n @Inject(AUTH_CONFIG) private readonly authConfig: AuthConfig) {\n const params = new HttpParams();\n params.append('_', Date.now().toFixed(0));\n this._wellKnown = this.http.get<{\n authorization_endpoint: string;\n token_endpoint: string;\n end_session_endpoint: string;\n issuer: string;\n }>(this.authConfig.wellKnown, {\n params,\n headers: new HttpHeaders({\n accept: CONTENT_TYPE.APPLICATION_JSON\n })\n }).pipe(\n map((wellKnown) => (\n {\n endpoints: {\n auth : wellKnown.authorization_endpoint,\n token : wellKnown.token_endpoint,\n logout: wellKnown.end_session_endpoint\n },\n issuer : wellKnown.issuer\n }\n )\n ),\n shareReplay(1)\n );\n }\n\n public get wellKnown(): Observable<WellKnown> {\n return this._wellKnown;\n }\n}\n","import {Location} from '@angular/common';\nimport {Inject, Injectable} from '@angular/core';\nimport {AuthProvider, AuthTokenType, ErrorType, ReplaySubjectExt, toString, UserInfo} from '@dbg-riskit/common';\nimport {HttpService} from '@dbg-riskit/angular-http';\nimport {EMPTY, Observable, of, throwError} from 'rxjs';\nimport {catchError, defaultIfEmpty, first, map, shareReplay, switchMap, tap} from 'rxjs/operators';\nimport {AUTH_CONFIG, AuthConfig, AuthFlow} from './auth.config';\nimport {AuthStorageService} from './auth.storage.service';\nimport {JwtHelper} from './jwt.helper';\nimport {NonceGenerator} from './nonce.generator';\nimport {OpenIDQueryParams, RequestUtils} from './request.utils';\nimport {AuthResponse, TokenData} from './token.responses';\nimport {WellKnown, WellKnownService} from './well.known.service';\n\nexport const AUTH_CHECK_INTERVAL = 60000;\n\nconst OPEN_ID_SCOPE = 'openid';\nconst DEFAULT_OPEN_ID_SCOPES = ['profile', 'email', 'address', 'phone'];\n\nconst UNSUPPORTED_AUTH_FLOW_TYPE = 'Unsupported auth flow type!';\n\nfunction unsupportedFlowTypeError(): Error {\n return new Error(UNSUPPORTED_AUTH_FLOW_TYPE);\n}\n\n@Injectable()\nexport class AuthService implements AuthProvider {\n\n private readonly _loggedInStream: ReplaySubjectExt<boolean> = new ReplaySubjectExt<boolean>(1);\n\n private tokenData?: UserInfo;\n\n private refreshTimeout?: NodeJS.Timer | null;\n private authCheckInterval?: NodeJS.Timer | null;\n\n private readonly redirectURL: string;\n private readonly openIDScope: string;\n private readonly _initService: Observable<WellKnown>;\n\n public constructor(@Inject(AUTH_CONFIG) private readonly authConfig: AuthConfig,\n private readonly wellKnownService: WellKnownService,\n private readonly http: HttpService,\n private readonly storage: AuthStorageService,\n location: Location) {\n\n // Use false only if === false\n this.authConfig.useNonce = this.authConfig.useNonce !== false;\n\n this.redirectURL = RequestUtils.getBaseURL(location, this.authConfig.loginRoute);\n this.openIDScope = [OPEN_ID_SCOPE].concat(this.authConfig.scope || DEFAULT_OPEN_ID_SCOPES).join(' ');\n\n // Try to load tokens and process them\n this._initService = this.wellKnownService.wellKnown.pipe(\n switchMap((wellKnown: WellKnown) => this.processToken({\n id_token : this.storage.idToken,\n access_token : this.storage.accessToken,\n refresh_token: this.storage.refreshToken\n }, false) // Do not sync time as we are now in the future ;)\n .pipe(\n catchError(() => this.tryToRefresh()),\n defaultIfEmpty(0),\n map(() => wellKnown)\n )\n ),\n shareReplay(1)\n );\n }\n\n public get loggedIn(): Observable<boolean> {\n return this._initService.pipe(\n map(() => !!this.tokenData)\n );\n }\n\n public get userProfile(): Observable<UserInfo | undefined> {\n return this._initService.pipe(\n map(() => this.tokenData)\n );\n }\n\n public get loggedInStream(): Observable<boolean> {\n return this._loggedInStream.asObservable();\n }\n\n public emitLoginStatusChange(status: boolean) {\n if (this._loggedInStream.lastValue !== status) {\n this._loggedInStream.next(status);\n }\n }\n\n // <editor-fold defaultstate=\"collapsed\" desc=\"Indirect login\">\n\n public loginViaAuthService(): Observable<boolean> {\n return this.wellKnownService.wellKnown.pipe(\n switchMap((wellKnown: WellKnown) =>\n NonceGenerator.generateNonce().pipe(\n map((nonce) => ({\n wellKnown,\n nonce\n }))\n )\n ),\n map(({wellKnown, nonce}) => {\n let responseType: string;\n switch (this.authConfig.flow) {\n case AuthFlow.AUTHORIZATION_CODE:\n responseType = 'code';\n break;\n case AuthFlow.IMPLICIT:\n responseType = 'id_token token';\n break;\n case AuthFlow.HYBRID:\n responseType = 'code id_token';\n break;\n default:\n throw unsupportedFlowTypeError();\n }\n let location = `${wellKnown.endpoints.auth\n }?response_type=${encodeURIComponent(responseType)\n }&scope=${encodeURIComponent(this.openIDScope)\n }&client_id=${encodeURIComponent(this.authConfig.clientID)\n }&redirect_uri=${encodeURIComponent(this.redirectURL)}`;\n if (this.authConfig.useNonce) {\n this.storage.nonce = nonce;\n location += `&nonce=${encodeURIComponent(nonce)}`;\n }\n RequestUtils.locationHref = location;\n return true;\n })\n );\n }\n\n public checkLocationForLoginData(): Observable<boolean> {\n return this.wellKnownService.wellKnown.pipe(\n switchMap(() => {\n const params = RequestUtils.getOpenIDQueryParams();\n\n if (params.error) {\n return throwError({\n message : `(${params.error}) ${params.errorDesc}`,\n status : 500,\n errorType: ErrorType.AUTH\n });\n }\n\n switch (this.authConfig.flow) {\n case AuthFlow.HYBRID:\n return this.checkParametersHybridFlow(params);\n case AuthFlow.AUTHORIZATION_CODE:\n return this.checkParametersAuthCodeFlow(params);\n case AuthFlow.IMPLICIT:\n return this.checkParametersImplicitFlow(params);\n default:\n return throwError(unsupportedFlowTypeError());\n }\n })\n );\n }\n\n private checkParametersHybridFlow(params: OpenIDQueryParams) {\n if (!params.id_token || !params.code) {\n return throwError({\n message : 'Authentication server did not sent required data.',\n status : 500,\n errorType: ErrorType.AUTH\n });\n }\n // Login using token first\n return this.processToken({\n id_token: params.id_token\n }).pipe(\n tap(() => {\n // Request permanent token + refresh token\n this.requestTokenBasedOnCode(params.code!, this.redirectURL).pipe(\n catchError(() => this.logout())\n ).subscribe((tokenValid: boolean) => {\n if (!tokenValid) {\n this.logout().pipe(first()).subscribe();\n }\n });\n })\n );\n }\n\n private checkParametersAuthCodeFlow(params: OpenIDQueryParams) {\n if (!params.code) {\n return throwError({\n message : 'Authentication server did not send required data.',\n status : 500,\n errorType: ErrorType.AUTH\n });\n }\n // Request token + refresh token\n return this.requestTokenBasedOnCode(params.code, this.redirectURL);\n }\n\n private checkParametersImplicitFlow(params: OpenIDQueryParams) {\n if (!params.id_token || !params.access_token) {\n return throwError({\n message : 'Authentication server did not send required data.',\n status : 500,\n errorType: ErrorType.AUTH\n });\n }\n // Login using id_token and access_token. Refresh is not permited here\n return this.processToken({\n id_token : params.id_token,\n access_token: params.access_token\n });\n }\n\n private requestTokenBasedOnCode(code: string, redirectURI: string): Observable<boolean> {\n return this.wellKnownService.wellKnown.pipe(\n switchMap((wellKnown: WellKnown) => this.http.post<AuthResponse>({\n resourceURL: wellKnown.endpoints.token,\n data : HttpService.toHttpParams({\n code,\n redirect_uri : redirectURI,\n grant_type : 'authorization_code',\n nonce : this.authConfig.useNonce ? this.storage.nonce : null,\n client_id : this.authConfig.clientID,\n client_secret: this.authConfig.clientSecret\n }),\n tokenType : AuthTokenType.NONE,\n endpoint : '' // Do not use any prefix\n })),\n switchMap((response: AuthResponse) => this.processToken(response))\n );\n }\n\n // </editor-fold>\n\n // <editor-fold defaultstate=\"collapsed\" desc=\"Direct login\">\n\n public directLogin(username: string, password: string): Observable<boolean> {\n return this.wellKnownService.wellKnown.pipe(\n switchMap((wellKnown: WellKnown) => this.http.post<AuthResponse>({\n resourceURL: wellKnown.endpoints.token,\n data : HttpService.toHttpParams({\n username,\n password,\n grant_type : 'password',\n scope : this.openIDScope,\n client_id : this.authConfig.clientID,\n client_secret: this.authConfig.clientSecret\n }),\n tokenType : AuthTokenType.NONE,\n endpoint : '' // Do not use any prefix\n })),\n switchMap((response: AuthResponse) => this.processToken(response))\n );\n }\n\n // </editor-fold>\n\n public processToken(response: AuthResponse, syncTime = true): Observable<boolean> {\n return this.wellKnownService.wellKnown.pipe(\n switchMap(() => {\n if (this.validateResponseNonce(response)) {\n return throwError({\n status : 401,\n message : 'Non-matching nonce.',\n errorType: ErrorType.AUTH\n });\n }\n if (AuthService.validateResponseTokenType(response)) {\n return throwError({\n status : 401,\n message : 'Invalid token type.',\n errorType: ErrorType.AUTH\n });\n }\n\n return this.storeTokensIfValid(response, syncTime);\n })\n );\n }\n\n private validateResponseNonce(response: AuthResponse) {\n return this.authConfig.useNonce && response.nonce && response.nonce !== this.storage.nonce;\n }\n\n private static validateResponseTokenType(response: AuthResponse) {\n return response.token_type && response.token_type.toLocaleLowerCase() !== 'bearer';\n }\n\n private storeTokensIfValid(response: AuthResponse, syncTime: boolean) {\n if (response.id_token == null) {\n return throwError({\n status : 401,\n message : 'Authentication failed. Server did not generate a token.',\n errorType: ErrorType.AUTH\n });\n }\n\n this.storage.idToken = response.id_token;\n const chain = this.validateToken(response.id_token).pipe(\n tap((tokenData: TokenData) => {\n this.tokenData = tokenData;\n })\n );\n if (response.access_token != null) {\n chain.pipe(switchMap(() => this.validateToken(response.access_token!)));\n }\n return chain.pipe(\n map(() => {\n // store username and token in local storage to keep user logged in between page refreshes\n this.storage.idToken = response.id_token;\n this.storage.accessToken = response.access_token;\n if (response.refresh_token) {\n this.storage.refreshToken = response.refresh_token;\n }\n if (syncTime) {\n // Sync our local time with the auth server time as we need to refresh tokens\n // at correct time point. We use token iat here as this is the\n // \"current server time\" - \"request duration (ignored as it is small, max. few sec.)\".\n this.storage.syncTime();\n }\n this.afterLogin();\n return true;\n })\n );\n }\n\n private afterLogin(): void {\n switch (this.authConfig.flow) {\n case AuthFlow.DIRECT:\n case AuthFlow.AUTHORIZATION_CODE:\n this.setupAuthCheck();\n this.setupTokenRefresh();\n this.emitLoginStatusChange(true);\n break;\n case AuthFlow.HYBRID:\n if (this.storage.accessToken) {\n this.setupAuthCheck();\n this.setupTokenRefresh();\n }\n this.emitLoginStatusChange(true);\n break;\n case AuthFlow.IMPLICIT:\n this.setupAuthCheck();\n this.emitLoginStatusChange(true);\n break;\n default:\n throw unsupportedFlowTypeError();\n }\n }\n\n public logout(): Observable<never> {\n const logout = () => {\n // remove user from local storage and clear http auth header\n delete this.tokenData;\n this.storage.clear();\n this.disableAuthCheck();\n this.disableTokenRefresh();\n this.emitLoginStatusChange(false);\n return EMPTY;\n };\n const idToken = this.storage.idToken;\n if (idToken) {\n // Send logout request first...\n return this.wellKnownService.wellKnown.pipe(\n switchMap((wellKnown: WellKnown) => this.http.get<void>({\n resourceURL: wellKnown.endpoints.logout,\n params : {\n id_token_hint: idToken\n },\n tokenType : AuthTokenType.NONE,\n endpoint : '' // Do not use any prefix\n })),\n catchError(logout),\n switchMap(logout)\n );\n } else {\n return this.wellKnownService.wellKnown.pipe(\n catchError(logout),\n switchMap(logout)\n );\n }\n }\n\n // <editor-fold defaultstate=\"collapsed\" desc=\"Periodic Auth check\">\n\n private setupAuthCheck() {\n this.disableAuthCheck();\n this.authCheckInterval = setInterval(() => this.checkAuth(), AUTH_CHECK_INTERVAL);\n }\n\n private disableAuthCheck() {\n if (this.authCheckInterval != null) {\n clearInterval(this.authCheckInterval);\n this.authCheckInterval = null;\n }\n }\n\n private checkAuth(): void {\n this.wellKnownService.wellKnown.pipe(\n switchMap(() => {\n if (!this.tokenData\n || !this.storage.idToken\n || !this.storage.accessToken\n || this.storage.refresh_in <= 0) {\n return this.logout();\n }\n return of(true);\n })\n ).subscribe();\n }\n\n // </editor-fold>\n\n // <editor-fold defaultstate=\"collapsed\" desc=\"Token refresh\">\n\n private setupTokenRefresh(): void {\n this.disableTokenRefresh();\n this.refreshTimeout = setTimeout(() => this.refreshToken(), this.storage.refresh_in);\n }\n\n private disableTokenRefresh(): void {\n if (this.refreshTimeout != null) {\n clearTimeout(this.refreshTimeout);\n this.refreshTimeout = null;\n }\n }\n\n private tryToRefresh(): Observable<boolean> {\n return this.wellKnownService.wellKnown.pipe(\n switchMap((wellKnown: WellKnown) => {\n if (!this.storage.refreshToken) {\n return throwError({\n message : 'Could not refresh. Not logged in!',\n status : 500,\n errorType: ErrorType.AUTH\n });\n }\n return this.http.post<AuthResponse>({\n resourceURL: wellKnown.endpoints.token,\n data : HttpService.toHttpParams({\n grant_type : 'refresh_token',\n client_id : this.authConfig.clientID,\n client_secret: this.authConfig.clientSecret,\n refresh_token: this.storage.refreshToken\n }),\n tokenType : AuthTokenType.NONE,\n endpoint : '' // Do not use any prefix\n });\n }),\n switchMap((response: AuthResponse) => this.processToken(response)),\n catchError(() => of(false))\n );\n }\n\n private refreshToken(): void {\n this.tryToRefresh().pipe(\n switchMap((refreshed: boolean) => {\n if (!refreshed) {\n return this.logout();\n }\n return EMPTY;\n })\n ).subscribe();\n }\n\n // </editor-fold>\n\n // <editor-fold defaultstate=\"collapsed\" desc=\"Token validation\">\n\n private validateToken(token: string): Observable<TokenData> {\n return this.wellKnownService.wellKnown.pipe(\n switchMap((wellKnown: WellKnown) => {\n\n function throwCatchedError(err: unknown) {\n return throwError({\n status : 500,\n message : err ? toString(err) : 'Error parsing token from auth response!',\n errorType: ErrorType.AUTH\n });\n }\n\n try {\n const tokenData: TokenData = JwtHelper.decodeToken(token);\n\n // Check expiration\n return AuthService.checkTokenExpiration(token).pipe(\n // Check nonce\n switchMap(() => this.checkTokenNonce(tokenData)),\n\n // Check issuer\n switchMap(() => AuthService.checkTokenIssuer(tokenData, wellKnown)),\n\n // Check audience\n switchMap(() => this.checkTokenAudience(tokenData)),\n\n map(() => tokenData),\n catchError(throwCatchedError)\n );\n } catch (err) {\n return throwCatchedError(err);\n }\n }));\n }\n\n private static checkTokenExpiration(token: string): Observable<void> {\n if (JwtHelper.isTokenExpired(token)) {\n return throwError({\n status : 500,\n message : 'Invalid token expiration!',\n errorType: ErrorType.AUTH\n });\n }\n return of(undefined);\n }\n\n private checkTokenNonce(tokenData: TokenData): Observable<void> {\n if (this.authConfig.useNonce) {\n switch (this.authConfig.flow) {\n case AuthFlow.AUTHORIZATION_CODE:\n case AuthFlow.HYBRID:\n case AuthFlow.IMPLICIT:\n if (tokenData.nonce !== this.storage.nonce) {\n return throwError({\n status : 500,\n message : 'Non-matching nonce!',\n errorType: ErrorType.AUTH\n });\n }\n break;\n case AuthFlow.DIRECT:\n // No check here - nonce not supported\n break;\n default:\n return throwError({\n status : 500,\n message : UNSUPPORTED_AUTH_FLOW_TYPE,\n errorType: ErrorType.AUTH\n });\n }\n }\n\n return of(undefined);\n }\n\n private static checkTokenIssuer(tokenData: TokenData, wellKnown: WellKnown): Observable<void> {\n if (tokenData.iss !== wellKnown.issuer) {\n return throwError({\n status : 500,\n message : 'Invalid token issuer!',\n errorType: ErrorType.AUTH\n });\n }\n\n return of(undefined);\n }\n\n private checkTokenAudience(tokenData: TokenData): Observable<void> {\n if (Array.isArray(tokenData.aud)) {\n if (!tokenData.aud.some((aud: string) => aud === this.authConfig.clientID)\n || tokenData.azp !== this.authConfig.clientID) {\n return throwError({\n status : 500,\n message : 'Invalid token audience!',\n errorType: ErrorType.AUTH\n });\n }\n\n } else if (tokenData.aud !== this.authConfig.clientID) {\n return throwError({\n status : 500,\n message : 'Invalid token audience!',\n errorType: ErrorType.AUTH\n });\n }\n\n return of(undefined);\n }\n\n // </editor-fold>\n}\n","import {Inject, Injectable} from '@angular/core';\nimport {Router, RouterStateSnapshot} from '@angular/router';\nimport {HttpService} from '@dbg-riskit/angular-http';\nimport {defer, EMPTY, merge, Observable, of} from 'rxjs';\nimport {defaultIfEmpty, first, map, switchMap} from 'rxjs/operators';\nimport {AUTH_CONFIG, AuthConfig, AuthFlow} from './auth.config';\nimport {AuthService} from './auth.service';\nimport {AuthStorageService} from './auth.storage.service';\nimport {RequestUtils} from './request.utils';\nimport {WellKnownService} from './well.known.service';\n\n@Injectable()\nexport class AuthRoutingFlowService {\n\n public constructor(private readonly router: Router,\n private readonly http: HttpService,\n private readonly authService: AuthService,\n private readonly storage: AuthStorageService,\n @Inject(AUTH_CONFIG) private readonly authConfig: AuthConfig,\n private readonly wellKnownService: WellKnownService) {\n\n // Subscribe for login changes so we do correct navigation after logout\n this.authService.loggedIn\n .subscribe(\n () => {\n try {\n merge(this.http.unauthorized.pipe(map(() => false)),\n this.authService.loggedInStream)\n .subscribe((loggedIn: boolean) => {\n if (!loggedIn) {\n this.logout().pipe(first()).subscribe();\n }\n });\n } catch (ignore) {\n // Happens in tests only\n }\n });\n }\n\n public get authFlow(): AuthFlow {\n return this.authConfig.flow;\n }\n\n public get authorizationCodeFlow(): boolean {\n return this.authFlow === AuthFlow.AUTHORIZATION_CODE;\n }\n\n public get implicitFlow(): boolean {\n return this.authFlow === AuthFlow.IMPLICIT;\n }\n\n public get hybridFlow(): boolean {\n return this.authFlow === AuthFlow.HYBRID;\n }\n\n public get directFlow(): boolean {\n return this.authFlow === AuthFlow.DIRECT;\n }\n\n public logout(state?: RouterStateSnapshot): Observable<boolean> {\n return this.authService.loggedIn.pipe(\n switchMap((res: boolean) => {\n if (res) {\n return this.authService.logout();\n }\n return EMPTY;\n }),\n defaultIfEmpty(0),\n switchMap(() => {\n this.storeRequestedPath(state);\n return this.doLogin();\n })\n );\n }\n\n public login(username?: string, password?: string): Observable<boolean> {\n return defer(() => {\n switch (this.authFlow) {\n case AuthFlow.DIRECT:\n if (username == null || password == null) {\n throw new Error('Username and password expexted!');\n }\n return this.authService.directLogin(username, password).pipe(\n switchMap((res: boolean) => this.loginRedirect(res))\n );\n case AuthFlow.AUTHORIZATION_CODE:\n case AuthFlow.IMPLICIT:\n case AuthFlow.HYBRID:\n return this.doLogin();\n default:\n throw new Error('Unsupported auth flow detected!');\n }\n });\n }\n\n public loginViaService(): Observable<boolean> {\n return this.authService.loggedIn.pipe(\n switchMap((res: boolean) => {\n switch (this.authFlow) {\n case AuthFlow.AUTHORIZATION_CODE:\n case AuthFlow.IMPLICIT:\n case AuthFlow.HYBRID:\n if (res) {\n return this.loginRedirect(res);\n }\n if (!RequestUtils.hasOpenIDQueryParams()) {\n return this.doLogin().pipe(map(() => res));\n }\n return this.authService.checkLocationForLoginData().pipe(\n switchMap((loggedIn: boolean) => this.loginRedirect(loggedIn))\n );\n case AuthFlow.DIRECT:\n return of(true);\n default:\n throw new Error('Unknown auth flow detected!');\n }\n })\n );\n }\n\n public storeRequestedPath(state: RouterStateSnapshot = this.router.routerState.snapshot): void {\n if (state.url.startsWith(this.authConfig.loginRoute)) {\n this.storage.authRequestedPath = null;\n } else {\n this.storage.authRequestedPath = state.url;\n }\n }\n\n private loginRedirect(res: boolean): Observable<boolean> {\n return this.wellKnownService.wellKnown.pipe(\n switchMap(() => {\n if (res) {\n if (this.storage.authRequestedPath) {\n this.router.navigateByUrl(this.storage.authRequestedPath);\n this.storage.authRequestedPath = null;\n } else {\n this.router.navigate([this.authConfig.afterLoginRedirectRoute]);\n }\n return of(res);\n } else {\n return this.doLogin().pipe(map(() => res));\n }\n })\n );\n }\n\n private doLogin(): Observable<boolean> {\n switch (this.authFlow) {\n case AuthFlow.DIRECT:\n return this.wellKnownService.wellKnown.pipe(\n map(() => {\n this.router.navigate([this.authConfig.loginRoute]);\n return true;\n })\n );\n case AuthFlow.AUTHORIZATION_CODE:\n case AuthFlow.IMPLICIT:\n case AuthFlow.HYBRID:\n return this.authService.loginViaAuthService();\n default:\n throw new Error('Unknown auth flow detected!');\n }\n }\n}\n","import {Injectable} from '@angular/core';\nimport {ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot} from '@angular/router';\nimport {Observable, of} from 'rxjs';\nimport {map, switchMap} from 'rxjs/operators';\nimport {AuthRoutingFlowService} from './auth.routing.flow.service';\nimport {AuthService} from './auth.service';\n\n@Injectable()\nexport class AuthGuard implements CanActivate, CanActivateChild {\n\n public constructor(private readonly authService: AuthService,\n private readonly authRoutingFlowService: AuthRoutingFlowService) {\n }\n\n public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {\n return this.authService.loggedIn.pipe(\n switchMap((res: boolean) => {\n if (res) {\n return of(true);\n }\n\n return this.authRoutingFlowService.logout(state).pipe(\n map(() => false)\n );\n })\n );\n }\n\n public canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {\n return this.canActivate(childRoute, state);\n }\n}\n","import {HTTP_INTERCEPTORS} from '@angular/common/http';\nimport {InjectionToken, ModuleWithProviders, NgModule, Optional, SkipSelf} from '@angular/core';\nimport {RouterModule} from '@angular/router';\n\nimport {AUTH_PROVIDER} from '@dbg-riskit/angular-common';\nimport {HttpConfig, HttpModule} from '@dbg-riskit/angular-http';\nimport {LoggingModule} from '@dbg-riskit/angular-logging';\n\nimport {AUTH_CONFIG, AuthConfig} from './auth.config';\nimport {AuthHttpInterceptor} from './auth.http.interceptor';\nimport {AuthRoutingFlowService} from './auth.routing.flow.service';\nimport {AuthGuard} from './auth.routing.guard';\nimport {AuthService} from './auth.service';\nimport {AuthStorageService} from './auth.storage.service';\nimport {WellKnownService} from './well.known.service';\n\n// AoT workaround - we have to provide it using factory to prevent compiler from replacing\n// \"window.prop\" expressions.\nexport const TMP_HTTP_CONFIG = new InjectionToken<HttpConfig>('risk.auth.tmp_http_provider');\n\n@NgModule({\n imports : [\n RouterModule,\n HttpModule,\n LoggingModule\n ],\n providers: [\n AuthStorageService,\n AuthService,\n AuthGuard,\n AuthRoutingFlowService,\n WellKnownService,\n {\n provide : AUTH_PROVIDER,\n useExisting: AuthService\n },\n {\n provide : HTTP_INTERCEPTORS,\n useClass: AuthHttpInterceptor,\n multi : true\n }\n ]\n})\nexport class AuthModule {\n\n public constructor(@Optional() @SkipSelf() parentModule: AuthModule) {\n if (parentModule) {\n throw new Error(\n 'AuthModule is already loaded. Import it in the AppModule only');\n }\n }\n\n // AoT workaround - we have to provide it using factory to prevent compiler from replacing\n // \"window.prop\" expressions.\n public static forAuthConfig(config: () => AuthConfig): ModuleWithProviders<AuthModule> {\n return {\n ngModule : AuthModule,\n providers: [\n {\n provide : AUTH_CONFIG,\n useFactory: config\n }\n ]\n };\n }\n}\n","import {AuthConfig, AuthFlow, AuthFlowNames, AuthSpecScopes} from './auth.config';\n\ndeclare namespace window {\n let authWellKnownEndpoint: string;\n let authClientID: string;\n let authClientSecret: string;\n let authUseNonce: boolean;\n let authScopes: AuthSpecScopes[];\n let authFlow: AuthFlowNames;\n}\n\nexport type PartialAuthConfig = Pick<AuthConfig, Exclude<keyof AuthConfig, 'loginRoute' | 'afterLoginRedirectRoute'>>;\n\nexport function readAuthConfig(): PartialAuthConfig {\n return {\n wellKnown : window.authWellKnownEndpoint,\n clientID : window.authClientID,\n clientSecret: window.authClientSecret,\n flow : AuthFlow.byType(window.authFlow),\n scope : window.authScopes,\n useNonce : window.authUseNonce == null ? true : window.authUseNonce\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public_api';\n"],"names":["i1.AuthStorageService","i1.WellKnownService","i3.AuthStorageService","i5.WellKnownService","i1.AuthService","i2.AuthRoutingFlowService"],"mappings":";;;;;;;;;;;;;;;;;MAQa,QAAQ,CAAA;AAOjB,IAAA,WAAA,CAAqC,KAAa,EAAA;AAAb,QAAA,IAAK,CAAA,KAAA,GAAL,KAAK,CAAQ;AAC9C,QAAA,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;KACxC;AAED,IAAA,IAAW,IAAI,GAAA;QACX,OAAO,IAAI,CAAC,KAAK,CAAC;KACrB;IAEM,OAAO,MAAM,CAAC,IAAmB,EAAA;AACpC,QAAA,OAAO,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;KACvC;;AAhBuB,QAAa,CAAA,aAAA,GAAgC,EAAE,CAAC;AACjD,QAAA,CAAA,kBAAkB,GAAG,IAAI,QAAQ,CAAC,mCAAmC,CAAC,CAAC;AACvE,QAAA,CAAA,QAAQ,GAAG,IAAI,QAAQ,CAAC,yBAAyB,CAAC,CAAC;AACnD,QAAA,CAAA,MAAM,GAAG,IAAI,QAAQ,CAAC,uBAAuB,CAAC,CAAC;AAC/C,QAAA,CAAA,MAAM,GAAG,IAAI,QAAQ,CAAC,uBAAuB,CAAC,CAAC;MA4B7D,WAAW,GAAG,IAAI,cAAc,CAAa,iBAAiB;;ACzC3E;;AAEG;AAIH;MACa,SAAS,CAAA;IAEX,OAAO,WAAW,CAAC,KAAa,EAAA;QACnC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAE/B,QAAA,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;AACpB,YAAA,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;AAC5C,SAAA;QAED,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,IAAI,CAAC,OAAO,EAAE;AACV,YAAA,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;AAC9C,SAAA;AAED,QAAA,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;KAC9B;AAEM,IAAA,OAAO,sBAAsB,CAAC,KAAa,EACb,WAAiD,KAAK,EAAA;QAEvF,MAAM,OAAO,GAAG,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAE7C,QAAA,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE;AACnC,YAAA,OAAO,IAAI,CAAC;AACf,SAAA;QAED,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AAEtC,QAAA,OAAO,IAAI,CAAC;KACf;AAEM,IAAA,OAAO,cAAc,CAAC,KAAa,EAAE,aAAsB,EAAA;QAC9D,MAAM,IAAI,GAAG,SAAS,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC;QACrD,OAAO,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;KAClD;AAEM,IAAA,OAAO,QAAQ,