Angular2-jwt - AuthHttp, refresh tokens, putting it all together

blgrnboy picture blgrnboy · Jan 17, 2017 · Viewed 12k times · Source

It kind of seems like I am going in circles here, and perhaps it's because of the use of so many subscriptions and having to chain them together.

I want to be able to refresh a token if it's expired using the refresh token. Here is what I have, and I would really appreciate a simple working example if possible.

In summary, how can I ensure that the AudienceService first checks if the token is valid, if not, it tries to refresh it using the refresh token, and then makes a call to the endpoint with the appropriate token?

app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule } from '@angular/router';
import { Http, RequestOptions } from '@angular/http';
import { ConfirmDialogModule, ListboxModule, PickListModule } from 'primeng/primeng';

import { AppComponent } from './app.component';
import { HeaderComponent } from './components/header/header.component';
import { HomeComponent } from './components/home/home.component';
import { ListAudiencesComponent } from './components/audiences/list-audiences/list-audiences.component';

import { AudienceService } from './services/audience.service';
import { LoggingService } from './services/logging.service';
import { RoleService } from './services/role.service';
import { AuthService } from './services/auth.service';
import { UserService } from './services/user.service';
import { AuthGuard } from './services/auth-guard.service'
import { AuthHttp, AuthConfig, provideAuth } from 'angular2-jwt';
import { ListRolesComponent } from './components/roles/list-roles/list-roles.component';
import { EditRoleAudiencesComponent } from './components/roles/edit-role-audiences/edit-role-audiences.component';
import { ModifyRoleComponent } from './components/roles/modify-role/modify-role.component';
import { LoginComponent } from './components/login/login.component';
import { UnauthorizedComponent } from './components/unauthorized/unauthorized.component';

export function authHttpServiceFactory(http: Http, options: RequestOptions) {
  return new AuthHttp(new AuthConfig({
    tokenName: 'token',
          tokenGetter: (() => sessionStorage.getItem('id_token')),
          globalHeaders: [{'Content-Type':'application/json'}],
     }), http, options);
}

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    HomeComponent,
    ListAudiencesComponent,
    ListRolesComponent,
    EditRoleAudiencesComponent,
    ModifyRoleComponent,
    LoginComponent,
    UnauthorizedComponent
  ],
  imports: [
    BrowserModule,
    ConfirmDialogModule,
    FormsModule,
    HttpModule,
    ListboxModule,
    PickListModule,
    ReactiveFormsModule,
    RouterModule.forRoot([
            { path: '', redirectTo: 'home', pathMatch: 'full' },
            { path: 'home', component: HomeComponent },
            { path: 'unauthorized', component: UnauthorizedComponent },
            { path: 'audiences', component: ListAudiencesComponent, canActivate: [AuthGuard] },
            { path: 'roles', component: ListRolesComponent, canActivate: [AuthGuard] },
            { path: 'roles/modify/:name', component: ModifyRoleComponent, canActivate: [AuthGuard] },
            { path: '**', redirectTo: 'home' }
        ]),
  ],
  providers: [
    {
      provide: AuthHttp,
      useFactory: authHttpServiceFactory,
      deps: [Http, RequestOptions]
    },
    AudienceService, AuthGuard, AuthService, LoggingService, RoleService, UserService
    ],
  bootstrap: [AppComponent]
})
export class AppModule { }

auth.service.ts:

import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions, URLSearchParams } from '@angular/http';
import { environment } from '../../environments/environment';
import { tokenNotExpired } from 'angular2-jwt';

@Injectable()
export class AuthService {

  tokenEndpoint = environment.token_endpoint;
  constructor(private http: Http ) { }

  login(username: string, password: string) {
    let headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
    let options = new RequestOptions({ headers: headers });
    let body = new URLSearchParams();
    body.set('username', username);
    body.set('password', password);
    body.set('client_id', '099153c2625149bc8ecb3e85e03f0022');
    body.set('grant_type', 'password');

    console.log("Got here");

    return this.http.post(this.tokenEndpoint, body, options)
    .map(res => res.json())
    .subscribe(
        data => {
          localStorage.setItem('id_token', data.access_token);
          localStorage.setItem('refresh_token', data.refresh_token);
        },
        error => console.log(error)
      );
  }

  loggedIn() {
    if (tokenNotExpired()) {
      return true;
    } else {
      this.refreshToken()
      .subscribe(
          data => {
            if (data.error) {
              this.logout();
            } else {
              localStorage.setItem('id_token', data.access_token);
              localStorage.setItem('refresh_token', data.refresh_token);
              console.log("Token was refreshed.");
            }
          },
          error => this.logout(),
          () =>  {
            return tokenNotExpired();
          }
        );
    }
  }

  refreshToken() {
    let refToken = localStorage.getItem('refresh_token');
    if (refToken) {
      let headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
      let options = new RequestOptions({ headers: headers });
      let body = new URLSearchParams();
      body.set('client_id', '099153c2625149bc8ecb3e85e03f0022');
      body.set('grant_type', 'refresh_token');
      body.set('refresh_token', refToken);

      return this.http.post(this.tokenEndpoint, body, options)
      .map(res => res.json());
    } else {
      this.logout();
    }
  }

  tokenRequiresRefresh(): boolean {
    if (!this.loggedIn()) {
      console.log("Token refresh is required");
    }

    return !this.loggedIn();
  }

  logout() {
    localStorage.removeItem('id_token');
    localStorage.removeItem('refresh_token');
  }
}

audience.service.ts:

import 'rxjs/Rx';
import { Observable } from 'rxjs/Observable';
import { environment } from '../../environments/environment';
import { AuthHttp } from 'angular2-jwt';
import { AuthService } from './auth.service';

import { AddDeleteAudienceModel } from './AddAudienceModel';

@Injectable()
export class AudienceService {

  baseApiUrl = environment.api_endpoint;

  constructor(private http: Http, private authHttp: AuthHttp, private authService: AuthService) { }

  getAllAudiences()
  {
    if (this.authService.tokenRequiresRefresh()) {
      this.authService.refreshToken();
    }

    if (this.authService.loggedIn()) {
      return this.authHttp.get(this.baseApiUrl + 'audience/all').map(res => res.json());
    }
  }
}

Answer

blgrnboy picture blgrnboy · Feb 14, 2017

auth.service.ts

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Http, Headers, RequestOptions, URLSearchParams } from '@angular/http';
import { environment } from '../../environments/environment';
import { tokenNotExpired, JwtHelper } from 'angular2-jwt';
import { Subject, Observable } from 'rxjs';

@Injectable()
export class AuthService {

  tokenEndpoint = environment.token_endpoint;
  requireLoginSubject: Subject<boolean>;
  tokenIsBeingRefreshed: Subject<boolean>;
  lastUrl: string;
  jwtHelper: JwtHelper = new JwtHelper();

  constructor(private http: Http, private router: Router) { 
    this.requireLoginSubject = new Subject<boolean>();
    this.tokenIsBeingRefreshed = new Subject<boolean>();
    this.tokenIsBeingRefreshed.next(false);
    this.lastUrl = "/home";
  }

  isUserAuthenticated() {

    if(this.loggedIn()) {
      this.requireLoginSubject.next(false);
      return true;
    } else {
      return false;
    }
  }

  login(username: string, password: string) {
    let headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
    let options = new RequestOptions({ headers: headers });
    let body = new URLSearchParams();
    body.set('username', username);
    body.set('password', password);
    body.set('client_id', '099153c2625149bc8ecb3e85e03f0022');
    body.set('grant_type', 'password');

    return this.http.post(this.tokenEndpoint, body, options).map(res => res.json());
  }

  loggedIn() {
    return tokenNotExpired();
  }

  addTokens(accessToken: string, refreshToken: string) {
    localStorage.setItem('id_token', accessToken);
    localStorage.setItem('refresh_token', refreshToken);
  }

  getRefreshTokenExpirationDate() {
    var token = localStorage.getItem('id_token');
    if (token) {
      let tokenExpDate = this.jwtHelper.getTokenExpirationDate(token);
      let sessionExpDate = new Date(tokenExpDate.getTime() + 4*60000);
      if (new Date() > sessionExpDate) {
        this.logout();
      }
      return sessionExpDate;
    }

    return null;
  }

  hasRefreshToken() {
    let refToken = localStorage.getItem('refresh_token');

    if (refToken == null) {
      this.logout();
    }

    return refToken != null;
  }

  refreshTokenSuccessHandler(data) {
    if (data.error) {
        console.log("Removing tokens.");
        this.logout();
        this.requireLoginSubject.next(true);
        this.tokenIsBeingRefreshed.next(false);
        this.router.navigateByUrl('/unauthorized');
        return false;
    } else {
        this.addTokens(data.access_token, data.refresh_token);
        this.requireLoginSubject.next(false);
        this.tokenIsBeingRefreshed.next(false);
        console.log("Refreshed user token");
    }
  }

  refreshTokenErrorHandler(error) {
    this.requireLoginSubject.next(true);
    this.logout();
    this.tokenIsBeingRefreshed.next(false);
    this.router.navigate(['/sessiontimeout']);
    console.log(error);
  }

  refreshToken() {
    let refToken = localStorage.getItem('refresh_token');
    //let refTokenId = this.jwtHelper.decodeToken(refToken).refreshTokenId;
    let headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
    let options = new RequestOptions({ headers: headers });
    let body = new URLSearchParams();
    body.set('client_id', '099153c2625149bc8ecb3e85e03f0022');
    body.set('grant_type', 'refresh_token');
    body.set('refresh_token', refToken);

    return this.http.post(this.tokenEndpoint, body, options)
      .map(res => res.json());
  }

  tokenRequiresRefresh(): boolean {
    if (!this.loggedIn()) {
      console.log("Token refresh is required");
    }

    return !this.loggedIn();
  }

  logout() {
    localStorage.removeItem('id_token');
    localStorage.removeItem('refresh_token');
    this.requireLoginSubject.next(true);
  }
}

auth-http.service.ts

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import 'rxjs/Rx';
import { Observable } from 'rxjs/Observable';
import { environment } from '../../environments/environment';
import { AuthHttp } from 'angular2-jwt';
import { AuthService } from './auth.service';

@Injectable()
export class AuthHttpService {

  constructor(private authHttp: AuthHttp, private authService: AuthService, private router: Router) { }

  get(endpoint: string) {
    if (this.authService.tokenRequiresRefresh()) {
      this.authService.tokenIsBeingRefreshed.next(true);
      return this.authService.refreshToken().switchMap(
        (data) => {
          this.authService.refreshTokenSuccessHandler(data);
          if (this.authService.loggedIn()) {
            this.authService.tokenIsBeingRefreshed.next(false);
            return this.getInternal(endpoint);
          } else {
            this.authService.tokenIsBeingRefreshed.next(false);
            this.router.navigate(['/sessiontimeout']);
            return Observable.throw(data);
          }
        }
      ).catch((e) => {
        this.authService.refreshTokenErrorHandler(e);
        return Observable.throw(e);
      });
    }
    else {
      return this.getInternal(endpoint);
    }
  }

  post(endpoint: string, body: string) : Observable<any> {
    if (this.authService.tokenRequiresRefresh()) {
      this.authService.tokenIsBeingRefreshed.next(true);
      return this.authService.refreshToken().switchMap(
        (data) => {
          this.authService.refreshTokenSuccessHandler(data);
          if (this.authService.loggedIn()) {
            this.authService.tokenIsBeingRefreshed.next(false);
            return this.postInternal(endpoint, body);
          } else {
            this.authService.tokenIsBeingRefreshed.next(false);
            this.router.navigate(['/sessiontimeout']);
            return Observable.throw(data);
          }
        }
      ).catch((e) => {
        this.authService.refreshTokenErrorHandler(e);
        return Observable.throw(e);
      });
    }
    else {
      return this.postInternal(endpoint, body);
    }
  }

  private getInternal(endpoint: string) {
    return this.authHttp.get(endpoint);
  }

  private postInternal(endpoint: string, body: string) {
    return this.authHttp.post(endpoint, body);
  }

}

audience.service.ts

import { Injectable } from '@angular/core';
import 'rxjs/Rx';
import { Observable } from 'rxjs/Observable';
import { environment } from '../../environments/environment';
import { AuthHttpService } from './auth-http.service';

import { AddDeleteAudienceModel } from './AddAudienceModel';

@Injectable()
export class AudienceService {

  baseApiUrl = environment.api_endpoint;

  constructor(private authHttpService: AuthHttpService) { }

  getAllAudiences()
  {
    return this.authHttpService.get(this.baseApiUrl + 'audience/all').map(res => res.json());
  }

  addAudience(model: AddDeleteAudienceModel) {
    return this.authHttpService.post(this.baseApiUrl + 'audience', JSON.stringify(model)).map(res => res.json());
  }

  deleteAudience(model: AddDeleteAudienceModel) {
    return this.authHttpService.post(this.baseApiUrl + 'audience/delete', JSON.stringify(model)).map(res => res.json());
  }

}