Search code examples
angularpromiserxjsionic4ionic-storage

Ionic storage get returns a promise "Bearer [object Promise]" How to return a value and use it as authorization token?


Ionic storage.get('token').then() function returns a promise so it is returning a promise object instead of the refresh token.

I am working on a Ionic 4 angular project where I am using JWT for authentication. Using the HTTP interceptor I was able to send the access token as the authorization headers bearer token. Because JWT expires very quickly I need to refresh the token. I am using a Python and Flask backend where after successful login the server response contains both the access i.e. JWT and refresh token. In my Python server to the refresh the token I need to do a POST request to the refresh end point with the refresh token as as the authorization headers bearer token. In response the server send me the access token.

The steps that I have followed are:

  1. After successful log in I am saving the access token and the refresh token in the Ionic storage.
  2. Sending the access token with each request using the Angular HTTP interceptor.
  3. If there is an error the server response with the appropriate error response code then I was sending a refresh token request adding the refresh token as the bearer token authorization header
  4. Then from the server response saving the access token in the Ionic storage again and adding the new access token with each request.

The problem that I am facing is when I send the refresh token request instead of sending the refresh token as the authorization header the request is sending a “Bearer [object Promise]”.

The problem is in my auth service and getAccessTokenUsingRefreshToken( ) function which returns an observable. as this.storage.get(‘refresh_token’).then( ) returns a promise so it is returning a promise object instead of the token.

The code of my auth service is as follows:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse, HttpRequest } from '@angular/common/http';
import { BehaviorSubject, throwError, Observable, from } from 'rxjs';
import { Platform, AlertController } from '@ionic/angular';
import { Storage } from '@ionic/storage';
import { JwtHelperService } from '@auth0/angular-jwt';
import { tap, catchError, mergeMap } from 'rxjs/operators';
import { User } from '../models/user.model';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {
  constructor(
    private http: HttpClient,
    private helper: JwtHelperService,
    private storage: Storage,
    private platform: Platform,
    private alertController: AlertController) {
    // this.platform.ready().then(() => {
    //   this.checkToken();
    // });
  }
  url = 'http://localhost:5000'; 
  ACCESS_TOKEN = 'access_token';
  REFRESH_TOKEN = 'refresh_token';
  user = null;
  token;
  // refreshToken;
  authenticationState = new BehaviorSubject(false);





  register(user: User): Observable<User> {
    // if (user.id === null)
    console.log(user);
    return this.http.post<User>(`${this.url}/register`, user)
      .pipe(
        tap(res => {
          this.storage.set(this.ACCESS_TOKEN, res[this.ACCESS_TOKEN]);
          this.storage.set(this.REFRESH_TOKEN, res[this.REFRESH_TOKEN]);
          this.user = this.helper.decodeToken(res[this.ACCESS_TOKEN]);
          // console.log(this.storage.get(this.REFRESH_TOKEN));
          this.authenticationState.next(true);
        }),
      );
  }


  login(data) {
    return this.http.post(`${this.url}/auth`, data)
      .pipe(
        tap(res => {

          this.storage.set(this.ACCESS_TOKEN, res[this.ACCESS_TOKEN]);
          this.storage.set(this.REFRESH_TOKEN, res[this.REFRESH_TOKEN]);
          this.user = this.helper.decodeToken(res[this.ACCESS_TOKEN]);
          // this.storage.get(this.REFRESH_TOKEN);
          // console.log(this.storage.get(this.ACCESS_TOKEN));
          // console.log(this.getRefreshToken());
          this.authenticationState.next(true);
        }),
      );
  }

  logout() {
    this.storage.remove(this.ACCESS_TOKEN).then(() => {
      this.authenticationState.next(false);
    });
    this.storage.remove(this.REFRESH_TOKEN);
  }


  private addToken(token: any) {
    if (token) {
      const httpOptions = {
        headers: new HttpHeaders({
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${token}`
        })
      };
      return httpOptions;
    }
  }

 getAccessTokenUsingRefreshToken() {
    const refreshToken = this.storage.get('refresh_token').then((result) => {
      return result;
    });

    const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${refreshToken}`
      })
    };
    return this.http.post<any>(`${this.url}/token/refresh`, 'body', httpOptions ).pipe(tap(tokens => {
      console.log(tokens['access_token']);
      console.log(tokens);
      this.storage.set(this.ACCESS_TOKEN, tokens[this.ACCESS_TOKEN]);
      console.log(this.storage.get('access_token'));
    }));

  }


  checkToken(): Promise<any> {
    return this.storage.get(this.ACCESS_TOKEN).then(token => {
      if (token) {
        this.token = token;

        if (!this.helper.isTokenExpired(this.token)) {
          this.user = this.helper.decodeToken(this.token);
          this.authenticationState.next(true);
        } else {
          this.storage.remove(this.ACCESS_TOKEN);
          this.storage.remove(this.REFRESH_TOKEN);
        }
      }
    });
  }

  getToken() {
    return this.storage.get(this.ACCESS_TOKEN);
  }
  isAuthenticated() {
    return this.authenticationState.value;
  }


}



This is my HTTP interceptor code

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable, from, throwError, BehaviorSubject } from 'rxjs';
import { Storage } from '@ionic/storage';
// import { _throw } from 'rxjs/observable/throw';
import { catchError, mergeMap, switchMap, filter, take } from 'rxjs/operators';
import { AlertController } from '@ionic/angular';
import { AuthenticationService } from './authentication.service';


