File

libs/oauth/src/lib/oauth.service.ts

Index

Methods

Constructor

constructor(initialOauthConfigurations: OauthConfig[], storage: Storage, location: Location, locationService: LocationService, http: HttpClient, router: Router, locationStrategy: LocationStrategy, platformLocation: PlatformLocation, talyRoutingUtilsService: TalyRoutingUtilsService)
Parameters :
Name Type Optional
initialOauthConfigurations OauthConfig[] No
storage Storage No
location Location No
locationService LocationService No
http HttpClient No
router Router No
locationStrategy LocationStrategy No
platformLocation PlatformLocation No
talyRoutingUtilsService TalyRoutingUtilsService No

Methods

Async authorize
authorize(oauthConfigName?: string, clientState?: Record)
Parameters :
Name Type Optional Description
oauthConfigName string Yes
  • if set, will use the given configuration. If left undefined, will use the default configuration.
clientState Record<string | > Yes
Returns : Promise<void>
clearQueryParams
clearQueryParams()
Returns : void
decodeClientState
decodeClientState(stateString: string)
Parameters :
Name Type Optional
stateString string No
Returns : Record<string, >
getAccessToken
getAccessToken(oauthConfigName?: string)
Parameters :
Name Type Optional
oauthConfigName string Yes
Returns : string | null
getFirstAvailableAccessTokenForUrl
getFirstAvailableAccessTokenForUrl(url: string)
Parameters :
Name Type Optional
url string No
Returns : string | undefined

the access token from the first configuration that has one set

getFirstConfigWithAccessTokenResponsibleForUrl
getFirstConfigWithAccessTokenResponsibleForUrl(url: string)
Parameters :
Name Type Optional
url string No
getIdentityToken
getIdentityToken(oauthConfigName?: string)
Parameters :
Name Type Optional Description
oauthConfigName string Yes
  • if set, will use the given configuration. If left undefined, will use the default configuration.
Returns : string
Async logout
logout(oauthConfigName?: string)

Logs the user out of the session with one IdP configuration.

Parameters :
Name Type Optional Description
oauthConfigName string Yes
  • if set, user will be logged out of this specific IdP's config. If not set, default IdP config will be used.
Returns : Promise<void>
refreshTokenIfRequired
refreshTokenIfRequired(oauthConfigName?: string)
Parameters :
Name Type Optional Description
oauthConfigName string Yes
  • if set, will use the given configuration. If left undefined, will use the default configuration.
Returns : Promise<undefined>

a Promise that resolves whenever the refresh token request finished.

Async retrieveToken
retrieveToken(oauthConfigName?: string | undefined, refresh)
Parameters :
Name Type Optional Default value Description
oauthConfigName string | undefined Yes
  • if set, will use the given configuration. If left undefined, will use the default configuration.
refresh No false
  • if set to true, the refresh flow will be performed
import { TalyRoutingUtilsService } from '@allianz/taly-core';
import {
  HashLocationStrategy,
  Location as LocationService,
  LocationStrategy,
  PlatformLocation
} from '@angular/common';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { Event, EventType, Router, RoutesRecognized } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { filter, mapTo, take } from 'rxjs/operators';
import {
  LOCATION,
  OAUTH_CONFIGURATIONS,
  OauthConfig,
  OauthConfigWithAppTokenPageIdOrInternalPath,
  OauthConfigWithAppTokenUrl,
  STORAGE
} from './symbols';

function extractAngularPathFromParentAppIfAny(angularPath: string, possibleJourneyPaths: string[]) {
  const matchingIndex = possibleJourneyPaths.findIndex((element) => angularPath.endsWith(element));
  if (matchingIndex !== -1) {
    const matchingElement = possibleJourneyPaths[matchingIndex];
    return angularPath.slice(0, angularPath.length - matchingElement.length);
  }
  return angularPath;
}

interface OauthTokenResponse {
  access_token: string;
  id_token: string;
  refresh_token: string;
  expires_in: number;
}

@Injectable()
export class OauthService {
  private static RESPONSE_TYPE = 'code';
  private static ACCESS_TOKEN_GRANT_TYPE = 'authorization_code';
  private static REFRESH_TOKEN_GRANT_TYPE = 'refresh_token';

  private static CLIENT_STATE_MARKER = 'CLIENT_STATE';

  private static STORAGE_KEY_VERIFIER = 'storedCodeVerifier';
  private static STORAGE_KEY_ACCESS_TOKEN = 'accessToken';
  private static STORAGE_KEY_ID_TOKEN = 'idToken';
  private static STORAGE_KEY_REFRESH_TOKEN = 'refreshToken';
  private static STORAGE_KEY_EXPIRES_AT = 'expiresAt';
  private static STORAGE_KEY_CURRENT_CONFIG = 'currentOauthConfig';

  /**
   * Default value that will be used when {@link TalyOauthConfig.refreshBeforeExpiryThreshold} is undefined.
   */
  private static DEFAULT_REFRESH_BEFORE_EXPIRY_THRESHOLD = 300000;

  // The angular compiler is not able to properly resolve these "ambient" types (Browser API)
  // during a production build if they are injected.
  // We use this workaround to be able to inject them without a type whilst keeping type safety
  // see this issue for details: https://github.com/angular/angular/issues/20351
  private _storage: Storage;
  private _location: Location;
  private oauthConfigurations!: OauthConfigWithAppTokenUrl[];

  private performingTokenRefresh$ = new BehaviorSubject<boolean>(false);

  constructor(
    @Inject(OAUTH_CONFIGURATIONS) private initialOauthConfigurations: OauthConfig[],
    @Inject(STORAGE) storage: Storage,
    @Inject(LOCATION) location: Location,
    private locationService: LocationService,
    private http: HttpClient,
    private router: Router,
    private locationStrategy: LocationStrategy,
    private platformLocation: PlatformLocation,
    private talyRoutingUtilsService: TalyRoutingUtilsService
  ) {
    this._storage = storage;
    this._location = location;
    this.assertConfigurationValidity();
    this.setOauthConfigurations();
  }

  private setOauthConfigurations() {
    const hasInternalPathConfig = this.initialOauthConfigurations.some(
      (config) => 'appTokenPageIdOrInternalPath' in config
    );
    if (!hasInternalPathConfig) {
      this.oauthConfigurations = this.initialOauthConfigurations as OauthConfigWithAppTokenUrl[];
      return;
    }

    this.router.events
      .pipe(
        filter((event: Event) => event.type === EventType.RoutesRecognized),
        take(1)
      )
      .subscribe((event: Event) => {
        this.oauthConfigurations = this.initialOauthConfigurations.map((config) => {
          if ('appTokenPageIdOrInternalPath' in config) {
            const { appTokenPageIdOrInternalPath, ...rest } =
              config as OauthConfigWithAppTokenPageIdOrInternalPath;

            const configCopy: OauthConfigWithAppTokenUrl = {
              ...rest,
              appTokenUrl: this.getAppTokenUrl(
                appTokenPageIdOrInternalPath,
                (event as RoutesRecognized).url
              )
            };

            return configCopy;
          }

          return config;
        });
      });
  }

  // "routerUrl" is needed to extract the Angular routing path from a parent app in the module integration
  private getAppTokenUrl(appTokenPageIdOrInternalPath: string, routerUrl: string) {
    function removeSlashesIfAny(inputString: string) {
      return inputString.replace(/^\/+|\/+$/g, '');
    }
    appTokenPageIdOrInternalPath = removeSlashesIfAny(appTokenPageIdOrInternalPath);

    const routerUrlWithoutParams = routerUrl.split('?')[0];

    const possibleJourneyPaths = this.talyRoutingUtilsService.getPossibleJourneyPaths();

    const angularPathFromParent = extractAngularPathFromParentAppIfAny(
      routerUrlWithoutParams,
      possibleJourneyPaths
    );

    const appTokenRoutePath = LocationService.joinWithSlash(
      angularPathFromParent,
      appTokenPageIdOrInternalPath
    );

    let preparedExternalUrl = this.locationService.prepareExternalUrl(appTokenRoutePath);

    // The `prepareExternalUrl` from PFE's `HashOnlyLocationStrategy` (used when the `useHashLocationStrategy` flag is set)
    // doesn't add the base href to the URL. Therefore we do it manually
    if (
      this.locationStrategy instanceof HashLocationStrategy &&
      !preparedExternalUrl.startsWith(this._location.pathname)
    ) {
      preparedExternalUrl = LocationService.joinWithSlash(
        this._location.pathname,
        preparedExternalUrl
      );
    }

    return `${this._location.origin}${preparedExternalUrl}`;
  }

