Skip to content

Support for refresh and retry upon 401/403 errors in interceptor #414

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jeroenheijmans opened this issue Aug 30, 2018 · 1 comment
Closed

Comments

@jeroenheijmans
Copy link
Collaborator

jeroenheijmans commented Aug 30, 2018

There's good documentation on interceptors, and I'm trying to combine it with this approach on Stack Overflow for retries upon 401/403 errors. However, I think some extension points are missing.

The OAuthResourceServerErrorHandler type isn't "strong" enough for retries, because the handleError signature doesn't include access to the req (request) or next (next interceptor) for implementing the retry.

I can of course write my own HttpInterceptor that mixes the DefaultOAuthInterceptor with the approach from the SO post. But I don't like that approach too much, because then I effectively fork the DefaultOAuthInterceptor, missing out on any bugfixes in future versions.

I'm happy to create a PR for improving this. But then (a) I would need to know that this would be something that'll be potentially accepted, and (b) would like some guidance on the direction of the approach, because I can see these major approaches:

  1. Composition: create a type or interface for objects that get optionally injected into the DefaultOAuthInterceptor, that allow implementing a retry strategy.
  2. Composition: refactor the DefaultOAuthInterceptor and extract parts of the intercept function into seperate types, so that they can be reused.
  3. Inheritance: split up the intercept method a bit so that a subclass can be created that uses the super class but extends its features.
  4. Feature: create a second HttpInterceptor in the library itself that has all the Default interceptor's features plus also a retry+login strategy built in.

I lean towards the first option myself, because it's more in line with how the other extension points in this library work.

Edit, footnote: in #429 I was attended to the fact that this story seems to focus mostly on retries. However, it is in fact more about having to log in upon 401/403 (and I think that if that can be done with a silent refresh, a retry is a good additional bonus - also, whether you want a hard login redirection in addition to an attempt at silent refresh, is an application concern imho).

@adrianbenjuya
Copy link
Contributor

Based on the links @jeroenheijmans shared and on what this package offers I came into a solution for retrying requests that have failed with 401 error. I had to create an additional interceptor for errors but I'm not that happy to have to manually set the authentication token by myself again, since that feature is already present in the DefaultOAuthInterceptor. Here my solution

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { accessTokenUrlValidator } from '@shared/helpers';
import { OAuthService } from 'angular-oauth2-oidc';
import { from, Observable, throwError } from 'rxjs';
import { catchError, filter, finalize, switchMap, take } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class UnauthorizedErrorInterceptor implements HttpInterceptor {
  private refreshTokenInProgress = false;

  constructor(private oauthService: OAuthService, private router: Router) {

  }

  private getAuthRequest(req: HttpRequest<any>): HttpRequest<any> {
    return req.clone({
      setHeaders: {
        'Authorization': 'Bearer ' + this.oauthService.getAccessToken()
      }
    });
  }

  private initImplicitFlow(): void {
    const encodedUri = encodeURIComponent(this.router.url);
    this.oauthService.initImplicitFlow(encodedUri);
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    if (!accessTokenUrlValidator(req.url)) {
      return next.handle(req);
    }

    return next.handle(req).pipe(
      catchError((response: HttpResponse<any>) => {
        if (response && response.status === 401) {
          if (this.refreshTokenInProgress) {
            // If refreshTokenInProgress is true, we will wait until token is silently refreshed
            // which means the new token is ready and we can retry the request
            return this.oauthService.events.pipe(
              filter(result => result.type === 'silently_refreshed'),
              take(1),
              switchMap(() => next.handle(this.getAuthRequest(req)))
            );
          } else {
            this.refreshTokenInProgress = true;

            return from(this.oauthService.silentRefresh()).pipe(
              switchMap(() => next.handle(this.getAuthRequest(req))),
              catchError(error => {
                this.initImplicitFlow();
                return throwError(error);
              }),
              // When the call to silentRefresh completes we reset the refreshTokenInProgress to false
              // for the next time the token needs to be refreshed
              finalize(() => this.refreshTokenInProgress = false)
            );
          }
        }

        return throwError(response);
      })
    );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants