Search code examples
angularjwtangular2-jwt

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


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());
    }
  }
}

Solution

  • 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());
      }
    
    }