Angular2-jwt: How to extend AuthHttp to react on 401 error?

Created on 29 Sep 2016  Â·  16Comments  Â·  Source: auth0/angular2-jwt

Hi,

i want to react on 401 Errors on Login, i've already read https://github.com/auth0/angular2-jwt/issues/37, where a solution to extend the class AuthHttp is described.
However back then Angular 2 still had the provide function.

I can understand that you want to keep angualr2-jwt thin, however this is something nearly everybody who uses jwt has to deal with.

Is there a clean way to do this, which will be most likely compatible with future updates?

question

Most helpful comment

this is my solution, i hope you like it.

file: authHttpInterceptor.ts

import {RequestOptions, Response, Request, RequestOptionsArgs, Headers, Http} from "@angular/http";
import {Observable} from "rxjs";
import {AuthHttp, AuthConfig} from "angular2-jwt";
import {AuthService} from "../services/auth.service";


export class AuthHttpInterceptor extends AuthHttp {

  constructor(http: Http, defaultOptions: RequestOptions, private authService: AuthService) {
    super(new AuthConfig({
      tokenName: 'token',
      tokenGetter: (() => localStorage.getItem('token')),
      globalHeaders: [{'Content-Type': 'application/json'}],
    }), http, defaultOptions)
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.request(url, options));
  }

  get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.get(url, options));
  }

  post(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.post(url, body, this.getRequestOptionArgs(options)));
  }

  put(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.put(url, body, this.getRequestOptionArgs(options)));
  }

  delete(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.delete(url, options));
  }

  private getRequestOptionArgs(options?: RequestOptionsArgs): RequestOptionsArgs {
    if (options == null) {
      options = new RequestOptions();
    }
    if (options.headers == null) {
      options.headers = new Headers();
    }
    options.headers.append('Content-Type', 'application/json');
    return options;
  }

  private isUnauthorized(status: number): boolean {
    return status === 0 || status === 401 || status === 403;
  }

  intercept(observable: Observable<Response>): Observable<Response> {

    return observable.catch((err, source) => {
      if (this.isUnauthorized(err.status)) {
        //logout the user or do what you want
        //this.authService.logout();

        if (err instanceof Response) {
          return Observable.throw(err.json().message || 'backend server error');
        }
        return Observable.empty();
      } else {
        return Observable.throw(err);
      }
    });

  }
}

file: auth.module.ts

import {NgModule} from "@angular/core";
import {Http, RequestOptions} from "@angular/http";
import {AuthHttp} from "angular2-jwt";
import {AuthHttpInterceptor} from "./authHttpInterceptor";
import {AuthService} from "../services/auth.service";

export function authHttpServiceFactory(http: Http, options: RequestOptions, authService: AuthService) {
  return new AuthHttpInterceptor(http, options, authService);
}

@NgModule({
  providers: [
    {
      provide: AuthHttp,
      useFactory: authHttpServiceFactory,
      deps: [Http, RequestOptions, AuthService]
    }
  ]
})
export class AuthModule {
}

All 16 comments

While provide has been replaced with an object literal, the solution is still the same. The reason why such functionality is not provided is everyone wants something different. For some, 401s and 403s mean fetching a refresh token, and for others, a redirect to a login is appropriate. In both cases, a very simple wrapper service will give you the power you need, and the flexibility to add more later, when you go to add default headers, global logging, etc.... For it to be added to this lib, we need to account for all scenarios and handle them gracefully, which usually amounts to substantially more code to do the same thing as a 20 line wrapper.

If and when http becomes more composable, I could see adding additional http decorators to handle scenarios like yours. In the meantime, if you want to wrap authhttp as a separate class and contribute that, I'm sure it would be appreciated.

@escardin thanks for you answer.
I wrote a wrapper which works great for me and might be useful for others as well,
suggestions concerning my solution are most welcome.

import { Injectable } from '@angular/core';
import { Http, Request, Response, RequestOptionsArgs, RequestOptions } from '@angular/http';
import { Router } from '@angular/router';
import { AuthHttp as JwtAuthHttp, AuthConfig } from 'angular2-jwt';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class CustomAuthHttp {

  constructor(private authHttp: JwtAuthHttp, private router: Router) {
  }

  private isUnauthorized(status: number): boolean {
    return status === 0 || status === 401 || status === 403;
  }

    private authIntercept(response: Observable<Response>): Observable<Response> {
    var sharableResponse = response.share()
    sharableResponse.subscribe(null, (err) => {
      if (this.isUnauthorized(err.status)) {
        this.router.navigate(['/login']);
      }
      // Other error handling may be added here, such as refresh token …
    });
    return sharableResponse;
  }
  public setGlobalHeaders(headers: Array<Object>, request: Request | RequestOptionsArgs) {
    this.authHttp.setGlobalHeaders(headers, request);
  }

  public request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return this.authIntercept(this.authHttp.request(url, options));
  }

  public get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.authIntercept(this.authHttp.get(url, options));
  }

  public post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
    return this.authIntercept(this.authHttp.post(url, options));
  }

  public put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
    return this.authIntercept(this.authHttp.put(url, options));
  }

  public delete(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.authIntercept(this.authHttp.delete(url, options));
  }

  public patch(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
    return this.authIntercept(this.authHttp.patch(url, options));
  }

  public head(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.authIntercept(this.authHttp.head(url, options));
  }

  public options(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.authIntercept(this.authHttp.options(url, options));
  }
}