  private assertConfigurationValidity() {
    if (
      new Set(this.initialOauthConfigurations.map((config) => config.name)).size !==
      this.initialOauthConfigurations.length
    ) {
      throw new Error(
        'Duplicate OAuth configuration names. Ensure that each configuration name only appears once.'
      );
    }
    if (this.initialOauthConfigurations.filter((config) => config.default).length > 1) {
      throw new Error(
        'Multiple OAuth configurations marked as default. Only one default configuration is allowed.'
      );
    }
  }

  /**
   * @param oauthConfigName - if set, will use the given configuration. If left undefined, will use the default configuration.
   */
  async authorize(oauthConfigName?: string, clientState?: Record<string, unknown>): Promise<void> {
    const oauthConfig = this.getRelevantOauthConfigOrThrowError(oauthConfigName);

    const authorizeURL = new URL(oauthConfig.authorizeUrl);
    const state = this.generateEncodedState(clientState);
    const codeVerifier = this.generateRandomString(128);
    const codeChallenge = await this.generateCodeChallenge(codeVerifier);

    authorizeURL.searchParams.set('client_id', oauthConfig.clientId);
    authorizeURL.searchParams.set('scope', oauthConfig.scope);
    authorizeURL.searchParams.set('response_type', OauthService.RESPONSE_TYPE);
    authorizeURL.searchParams.set('state', state);
    authorizeURL.searchParams.set('redirect_uri', oauthConfig.appTokenUrl);
    authorizeURL.searchParams.set('code_challenge', codeChallenge);
    authorizeURL.searchParams.set('code_challenge_method', 'S256');

    if (oauthConfig.acrValues) {
      authorizeURL.searchParams.set('acr_values', oauthConfig.acrValues.join(' '));
    }

    this.setCodeVerifier(oauthConfig.name, codeVerifier);
    this.setConfigurationNameForWhichLoginIsInProgress(oauthConfig.name);
    this._location.replace(authorizeURL.toString());
  }

  /**
   * @param oauthConfigName - if set, will use the given configuration. If left undefined, will use the default configuration.
   * @returns a Promise that resolves whenever the refresh token request finished.
   */
  refreshTokenIfRequired(oauthConfigName?: string): Promise<undefined> {
    if (this.performingTokenRefresh$.value === true) {
      return this.performingTokenRefresh$
        .pipe(
          filter((value) => value === false),
          take(1),
          mapTo(undefined)
        )
        .toPromise();
    }

    const oauthConfig = this.getRelevantOauthConfigOrThrowError(oauthConfigName);
    const expiryThreshold =
      oauthConfig.refreshBeforeExpiryThreshold ??
      OauthService.DEFAULT_REFRESH_BEFORE_EXPIRY_THRESHOLD;

    const needsTokenRenewal = this.getExpiresAt(oauthConfig.name) - expiryThreshold <= Date.now();
    if (!needsTokenRenewal) {
      return Promise.resolve(undefined);
    }

    this.performingTokenRefresh$.next(true);
    return this.retrieveToken(oauthConfig.name, true)
      .then(() => undefined)
      .finally(() => this.performingTokenRefresh$.next(false));
  }

  /**
   * @param oauthConfigName - if set, will use the given configuration. If left undefined, will use the default configuration.
   * @param refresh - if set to true, the refresh flow will be performed
   */
  async retrieveToken(
    oauthConfigName?: string | undefined,
    refresh = false
  ): Promise<OauthTokenResponse> {
    const oauthConfig = this.getRelevantOauthConfigOrThrowError(
      oauthConfigName ?? this.getConfigurationNameForWhichLoginIsInProgress() ?? undefined
    );
    const requestBody = refresh
      ? this.getRequestBodyForRefreshToken(oauthConfig)
      : this.getRequestBodyForInitialAccessToken(oauthConfig);

    const tokenResponse: OauthTokenResponse | undefined = await this.http
      .post<OauthTokenResponse>(oauthConfig.tokenUrl, requestBody.toString(), {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
      })
      .toPromise();

    if (!tokenResponse) {
      throw Error(`Can't retrieve Oauth token from ${oauthConfig.tokenUrl}`);
    }

    this.setAccessToken(oauthConfig.name, tokenResponse.access_token);
    this.setIdentityToken(oauthConfig.name, tokenResponse.id_token);

    if (tokenResponse.refresh_token) {
      this.setRefreshToken(oauthConfig.name, tokenResponse.refresh_token);
    }

    this.setExpiresAt(oauthConfig.name, this.getExpiryTime(tokenResponse.expires_in));
    this.clearConfigurationNameForWhichLoginIsInProgress();
    return tokenResponse;
  }

