Anuj TBE
Anuj TBE

Reputation: 9800

Angular 6: refresh token not working as expected

I'm using Angular 6 and using the API endpoint of Django REST Framework with OAuth2 implementation.

I'm my Angular application, I'm storing access_token, refresh_token, expires_in, generate_time, user_valid, token_type while user logins using his username and password, in localStorage

Since the expires_in is quite low up to few minutes, the access_token will expire even while the user is active on the page. Therefore, I need to regenerate access_token using saved refresh_token.

I have auth.interceptor.ts to added each request with access_token to authorize the request.

import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs';
import {AuthService} from './auth.service';
import {Injectable} from '@angular/core';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(
    public Auth: AuthService
  ) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('interceptor');
    request = request.clone({
      setHeaders: {
        Authorization: `${this.Auth.tokenType()} ${this.Auth.accessToken()}`
      }
    });

    return next.handle(request);
  }
}

and auth-guard.service.ts to guard protected URLs and also checking if the token is expired and regenerate access token if the token is expired.

import { Injectable } from '@angular/core';
import {CanActivate, Router} from '@angular/router';
import {AuthService} from './auth.service';

@Injectable({
  providedIn: 'root'
})

export class AuthGuardService implements CanActivate {

  constructor(
    public Auth: AuthService,
    public router: Router
  ) { }

  /**
   * this method is used to check if user is authenticated or not
   * if user is not authenticated, is redirected to login page
   */
  canActivate(): boolean {
    // if userValid is true and user is not authenticated,
    // probably access token has been expired,
    // refresh token
    if (this.Auth.userValid()) {
      if (!this.Auth.isAuthenticated()) {
        this.Auth.refreshAuthToken();
      }
    }

    // if user is not authenticated,
    // redirect to login page
    if (!this.Auth.isAuthenticated()) {
      this.router.navigate(['auth/login']);
    }
    return true;
  }
}

there is a auth.service.ts to check if the user is authenticated and valid and there is token.service.ts to manage tokens and save in localStorage and retrieve from storage.

import { Injectable } from '@angular/core';
import {HttpClient, HttpRequest} from '@angular/common/http';
import {ResourceProviderService} from '../resource-provider.service';
import {Observable} from 'rxjs';
import {AuthToken} from './auth.model';
import {TokenService} from './token.service';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  constructor(
    private resource: ResourceProviderService,
    private http: HttpClient,
    private token: TokenService
  ) { }

  /**
   * returns access token from token service
   */
  public accessToken(): string {
    return this.token.accessToken();
  }

  /**
   * Get authentication token credentials like access token, token secret, expires in
   * from the server for the authenticated user
   */
  getAuthToken(username: any, password: any): Observable<AuthToken> {
    const authFormData = new FormData();

    authFormData.append('grant_type', this.resource.grant_type);
    authFormData.append('client_id', this.resource.client_key);
    authFormData.append('client_secret', this.resource.client_secret);
    authFormData.append('scope', this.resource.scope);
    authFormData.append('username', username);
    authFormData.append('password', password);

    return this.http.post<AuthToken>(this.resource.url + '/auth/token/', authFormData);
  }

  /**
   * refresh token of the user using the refresh token stored
   */
  refreshAuthToken() {
    console.log('refresh token');
    const authFormData = new FormData();

    authFormData.append('grant_type', this.resource.grant_type_refresh_token);
    authFormData.append('client_id', this.resource.client_key);
    authFormData.append('client_secret', this.resource.client_secret);
    authFormData.append('refresh_token', this.token.refreshToken());


    this.http.post<AuthToken>(this.resource.url + '/auth/token/', authFormData).subscribe(
      response => {
        console.log('setting credentials');
        this.token.setCredentials(response);
      }, error => {
        console.log(error.status, error.error.error);
        console.log(error);
      }
    );
  }

  /**
   * Method set credentials using token service and
   * stores in local storage of the browser
   */
  setCredentials(response: AuthToken): void {
    this.token.setCredentials(response);
  }

  /**
   * Method is called to logout the user
   */
  clearCredentials(): void {
    this.token.clearCredentials();
  }

  isAuthenticated(): boolean {
    // Check whether the token is expired and return
    // true or false
    return !this.token.isTokenExpired();
  }

  userValid(): boolean {
    return this.token.userValid();
  }

  tokenType(): string {
    return this.token.tokenType();
  }
}

Whenever the access_token is expired, isAuthenticated() returns false and this.Auth.refreshAuthToken() is called from auth-guard service.

But it redirects to login page first and refreshAuthToken() is called after last and network request is broken after refreshing the token.

1. It does not redirect back to the page even after refreshing the access token.
2. Why does it load login page? I want to refresh the access token silently whenever the token is expired.

Upvotes: 1

Views: 2761

Answers (2)

Poul Kruijt
Poul Kruijt

Reputation: 71891

You should rewrite your canActivate method to account for the asynchronous behaviour of your refreshAuthToken method:

canActivate(): Observable<boolean> | boolean {
  if (!this.Auth.userValid()) {
    return this.cannotActivate();
  } else if (this.Auth.isAuthenticated()) {
    return true;
  } else {
    return this.Auth.refreshAuthToken().pipe(
      map(() => this.Auth.isAuthenticated() || this.cannotActivate())
    );
  }  
}

cannotActivate(): boolean {
  this.router.navigate(['auth/login']);
  return false;
}

But for this to work, you should also return an Observable from your refreshAuthToken method:

refreshAuthToken(): Observable<any> {
  //...

  return this.http.post<AuthToken>(this.resource.url + '/auth/token/', authFormData).pipe(
    tap((response) => this.token.setCredentials(response))
  )
}

Upvotes: 3

Ab Pati
Ab Pati

Reputation: 27

This is because, your auth guard is calling refresh token method from auth service which in turn is making an asynchronous HTTP request, and hence canActivate method in auth guard returns true before api calls complete because isAuthenticated() is still false.

I think you need to return a subscription from refreshToken method and subscribe it in auth guard. Once subscription is complete then you can return true and false. But before this we need to check whether canActivate will wait for subscription completion or not.

Regards Abhay

Upvotes: 2

Related Questions