Silent login when token expired?


#1

in the ionic way ? I use the below code but it does not work.

When I entered to homepage the log is not display as I want (In console log, it show : first then last, and the last is second) so It always return token invalid because it does not run in order. I dont know where I wrong.
Please help me. Thank you all !

  logIn(identifi : string, password : string ) {
    let headers = new Headers();
    headers.append('Access-Control-Allow-Origin', '*');
    headers.append('Access-Control-Allow-Methods', '*');
    headers.append('Access-Control-Allow-Headers', 'Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With');    
    let body = {
      "user_id" : identifi,
      "password" : password
    };
    let options = new RequestOptions({ headers: headers });
    return this.http.post(api.logIn, body, options).map((res:Response) => res.json());
  }  

  checkToken() {
    this.storage.get('user').then((user) => {
      if(user) {
        this.token = user.token;
        this.token_date = user.token_date - 100;
        this.currentDate = new Date();
        this.expDate = new Date(this.token_date * 1000);
        if (this.expDate === this.currentDate) {
          // token expired, login again     
          console.log('first');  
          this.logIn(user.user_id, user.password).subscribe(
            data => {
              this.storage.set("user", userInfo);  
              console.log('second');
              return  userInfo.token;
            },
            err => {
              console.log(err);
            });
        }
      }
    })
  }

  getDataHomepage(retailerId: number,pageIndex: number) {
    this.checkToken();
    console.log('last');
    let headers = new Headers();
    headers.append('Authorization', 'Bearer ' + this.token);
    let options = new RequestOptions({ headers: headers });
    return this.http.get(api.promotion + '?retailer_id=' + retailerId + '&page=' + pageIndex, options).map((res:Response) => res.json());
  }


#2

First off, CORS is a server-side thing. Lose all the headers junk in login(). Next, please read this post.

Done? checkToken(), which should be renamed because that’s not what it does, needs to return Observable<string>, because it’s of type C in that linked post, as getDataHomepage() cares about how it resolves. Its first word should also be return. That is going to make you have to rethink how it’s structured (likely using fromPromise and mergeMap), but once that’s done, then there will be no subscribe in it, and the subscribe will move to getDataHomepage instead, in which you will have the needed value.

Incidentally, I would strongly recommend looking into the interceptor feature introduced in the new HttpClient API, now that Ionic officially supports it. You can centralize all of your token management using it, so you won’t have to repeat all that boilerplate in other service provider methods.

Finally, storing passwords on device is extremely careless from a security perspective. Please stop doing that.


#3

Thank @rapropos for your help. Can you give me a sample code for this ? I’m really don’t know how to do it :frowning:


#4

Anyone help ? I’m still stuck :frowning:


#5

If you’re using JWT with a refreshtoken, you should check every HTTP call, if it returns an error code (let’s say Token expired) you have to use the refreshtoken to get a new one. Store it and use it while it lasts, then rinse and repeat if required.
I have some HTTP interceptor code to share but I guess you’ll have to modify some of your server logic or adapt the code to make a silent login.
J


#6

Hi @jsantari. Please share me the code for silent login. I don’t know how to change my code to the ionic way ? Any ideas ?
Thanks for your help.


#7

if you don’t know what is JWT you have a nice reading ahead. lots of links and information so I won’t post any.
In your app.module.ts file:

// Ref.: https://github.com/angular/angular/issues/11262
export function httpFactory(backend: XHRBackend, options: RequestOptions, global: AppState, toastCtrl: ToastController, eventService: EventsService) {
  return new ExtendedHttpService(backend, options, global, toastCtrl, eventService);
}

before the @ngMoidule declaration.
And in the same file, in the providers section:

// Ref.: https://gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c#gistcomment-2041630
    {
      provide: Http,
      useFactory: httpFactory,
      deps: [XHRBackend, RequestOptions, AppState, ToastController, EventsService]
   }

Remember to import all required dependencies.

In our project we had to make a complete “extended-http.service.ts” to handle expired tokens in a silent way (here goes the complete file):

// Ref.: https://gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
import { Injectable } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

import { ToastController } from "ionic-angular";
import { AppState } from "../app/app.global";
import { EventsService } from "./events-service";

@Injectable()
export class ExtendedHttpService extends Http {    

  constructor(backend: XHRBackend, 
    defaultOptions: RequestOptions,
    public global: AppState,
    public toastCtrl: ToastController,
    public eventService: EventsService) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    //do whatever 
    if (typeof url === 'string') {
      if (!options) {        
        options = { headers: new Headers() };        
      }
      this.setHeaders(options);
    } else {
      this.setHeaders(url);
    }    

