Skip to content

setupAutomaticSilentRefresh causing infinite token refresh loop #1331

Closed
@landon-buttars-wgu

Description

@landon-buttars-wgu

Describe the bug
this.oauthService.setupAutomaticSilentRefresh(); goes into infinite loop after access token expires.

Using the code+pkce flow without the offline_access scope.
It is unclear that offline_access is required as per this issue and is unavailable to me because of my auth provider (pingfed).

It refreshes, gets a new valid access, id, and refresh token, and then refreshes again.

My auth server is configured for very short access_token life spans. They're configured for 5 minutes.

From my understanding refresh tokens will still work without the offline_access scope as long as the refresh happens before the expiration of the access_token.

Using version "angular-oauth2-oidc": "^12.1.0"

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';

import { OAuthErrorEvent, OAuthService } from 'angular-oauth2-oidc';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false);
  isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();

  private isDoneLoadingSubject$ = new BehaviorSubject<boolean>(false);
  isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();

  /**
   * Publishes `true` if and only if (a) all the asynchronous initial
   * login calls have completed or errorred, and (b) the user ended up
   * being authenticated.
   *
   * In essence, it combines:
   *
   * - the latest known state of whether the user is authorized
   * - whether the ajax calls for initial log in have all been done
   */
  canActivateProtectedRoutes$: Observable<boolean> = combineLatest([
    this.isAuthenticated$,
    this.isDoneLoading$,
  ]).pipe(map((values) => values.every((b) => b)));

  constructor(private oauthService: OAuthService, private router: Router) {
    // Useful for debugging:
    this.oauthService.events.subscribe((event) => {
      if (event instanceof OAuthErrorEvent) {
        console.error('OAuthErrorEvent Object:', event);
      } else {
        console.warn('OAuthEvent Object:', event);
      }
    });

    // This is tricky, as it might cause race conditions (where access_token is set in another
    // tab before everything is said and done there.
    // TODO: Improve this setup. See: https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/issues/2
    window.addEventListener('storage', (event) => {
      // The `key` is `null` if the event was caused by `storage.clear()`
      if (
        (event.key !== 'access_token' && event.key !== null) ||
        event.key === null
      ) {
        return;
      }

      console.warn(
        'Noticed changes to access_token (most likely from another tab), updating isAuthenticated'
      );
      this.isAuthenticatedSubject$.next(
        this.oauthService.hasValidAccessToken()
      );

      if (!this.oauthService.hasValidAccessToken()) {
        this.navigateToLoginPage();
      }
    });

    this.oauthService.events.subscribe(() => {
      this.isAuthenticatedSubject$.next(
        this.oauthService.hasValidAccessToken()
      );
    });
    this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());

    this.oauthService.events
      .pipe(filter((e) => ['token_received'].includes(e.type)))
      .subscribe(() => this.oauthService.loadUserProfile());

    this.oauthService.events
      .pipe(
        filter((e) => ['session_terminated', 'session_error'].includes(e.type))
      )
      .subscribe(() => this.navigateToLoginPage());

    this.oauthService.setupAutomaticSilentRefresh(); // Commenting this out eliminates the loop but does not provide refresh functionality.
  }

  private navigateToLoginPage() {
    this.router.navigateByUrl('/login');
  }

  runInitialLoginSequence(): Promise<void> {
    if (location.hash) {
      console.log('Encountered hash fragment, plotting as table...');
      console.table(
        location.hash
          .substr(1)
          .split('&')
          .map((kvp) => kvp.split('='))
      );
    }

    return this.oauthService
      .loadDiscoveryDocument()
      .then(() => this.oauthService.tryLogin())
      .then(() => {
        this.isDoneLoadingSubject$.next(true);
      })
      .catch(() => this.isDoneLoadingSubject$.next(true));
  }

  login(targetUrl?: string) {
    this.oauthService.initLoginFlow(targetUrl || this.router.url);
  }

  logout() {
    this.oauthService.logOut();
  }

  hasValidToken() {
    return this.oauthService.hasValidAccessToken();
  }

  // These normally won't be exposed from a service like this, but
  // for debugging it makes sense.
  get accessToken() {
    return this.oauthService.getAccessToken();
  }
  get refreshToken() {
    return this.oauthService.getRefreshToken();
  }
  get identityClaims() {
    return this.oauthService.getIdentityClaims();
  }
  get idToken() {
    return this.oauthService.getIdToken();
  }
  get logoutUrl() {
    return this.oauthService.logoutUrl;
  }
}

Expected behavior
Silent Refresh should refresh the token and not cause an infinite loop.

Desktop (please complete the following information):

  • OS: MacOs
  • Browser: Chrome
  • Version: 112.0.5615.137

Additional context
I based this off of one of the examples and stripped all but code + pkce functionality.
"angular-oauth2-oidc": "^12.1.0"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions