HTTP Client with Authorization Headers from LocalStorage


#1

I have an http “interceptor” class to add my authorization headers to each call I make in my Ionic 2 application. I had it working fine with hard-coded authorization values for testing, but now I need to add the piece that gets the credentials from the local storage and I’m having issues figuring out how to return the right promise back to my service. I am getting the error “Cannot read property ‘map’ of undefined in [null]”.

I did not use Ionic v1 and don’t have a ton of experience with promises so I feel like there is something very simple I am missing. Both Ionic 2 and Angular 2 are pretty new so I haven’t been able to find the right solution online yet.

This is my code from my HttpClient interceptor class:

getAuth(){

     return this.local.get("UserName")
     .then(res =>
     {
         this.user = res;

         return this.local.get("Token").then( res => this.session=res );
     });
  }
 get(url) {
    
    let headers = new Headers({
      'Accept': 'application/json',
      'Content-Type': 'application/json; charset=utf-8'
    });
    let params = new URLSearchParams('format=json');

      this.getAuth().then(() => {

          var authHeader = 'Basic ' + this.encode(this.user + ':{SessionToken}' + this.session);
          headers.append('Authorization',authHeader); 

          return this.http.get(url, {
            headers: headers,
            search: params
          });        
      });
 }

The headers are correct and the call to .get works fine by itself. It’s only when I use it in my service and try to .map() that I get the error. This is how I call it from my service:

getTodos(id){
		var url: string = this.global.APIUrl + "/" + id;

		return this.httpClient.get(url)
		.map((res)=> res.json());
	}

If I console.log(this.httpClient.get(url)) at this point, it returns a promise, but the error indicates that it is returning “undefined”. Perhaps the HttpClient class isn’t compiling correctly but I’m not getting any errors from the class to know what is wrong.

Also, I have read several articles about using a .all() function to help clean up nested promises, but I haven’t found an example of how to do it in Ionic 2/Angular 2 specifically. I didn’t want to mess with it at the same time as this problem, but ideally, I would like to use something to stop the chaining if it exists.

And as a sidenote, I do have import ‘rxjs/add/operator/map’; in my service.


#2

There are a number of ways to approach this depending on things like whether you always want to wait for the authorization stuff, or sometimes want to do a synchronous “isLoggedIn” check. I’m going to assume for the moment for simplicity that (a) you always want to wait and (b) you don’t really need to store user and session separately.

getAuth() goes away. In its place we have authObs:Subject<string> = new ReplaySubject<string>(1);. At some point (you can put it in HttpClient’s constructor for the moment, if you wish), we do this:

Promise.all([
  this.local.get("UserName"),
  this.local.get("Token")
]).then((lsrv) => {
  let authHeader = 'Basic ' + this.encode(lsrv[0] + ':{SessionToken}' + lsrv[1]);
  this.authObs.next(authHeader);
});

Having authObs be a ReplaySubject with a single stack slot allows you to do deauthorization and reauthorization extremely simply, by just calling next on it.

Next, I would refactor your header modification into a function of its own, because this makes adding all the other Http methods like post/put/delete/&c one-liners.

private _addAuthToOptions(opts:RequestOptionArgs): Observable<RequestOptionArgs> {
  return this.authObs.map((authHeader) => {
    // conditionally add the Accept and Content-Type and search if you like, make sure opts has headers
    opts.headers.append('Authorization', authHeader);
    return opts;
  });
}

Now get() becomes:

get(url: string, options?: RequestOptionsArgs): Observable<Response> {
  return this._addAuthToOptions(options).flatMap((allopts) => {
    return this._http.get(url, allopts);
  });
}

I’m just typing this in, so apologies for any stupid syntax errors, but hopefully it at least gets you started.


#3

Thanks for your help! I have made these changes but have been struggling to get this to work. It seems like the .flatmap() call isn’t executing or isn’t returning because it will log a console statement right before, but then the GET is never requested to the API. (I don’t see it go across the Network log in Chrome Developer Tools) There are no errors being thrown in compile or browser window so I’m having trouble figuring out what is happening. Is there an import I need to do for flatmap besides the rxjs/operator/add/map?


#4

Yes, you need to import the flatmap operator once somewhere in the same way.


#5

Thank you! I finally found what to import and it is now working!

For others that might come to this thread, you must import ‘rxjs/add/operator/mergemap’ to use flatmap(). Since it wasn’t throwing an error, I had mistakenly thought it was included with ‘rxjs/add/operator/map’ which I had already imported. It would have saved me a lot of time if it had thrown an error!


#6

Thank you for opening this topic. I made a few alterations to the given answer. Here is my solution for the people that do stumble upon this thread. Do not forget to import Headers to make this work.

	var authHeader = this.addAuthHead(new Headers());
	return this.http.get(this.serverUrl + '/choice', {headers: authHeader}).map(res => res.json());

	addAuthHead(head) {
		var headers = head || new Headers();
		let authToken = localStorage.getItem('token');
		headers.append('Authorization', 'Bearer ' + JSON.parse(authToken));
		return headers;
        }

#7

One of the key points of this thread is “dealing with asynchronous responses from storage”. Given that context, you can’t do this.


#8

@HorseFly, I am having similar problem with my own Httpclient.

Would you mind sharing your code for this?
It would be great help to me.


#9

Here is what I have now in RC1:

import {Http, Headers} from '@angular/http';
import {RequestOptionsArgs, RequestMethod} from "@angular/http";
import {Injectable, Inject} from '@angular/core';
import {URLSearchParams} from '@angular/http';
import {Storage} from '@ionic/storage';
import {Subject, ReplaySubject} from 'rxjs/Rx';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class HttpInterceptor2 {
  authObs:Subject<string> = new ReplaySubject<string>(1);

  constructor(@Inject(Http) public http, public sql:Storage) {
    this.http = http;
    this.refreshAuth();
  }

  //Load the Subject
  refreshAuth()
  {    
    return this.sql.get("AuthUser").then((auth)=>{ 
      let a ={UserName:null,Token:null};

      if(auth!==undefined && auth!==null) {
        a=JSON.parse(auth);
      }
     
      let authHeader = 'Basic ' + this.encode(a.UserName + ':{SessionToken}' + a.Token);
      return this.authObs.next(authHeader); 
    });
  }

  //Build Header
  public _addAuthToOptions(opts: RequestOptionsArgs): Observable<RequestOptionsArgs> {
    return this.authObs.map((authHeader) => {
       
      if(!opts.headers.has("Content-Type"))
        opts.headers.append("Content-Type", 'application/json; charset=utf-8');

      if(opts.headers.has("Authorization"))
        opts.headers.delete("Authorization");

      opts.headers.append('Authorization', authHeader);
      return opts;
    });

  } 
    
  get(url: string): Observable<any> {
      let options: RequestOptionsArgs;
      options = {
        search: new URLSearchParams("Format=json"),
        method: RequestMethod.Get
      }
      options.headers = new Headers(); 
    
      return this._addAuthToOptions(options).flatMap((allopts) =>{ 
        return this.http.get(url, allopts);
      });
  }

  post(url:string, data:string): Observable<any> {
    
    let options: RequestOptionsArgs;
    options = {
      search: new URLSearchParams("Format=json"),
      method: RequestMethod.Post
    }
    options.headers = new Headers();

    return this._addAuthToOptions(options).flatMap((allopts) => {       
      return this.http.post(url, data, allopts);
    });
  }
}

I haven’t revisited whether it still needs to be done as such since they retooled the Storage object, but since it is still working, I have left it.


#10

Thanks a lot…i will try this tonight and let you know how i go.


#11

@HorseFly @rapropos

Hi i am tagging Robert as he might be able shed some light on my problem.

I tried the @HorseFly’s code, but for some reason observables stopped executing the “complete” event which it is used to for me before with when using synchronous storage calls (i.e. localStorage.setItem and localStorage.getItem) . To clarify api call is invoked properly with expected response returned, but it doesn’t go to either to error or complete event.

Surprisingly, it doesn’t throw any error but it just keep showing a spinner because i hide my spinner in the complete event.

My approach with the custom http client is little different as i am using multi-providers of angular2, which i personally feel doesn’t make any difference the context of the problem i am trying to solve.

If you can provide any sought help it would great.

Here is my custom Http client

import { Http, Request, RequestOptionsArgs, Response, RequestOptions, ConnectionBackend, Headers } from '@angular/http';

import { Observable } from 'rxjs/Observable';
import { Subject, ReplaySubject } from 'rxjs/Rx';
import { Events } from 'ionic-angular';

import 'rxjs/add/operator/concat'
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/mergemap';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/throw';


import { LocalStorageService, UserStorageInfoModel } from '../storage';


export class CustomHttpClient extends Http {

    authObs: Subject<UserStorageInfoModel> = new ReplaySubject<UserStorageInfoModel>(1);

    constructor(public backend: ConnectionBackend, public defaultOptions: RequestOptions,
        public events: Events, public localStorageService: LocalStorageService) {
        super(backend, defaultOptions);

        this.refreshUserInfo();
    }

    //Load the Subject
    refreshUserInfo() {
        return this.localStorageService.getLoggedInUserInfo()
            .then((userInfo) => {
                let userInfoModel: UserStorageInfoModel = new UserStorageInfoModel();

                if (userInfo !== undefined && userInfo !== null) {
                    userInfoModel = <UserStorageInfoModel>JSON.parse(userInfo);
                    return this.authObs.next(userInfoModel);
                }
            });
    }

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