Update in the authIntercept since normal responses should only be subscribed once otherwise the request is sent twice

@kelvynmarte
TypeError: response.share is not a function(…)

@zenghuiLee Make sure you import the share operator
import 'rxjs/add/operator/share';

@zenghuiLee what version of rxjs are you using?
im using: "rxjs": "5.0.0-beta.12"

The rxjs/Observable interface includes the share function in the version i am using.

Share has been around since the earliest alphas I used. It's there, it just needs to be added.

looking at this code, you don't need share, just use .do() (which also needs to be added)
return response.do(null,err->{})

@escardin so you are suggesting:

…
  private authIntercept(response: Observable<Response>): Observable<Response> {
    response.do(null, (err) => {
      if (this.isUnauthorized(err.status)) {
        this.router.navigate(['/login']);
      }
      // Other error handling may be added here, such as refresh token …
    });
    return response;
  }
…

I tested it quickly, it didn't work for, i got an exception, but i didn't go into detail since the solution above works …

vaguely yes.

 private authIntercept(response: Observable<Response>): Observable<Response> {
    return response
       .do(null, (err) => refreshOnUnauthorized(err))//probably should be a .retryWhen()
       .do(null, (err) => routeOnForbidden(err))
       .do(res => logOnSuccess(res), err => logOnError(err));
  }

.do() is the operator to use when you want to have a side effect as part of your chain. You shouldn't need to share the request and subscribe to it, as it makes your request hot. You also need to return the result of .do() since it won't get called unless it's subscribed to.

this is my solution, i hope you like it.

file: authHttpInterceptor.ts

import {RequestOptions, Response, Request, RequestOptionsArgs, Headers, Http} from "@angular/http";
import {Observable} from "rxjs";
import {AuthHttp, AuthConfig} from "angular2-jwt";
import {AuthService} from "../services/auth.service";


export class AuthHttpInterceptor extends AuthHttp {

  constructor(http: Http, defaultOptions: RequestOptions, private authService: AuthService) {
    super(new AuthConfig({
      tokenName: 'token',
      tokenGetter: (() => localStorage.getItem('token')),
      globalHeaders: [{'Content-Type': 'application/json'}],
    }), http, defaultOptions)
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.request(url, options));
  }

  get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.get(url, options));
  }

  post(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.post(url, body, this.getRequestOptionArgs(options)));
  }

  put(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.put(url, body, this.getRequestOptionArgs(options)));
  }

  delete(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return this.intercept(super.delete(url, options));
  }

  private getRequestOptionArgs(options?: RequestOptionsArgs): RequestOptionsArgs {
    if (options == null) {
      options = new RequestOptions();
    }
    if (options.headers == null) {
      options.headers = new Headers();
    }
    options.headers.append('Content-Type', 'application/json');
    return options;
  }

  private isUnauthorized(status: number): boolean {
    return status === 0 || status === 401 || status === 403;
  }

  intercept(observable: Observable<Response>): Observable<Response> {

    return observable.catch((err, source) => {
      if (this.isUnauthorized(err.status)) {
        //logout the user or do what you want
        //this.authService.logout();

        if (err instanceof Response) {
          return Observable.throw(err.json().message || 'backend server error');
        }
        return Observable.empty();
      } else {
        return Observable.throw(err);
      }
    });

  }
}

file: auth.module.ts

import {NgModule} from "@angular/core";
import {Http, RequestOptions} from "@angular/http";
import {AuthHttp} from "angular2-jwt";
import {AuthHttpInterceptor} from "./authHttpInterceptor";
import {AuthService} from "../services/auth.service";

export function authHttpServiceFactory(http: Http, options: RequestOptions, authService: AuthService) {
  return new AuthHttpInterceptor(http, options, authService);
}

@NgModule({
  providers: [
    {
      provide: AuthHttp,
      useFactory: authHttpServiceFactory,
      deps: [Http, RequestOptions, AuthService]
    }
  ]
})
export class AuthModule {
}

@kelvynmarte I've done exactly what u suggested. How did you define you app module for Http provider ?
I am getting an error 'Cannot instantiate cyclic dependency! Http ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1'

My AppModule looks like this:

providers: [{
provide: Http,
useFactory: HttpFactory,
deps: [XHRBackend, RequestOptions, AuthService]
}]

@alexnoise79 observable.catch((err, source) - no error status here, just text. So it's not possible to understand if it's expiration error or something else. Any idea? I'm using angular2-jwt version 0.2.3

@Viktor-Bredihin what you're catching depends. If you're catching a system error (like you would get with a cors problem) it will be kind of tough to deal with. If it's a response from your server (i.e. a 401) then it's a response same as if you got a 200. you can then do response.text() or response.json() as required.

@escardin I want to catch 'No JWT present or has expired' error, but it's just an instance of Error without any status

Be careful when copying the code snipped from @kelvynmarte above: The post, put and patch method fail to properly forward the body argument to the underlying AuthHttp, so they won't work properly. Maybe you want to edit your post.

@Viktor-Bredihin If you're writing a wrapper, imo it is better to check the token before issuing the request. Then you don't need to catch the error, and you can queue the request until you have a valid token.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Eddman picture Eddman  Â·  3Comments

sfabriece picture sfabriece  Â·  4Comments

mahendra2125 picture mahendra2125  Â·  4Comments

leosvelperez picture leosvelperez  Â·  5Comments

JaxonWright picture JaxonWright  Â·  4Comments