    return super.request(url, options).catch(this.catchErrors());
  }

  private catchErrors() {
    return (res: Response) => {
      if (res.status === 401 || res.status === 403) {
          let jsonRes: any = res.json();
          console.log('http interceptor catchErrors', jsonRes);          
          
          if(jsonRes.errorCode === 11){ // Token has expired
            this.refreshToken(this.global.get('refreshToken'))
                .map((res) => res.json() )
                .subscribe((data) => {
                        console.log('refreshToken', data);
                        let newToken: string = data.token;                    
                        this.global.set('token', newToken);                    
                    },
                    err => {
                        let jsonErr: any = err.json();
                        if(jsonErr.error === 'Unauthorized' && jsonErr.path === '/api/token'){
                            // RefreshToken has expired
                            // Show toast "Debe reingresar sus credenciales"                        
                            this.presentToast('Debe reingresar sus credenciales.', true, 'LoginPage');
                        }             
                    }
                );
          }        
        
        if(res.status === 403){
            this.presentToast('Debe reingresar sus credenciales.', true, 'LoginPage');
        }          
      }
      return Observable.throw(res);
    };
  }

  private setHeaders(objectToSetHeadersTo: Request | RequestOptionsArgs) {
    //add whatever header that you need to every request
    //in this example I add header token by using authService that I've created
    objectToSetHeadersTo.headers.set('Content-Type', 'application/json');
    objectToSetHeadersTo.headers.set('X-Requested-With', 'XMLHttpRequest'); 
    
    let token: string = this.global.get('token');
    token = (!!token)?token:'';
    objectToSetHeadersTo.headers.set('X-Authorization',  'Bearer ' + token);       
  }

  
  private refreshToken(refreshToken: string = ''): Observable<Response> {
      let BASE_URL: string = this.global.get('apiServer');      
      let headers: Headers = new Headers();      

      headers.append('Content-Type', 'application/json');
      headers.append('X-Requested-With', 'XMLHttpRequest');    
      headers.append('X-Authorization', 'Bearer ' + refreshToken);

      return super.request(BASE_URL + '/token', { headers: headers });
  }

  private toastActive: boolean = false;
  private presentToast(message: string, showCloseButton: boolean = false, targetPage?: string) {    

    if(!this.toastActive){
        let toast = this.toastCtrl.create({
            message: message,
            duration: (!showCloseButton)?1750:undefined,
            showCloseButton: showCloseButton,
            closeButtonText: 'Cerrar',
            position: 'bottom',
            dismissOnPageChange: true
        });   
        toast.dismissAll();

        toast.present().then(() => {
            this.toastActive = true;
            toast.onDidDismiss(() => {
                console.log('Dismissed toast');
                this.toastActive = false;                
                if(!!targetPage){                
                    this.eventService.broadcast('openPage', targetPage);
                }
            });
        });
    }
    
  }
}

We also use an EventService (found in https://stackoverflow.com/questions/34700438/global-events-in-angular-2 )
to call other module’s functions using event broadcasting (kind of ala AngularJS way).
Please try to understand the code, don’t just copy & paste it, otherwise you wion’t even understand how to solve your problem.

Best regards.
J


#8

The advice in the previous post is obsoleted by the Angular 4.3 HttpClient.


#9

Maybe so, but it helps in understanding the new API.
Here’s a good article for the afternoon reading https://blog.angularindepth.com/the-new-angular-httpclient-api-9e5c85fe3361
(the interceptor part is the one that can help you @Peter_D )

Best regards,
J


#10

Thanks @jsanta and @rapropos very much. I’m going to check it now


#11

I’ve added observable to function checkToken(), but It doesn’t return the data as I want, It returned Observable {_isScalar: false,…}. How can I get the data ?

  getToken():Observable<any> {
     return Observable.fromPromise(  this.storage.get('user').then((user) => {
      if(user) {
        this.token = user.token;
        this.token_date = user.token_date - 100;
        this.currentDate = new Date();
        this.expDate = new Date(this.token_date * 1000);
        if (this.expDate === this.currentDate) {
          // token expired, login again     
          console.log('first');  
    let headers = new Headers();
    headers.append('Access-Control-Allow-Origin', '*');
    headers.append('Access-Control-Allow-Methods', '*');
    headers.append('Access-Control-Allow-Headers', 'Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With');    
    let body = {
      "user_id" : identifi,
      "password" : password
    };
    let options = new RequestOptions({ headers: headers });
    return this.http.post(api.logIn, body, options).map((res:Response) => res.json());
        }
      }
    })
)
  }

  getDataHomepage(retailerId: number,pageIndex: number) {
          this.checkToken().subscribe(
            data => {
              console.log(data); //return Observable {_isScalar: false,...}
            });
    let headers = new Headers();
    headers.append('Authorization', 'Bearer ' + this.token);
    let options = new RequestOptions({ headers: headers });
    return this.http.get(api.promotion + '?retailer_id=' + retailerId + '&page=' + pageIndex, options).map((res:Response) => res.json());
  }

#12

Still stuck…Anyone can help me ?


#13

Unless your function is called in the exact millisecond, your code will never enter the expired block ( currentDate >== expiredDate will work better, even better using millis instead of Date, formatted dates are for us humans, machine works better with plain numbers).

Also I see no need to make a observable out of a promise, http post id already returning an observable, just chain the map before subscribing to checkToken.