    get(url: string, options?: RequestOptionsArgs): Observable<Response> {

        return this._addAuthToOptions(options).flatMap((allopts) => {
            return this.intercept(super.get(url, allopts));
        });
    }

    post(url: string, body: string, options?: RequestOptionsArgs): Observable<Response> {

        return this._addAuthToOptions(options).flatMap((allopts) => {
            return this.intercept(super.post(url, JSON.stringify(body), allopts))
            // return super.post(url, JSON.stringify(body), allopts)
        });

    }

    // Build Header
    public _addAuthToOptions(opts: RequestOptionsArgs): Observable<RequestOptionsArgs> {
        return this.authObs.map((userInfo) => {

            if (opts === null || opts === undefined) {
                opts = new RequestOptions();
            }

            if (opts.headers === null || opts.headers === undefined) {
                opts.headers = new Headers();
            }

            if (!opts.headers.has("Content-Type"))
                opts.headers.append("Content-Type", 'application/json; charset=utf-8');

            if (opts.headers.has("Authorization"))
                opts.headers.delete("Authorization");

            opts.headers.append('Authorization', userInfo.authenticationToken);
            return opts;
        });
    }

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

        return observable.catch((err) => {
            if (err.status === 401) {
                this.events.publish('user:loginExpired', 'TokenExpired');
                return Observable.empty<Response>();
            } else {
                throw Error(err);
            }
        });
    }
}

#12

@HorseFly @rapropos

Any help on this please?


#13

I admit to being pretty novice with Observables. How are you calling the get()? These are streams and subscriptions, so the Completed event would only be called after the last Next is done. It wouldn’t happen on every publish (.Subscribe() execution). I believe Completed would execute when you either do the Unsubscribe on the subscription or if you use .first() to only subscribe to the first next event of the stream. That’s my very basic understanding of how Completed work.

So, if you are using a Loading indicator with a stream of data that is going to be hot and publishing from multiple sources, you most likely just want to dismiss the spinner at the bottom of the Subscribe() and Error() separately. If your data is on demand, meaning it only executes when you call it in that module, you can do .first().subscribe() and it will trigger the completed event immediately as you are probably expecting.

I don’t know if that helps, but I know .first() wasn’t immediately obvious to me when I was doing isolated data calls.


#14

Thank you for your response.

With normal angular http it has always invoked the complete event for me,

In my case http call just returns 1 response and I am not sure whether I should have the need to call .First().Subscribe().
I did tried it out your suggestion and it invokes the complete event too, but i want this client to be generic to be able to used for stream of data.

Here is my login component and login service which will do a http post and call above CustomHttpClient.

--------------------------Login Component-------------------
    verifyLogin(): void {

        this.events.publish('spinner:show');

        this.loginService.validateUser(this.email, this.passWord)
            .subscribe((data: LoginResponse) => {
                this.loginResponse = data;
                console.log(this.loginResponse);
            },
            error => {
                this.events.publish('spinner:hide');
                throw error;
            }
            ,
            () => {

                console.log('Login Completed');

                if (this.loginResponse.errorMessage.length === 0) {

                    let userInfoModel = new UserStorageInfoModel();
                    userInfoModel.authenticationToken = this.loginResponse.callerInfo.authToken.authToken;
                    userInfoModel.role = 'admin';
                    userInfoModel.userId = 12123;

                    this.localStorageService.setLoggedInUserInfo(userInfoModel).then(() => {
                        this.nav.setRoot(DevicesTabComponent, { tabIndex: 0 });
                        console.log('Login completed');
                        this.events.publish('spinner:hide');
                    });
                }

            }
            );
---------------------------------------------------------------------


-----------------Login Service------------------
        validateUser(email: string, pwd: string): Observable<LoginResponse> {
            let url = 'http://somewebapi/logincontroller/validateuser';

             var userCredentials = new UserCredentials();
             userCredentials.email = email;
             userCredentials.password = pwd;


            return this.http.post(url, JSON.stringify(userCredentials)).map(this.extractData)
                .catch(this.handleError);

        }

        private extractData(res: Response) {
            let body = res.json();
            return body || {};
        }

        private handleError(error: any) {
            let errMsg = (error.message) ? error.message :
                error.status ? `${error.status} - ${error.statusText}` : 'Server error';
            console.error(errMsg); // log to console instead
            return Observable.throw(errMsg);
        }
   ----------------------------------------------------------------------------------------------------------------------

#15

I think you are fine with using .first() in this scenario. Your Login stream isn’t going to be firing off in the background without you reinstating the subscription each time and it only has one value at any given time. You wouldn’t be using .next() somewhere else to get the next login, essentially.