  decodeClientState(stateString: string): Record<string, unknown> {
    const decodedState = atob(decodeURIComponent(stateString));
    let userState: Record<string, unknown> = {};
    if (decodedState.startsWith(OauthService.CLIENT_STATE_MARKER)) {
      userState = JSON.parse(decodedState.substr(OauthService.CLIENT_STATE_MARKER.length));
    }
    return userState;
  }

  private generateEncodedState(clientState?: Record<string, unknown>): string {
    let state: string;
    if (clientState) {
      state = OauthService.CLIENT_STATE_MARKER + JSON.stringify(clientState);
    } else {
      state = this.generateRandomString(10);
    }
    return encodeURIComponent(btoa(state));
  }

  private getRequestBodyForInitialAccessToken(oauthConfig: OauthConfigWithAppTokenUrl): HttpParams {
    const code = this.getQueryParameter('code');
    if (!code) {
      throw new Error(
        'Error while creating the request body to fetch an access token. The query parameter "code" is missing from the URL.'
      );
    }
    return new HttpParams()
      .set('grant_type', OauthService.ACCESS_TOKEN_GRANT_TYPE)
      .set('code', code)
      .set('redirect_uri', oauthConfig.appTokenUrl)
      .set('client_id', oauthConfig.clientId)
      .set('code_verifier', this.getCodeVerifier(oauthConfig.name));
  }

  private getRequestBodyForRefreshToken(oauthConfig: OauthConfigWithAppTokenUrl): HttpParams {
    return new HttpParams()
      .set('grant_type', OauthService.REFRESH_TOKEN_GRANT_TYPE)
      .set('redirect_uri', oauthConfig.appTokenUrl)
      .set('client_id', oauthConfig.clientId)
      .set('refresh_token', this.getRefreshToken(oauthConfig.name));
  }

  private getExpiryTime(expiresIn: number): number {
    return Date.now() + expiresIn * 1000; // expiresIn comes in seconds, Date.now in milliseconds
  }