@Injectable({
  providedIn: 'root'
})
export class InterceptorService implements HttpInterceptor {
  private isRefreshing = false;
  private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);

  constructor(private storage: Storage, private alertCtrl: AlertController, private authenticationService: AuthenticationService) { }
  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    let promise = this.storage.get('access_token');

    return from(promise).pipe(mergeMap(token => {
      const clonedReq = this.addToken(req, token);
      return next.handle(clonedReq).pipe(catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          // console.log('executed');
          console.log(req);
          return this.handle401Error(req, next);
        } else {
          return throwError(error.message);
        }
      })
      );
    }
    ));
  }

  // Adds the token to your headers if it exists
  private addToken(request: HttpRequest<any>, token: any) {
    if (token) {
      let clone: HttpRequest<any>;
      clone = request.clone({
        setHeaders: {
          Accept: `application/json`,
          'Content-Type': `application/json`,
          Authorization: `Bearer ${token}`
        }
      });
      return clone;
    }

    return request;
  }


  private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      return this.authenticationService.getAccessTokenUsingRefreshToken().pipe(
        switchMap((token: any) => {
          this.isRefreshing = false;
          console.log(token);
          console.log('executed');
          this.refreshTokenSubject.next(token.access_token);
          return next.handle(this.addToken(request, token.access_token));
        }));

    } else {
      return this.refreshTokenSubject.pipe(
        filter(token => token != null),
        take(1),
        switchMap(access_token => {
          return next.handle(this.addToken(request, access_token));
        }));
    }
  }


}



Solution

  • I was finally able to solve it. The issue was in my interceptor. In my previous code I was intercepting each and every request and sending the Authorization header bearer token with it. The problem with that approach was when I was trying to get the access token using the refresh token my HTTP interceptor was sending the expired access token as the Authorization header as well. So I had to a logic in my intercept method where for any request in the refresh token endpoint should only have the request without the access token.

    if (req.url.endsWith('/token/refresh')) {
      return next.handle(req);
    }
    

    So, This is final code of the InterceptorService.ts

    import { Injectable } from '@angular/core';
    import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders, 
    HttpErrorResponse } from '@angular/common/http';
    import { Observable, from, throwError, BehaviorSubject } from 'rxjs';
    import { Storage } from '@ionic/storage';
    // import { _throw } from 'rxjs/observable/throw';
    import { catchError, mergeMap, switchMap, filter, take, map } from 'rxjs/operators';
    import { AlertController } from '@ionic/angular';
    import { AuthenticationService } from './authentication.service';
    
    
    @Injectable({
      providedIn: 'root'
    })
    export class InterceptorService implements HttpInterceptor {
      private isRefreshing = false;
      private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
    
      constructor(private storage: Storage, private alertCtrl: AlertController, 
    private authenticationService: AuthenticationService) { }
      intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    
    // sending the request only for the refresh token endpoint
    if (req.url.endsWith('/token/refresh')) {
      return next.handle(req);
    }
    
    let promise = this.storage.get('access_token');
    
    return from(promise).pipe(mergeMap(token => {
      const clonedReq = this.addToken(req, token);
      console.log(req);
      return next.handle(clonedReq).pipe(catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 500) {
          // console.log('executed');
          return this.handleAccessError(req, next);
        } else {
          return throwError(error.message);
        }
      })
      ) as any;
    }
    )) as any;
     }
    
      // Adds the token to your headers if it exists
      private addToken(request: HttpRequest<any>, token: any) {
      if (token) {
      let clone: HttpRequest<any>;
      clone = request.clone({
        setHeaders: {
          Accept: `application/json`,
          'Content-Type': `application/json`,
          Authorization: `Bearer ${token}`
        }
      });
      return clone;
    }
    
    return request;
    }
    
    
    
      private handleAccessError(request: HttpRequest<any>, next: HttpHandler) {
      if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);
    
      return this.authenticationService.getAccessTokenUsingRefreshToken().pipe(
        switchMap((token: any) => {
          this.isRefreshing = false;
          this.refreshTokenSubject.next(token);
          return next.handle(this.addToken(request, token));
        }));
    
    } else {
      return this.refreshTokenSubject.pipe(
        filter(token => token != null),
        take(1),
        switchMap(jwt => {
          return next.handle(this.addToken(request, jwt));
        }));
    }
    }
    
    }
    

    This is my final getAccessTokenUsingRefreshToken() method in the AuthenticationService:

        getAccessTokenUsingRefreshToken(): Observable<string> {
        return from(this.storage.get('refresh_token')).pipe(
          switchMap(refreshToken => {
            const httpOptions = {
              headers: new HttpHeaders({
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${refreshToken}`
              })
            };
            return this.http.post<any>(`${this.url}/token/refresh`, {}, httpOptions);
          }),
          map(response => response.access_token),
          tap(accessToken => this.storage.set(this.ACCESS_TOKEN, accessToken))
        );
      }