  /**
   * @param oauthConfigName - if set, will use the given configuration. If left undefined, will use the default configuration.
   */
  getIdentityToken(oauthConfigName?: string): string {
    const oauthConfig = this.getRelevantOauthConfigOrThrowError(oauthConfigName);
    const idToken = this._storage.getItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_ID_TOKEN,
        oauthConfig.name
      )
    );
    if (!idToken) {
      throw new Error(`ID token is missing for configuration ${oauthConfig.name}!`);
    }
    return idToken;
  }

  private setIdentityToken(oauthConfigName: string, identityToken: string) {
    this._storage.setItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_ID_TOKEN,
        oauthConfigName
      ),
      identityToken
    );
  }

  /**
   * @returns the access token from the first configuration that has one set
   */
  getFirstAvailableAccessTokenForUrl(url: string): string | undefined {
    const config = this.getFirstConfigWithAccessTokenResponsibleForUrl(url);
    return this.getAccessToken(config?.name) ?? undefined;
  }

  getFirstConfigWithAccessTokenResponsibleForUrl(
    url: string
  ): OauthConfigWithAppTokenUrl | undefined {
    for (const oauthConfig of this.oauthConfigurations) {
      if (!url.startsWith(oauthConfig.bffBaseUrl)) {
        continue;
      }

      const tokenForConfig = this._storage.getItem(
        this.getPrefixedStorageKeyForOauthConfigName(
          OauthService.STORAGE_KEY_ACCESS_TOKEN,
          oauthConfig.name
        )
      );
      if (tokenForConfig) {
        return oauthConfig;
      }
    }
    return undefined;
  }

  getAccessToken(oauthConfigName?: string): string | null {
    const oauthConfig = this.getRelevantOauthConfigOrThrowError(oauthConfigName);
    const accessToken = this._storage.getItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_ACCESS_TOKEN,
        oauthConfig.name
      )
    );
    return accessToken;
  }

  private setAccessToken(oauthConfigName: string, accessToken: string) {
    this._storage.setItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_ACCESS_TOKEN,
        oauthConfigName
      ),
      accessToken
    );
  }

  private getRefreshToken(oauthConfigName?: string): string {
    const oauthConfig = this.getRelevantOauthConfigOrThrowError(oauthConfigName);
    const refreshToken = this._storage.getItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_REFRESH_TOKEN,
        oauthConfig.name
      )
    );
    if (!refreshToken) {
      throw new Error(`Refresh token is missing for configuration ${oauthConfig.name}!`);
    }
    return refreshToken;
  }

  private setRefreshToken(oauthConfigName: string, refreshToken: string) {
    this._storage.setItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_REFRESH_TOKEN,
        oauthConfigName
      ),
      refreshToken
    );
  }

  private getCodeVerifier(oauthConfigName: string): string {
    const codeVerifier = this._storage.getItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_VERIFIER,
        oauthConfigName
      )
    );
    if (!codeVerifier) {
      throw new Error(`Code verifier is missing for configuration ${oauthConfigName}!`);
    }
    return codeVerifier;
  }

  private setCodeVerifier(oauthConfigName: string, verifier: string) {
    this._storage.setItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_VERIFIER,
        oauthConfigName
      ),
      verifier
    );
  }

  private getConfigurationNameForWhichLoginIsInProgress(): string | null {
    return this._storage.getItem(OauthService.STORAGE_KEY_CURRENT_CONFIG);
  }

  private setConfigurationNameForWhichLoginIsInProgress(configName: string): void {
    this._storage.setItem(OauthService.STORAGE_KEY_CURRENT_CONFIG, configName);
  }

  private clearConfigurationNameForWhichLoginIsInProgress(): void {
    this._storage.removeItem(OauthService.STORAGE_KEY_CURRENT_CONFIG);
  }

  private getExpiresAt(oauthConfigName: string): number {
    const storageEntry = this._storage.getItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_EXPIRES_AT,
        oauthConfigName
      )
    );
    if (!storageEntry) {
      return 0;
    }
    return Number(storageEntry);
  }

  private setExpiresAt(oauthConfigName: string, expiresAt: number) {
    this._storage.setItem(
      this.getPrefixedStorageKeyForOauthConfigName(
        OauthService.STORAGE_KEY_EXPIRES_AT,
        oauthConfigName
      ),
      expiresAt.toString()
    );
  }

  private getPrefixedStorageKeyForOauthConfigName(storageKey: string, oauthConfigName: string) {
    return `${oauthConfigName}.${storageKey}`;
  }

  /**
   * Logs the user out of the session with one IdP configuration.
   * @param oauthConfigName - if set, user will be logged out of this specific IdP's config. If not set, default IdP config will be used.
   */
  async logout(oauthConfigName?: string): Promise<void> {
    const oauthConfig = this.getRelevantOauthConfigOrThrowError(oauthConfigName);
    try {
      await this.endSession(oauthConfig);
    } catch (err) {
      console.error(`End session endpoint resulted in an error: ${err}`);
    }

    try {
      await this.revokeToken(oauthConfig, 'access_token');
    } catch (err) {
      console.error(`Revoking the access token resulted in an error: ${err}`);
    }

    try {
      await this.revokeToken(oauthConfig, 'refresh_token');
    } catch (err) {
      console.error(`Revoking refresh token resulted in an error: ${err}`);
    }

    for (const key of [
      OauthService.STORAGE_KEY_VERIFIER,
      OauthService.STORAGE_KEY_ACCESS_TOKEN,
      OauthService.STORAGE_KEY_ID_TOKEN,
      OauthService.STORAGE_KEY_REFRESH_TOKEN,
      OauthService.STORAGE_KEY_EXPIRES_AT
    ]) {
      this._storage.removeItem(this.getPrefixedStorageKeyForOauthConfigName(key, oauthConfig.name));
    }
  }

  private async endSession(oauthConfig: OauthConfigWithAppTokenUrl): Promise<void> {
    if (!oauthConfig.endSessionUrl) {
      return;
    }

    await this.http
      .get(
        `${oauthConfig.endSessionUrl}?client_id=${
          oauthConfig.clientId
        }&id_token_hint=${this.getIdentityToken(oauthConfig.name)}`
      )
      .toPromise();
  }

  private async revokeToken(
    oauthConfig: OauthConfigWithAppTokenUrl,
    tokenType: 'access_token' | 'refresh_token'
  ): Promise<void> {
    let token: string | null = null;
    try {
      token =
        tokenType === 'access_token'
          ? this.getAccessToken(oauthConfig.name)
          : this.getRefreshToken(oauthConfig.name);
    } catch (error) {
      // no token was set, so nothing to revoke
      return;
    }

    if (!oauthConfig.revokeUrl || !token) {
      return;
    }

    const requestBody = new HttpParams()
      .set('token_type_hint', tokenType)
      .set('token', token)
      .set('client_id', oauthConfig.clientId);

    await this.http
      .post(oauthConfig.revokeUrl, requestBody.toString(), {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
      })
      .toPromise();
  }

  private getRelevantOauthConfigOrThrowError(configName?: string): OauthConfigWithAppTokenUrl {
    let relevantConfig: OauthConfigWithAppTokenUrl | undefined;
    if (configName) {
      relevantConfig = this.oauthConfigurations.find((config) => config.name === configName);
      if (!relevantConfig) {
        throw new Error(`No OAuth configuration found with name "${configName}"!`);
      }
      return relevantConfig;
    }

    if (this.oauthConfigurations.length === 1) {
      return this.oauthConfigurations[0];
    }

    relevantConfig = this.oauthConfigurations.find((config) => config.default);
    if (!relevantConfig) {
      throw new Error(
        'No default OAuth configuration found. Please either call the method with an explicit configuration name or define one OAuth configuration as default = true.'
      );
    }
    return relevantConfig;
  }

  private generateRandomString(length: number): string {
    let text = '';
    const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

    for (let i = 0; i < length; i++) {
      text += possible.charAt(Math.floor(Math.random() * possible.length));
    }

    return text;
  }

  private async generateCodeChallenge(codeVerifier: string): Promise<string> {
    const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier));

    return btoa(String.fromCharCode(...new Uint8Array(digest)))
      .replace(/=/g, '')
      .replace(/\+/g, '-')
      .replace(/\//g, '_');
  }

  /*
   * We're first checking if there is an ongoing navigation that has query parameters.
   * If there is no current navigation or if it is missing the query parameter
   * we look at the current location's search parameters.
   */
  private getQueryParameter(key: string): string | null {
    let queryParameter: string | null = null;
    const currentNavigation = this.router.getCurrentNavigation();
    if (currentNavigation?.extractedUrl.queryParamMap) {
      queryParameter = currentNavigation.extractedUrl.queryParamMap.get(key);
    }
    if (queryParameter === null) {
      queryParameter = new URLSearchParams(this._location.search).get(key);
    }
    return queryParameter;
  }

  clearQueryParams() {
    if (this.locationStrategy instanceof HashLocationStrategy) {
      // The query params `code` and `state` are added in front of the hash
      // and the PFE's HashOnlyLocationStrategy can't remove them automatically
      // so we need to remove them manually
      const queryParams = new URLSearchParams(this._location.search);
      queryParams.delete('code');
      queryParams.delete('state');

      let newPath = '';
      if (new Set(queryParams.keys()).size === 0) {
        newPath = this._location.pathname + this._location.hash;
      } else {
        newPath = this._location.pathname + '?' + queryParams.toString() + this._location.hash;
      }

      // ATTENTION: HACK AHEAD!
      // this next line will replace the current URL of the browser with the one
      // without the query params but the LocationStrategy internally still has the
      // old URL with the query params so we call the `getBaseHref()` method to update
      // the internal state of the HashOnlyLocationStrategy in the next next line.
      // This works just because of the specific way the HashOnlyLocationStrategy is built.
      this.platformLocation.replaceState(null, '', newPath);
      this.locationStrategy.getBaseHref();
    } else {
      // Why the setTimeout? We need to wait for the current navigation to finish
      // before we can remove the query params
      setTimeout(() => {
        // stackoverflow reference for this solution: https://stackoverflow.com/a/52193044
        this.router.navigate([], {
          queryParamsHandling: 'merge',
          queryParams: { code: null, state: null }
        });
      }, 0);
    }
  }
}

results matching ""

    No results matching ""