user15684725
user15684725

Reputation: 31

Refresh JWT expired token with Angular 11 interceptor from Spring Boot back end

I am working on a spring boot + angular project in which a user logs in from Angular front end to the authentication api on Spring Boot, which returns a JWT token. I also set up an interceptor on Angular that appends the Authorization header with the JWT token for all requests.

I am looking for a way to intercept angualar requests so that when spring boot throws a 401 error once the JWT token is expired, the Angular front end will try to contact the new refreshtoken endpoint with the expired JWT and a new "isRefreshToken" header set to true to receive a new JWT.

This is my current AuthService

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

  constructor(private http: HttpClient) {}

  login(username: string, password: string) {
    return this.http
      .post<iUser>('http://localhost:8080/authenticate', { username, password }).pipe(
        tap(res => this.setSession(res)),
        shareReplay()
      )      
  }

  refreshToken(){
    return this.http.post<iUser>('http://localhost:8080/refreshtoken', {responseType: 'text' as 'json'}).pipe(
      tap(res => this.setSession(res)),
      shareReplay()
    )
  }

  private setSession(authResult) {

    let tokenInfo = this.getDecodedAccessToken(authResult.token);   
    const expiresAt = moment(tokenInfo.exp);

    localStorage.setItem('id_token', authResult.token);
    localStorage.setItem('expires_at', JSON.stringify(expiresAt.valueOf()));
    localStorage.setItem('userId', tokenInfo.userId);

  }

  logout() {
    localStorage.removeItem('id_token');
    localStorage.removeItem('expires_at');
    localStorage.removeItem('userId');
  }

  public isLoggedIn() {
    return moment().isBefore(this.getExpiration());
  }

  isLoggedOut() {
    return !this.isLoggedIn();
  }

  getExpiration() {
    const expiration = localStorage.getItem('expires_at');
    const expiresAt = JSON.parse(expiration);
    return moment.unix(expiresAt);
  }

  getDecodedAccessToken(token: string): any {
    try{
        return jwt_decode(token);
    }
    catch(Error){
        return null;
    }
  }
}

While this is the interceptor I am using:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

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


  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    let url = req.url.includes('localhost');

    const idToken = localStorage.getItem('id_token');

    if (idToken && url) {  

      const cloned = req.clone({
        headers: req.headers.set('Authorization', 'Bearer ' + idToken),
      });

      console.log(cloned);

      return next.handle(cloned);

    } else {
      return next.handle(req);
    }
  }
}

Upvotes: 1

Views: 2907

Answers (1)

avishekdr
avishekdr

Reputation: 1068

I can suggest to you another way of doing this which I used recently to log out a user when the token expires. Let me share my approach first:

loginUser(email: string, password: string) {
    const authData: AuthData = { email: email, password: password };
    this.http.post<{ token: string, expiresIn: number }>('http://localhost:3000/api/users/login', authData).subscribe( response => {
      const token = response.token;
      this.token = token;
      if(token) {
        const expiresInDuration = response.expiresIn;
        this.tokenTimer = setTimeout(() => {
          this.logout();
        }, expiresInDuration*1000);
        this.isAuthenticated = true;
        this.authStatusListener.next(true);
        const now = new Date();
        const expirationDate = new Date(now.getTime() + (expiresInDuration * 1000));
        this.saveAuthData(token, expirationDate);
        this.router.navigate(['']);
      }
    });
  }

logout() {
    this.token = null;
    this.isAuthenticated = false;
    this.authStatusListener.next(false);
    clearTimeout(this.tokenTimer);
    this.clearAuthData();
    this.router.navigate(['']);
  }

  private saveAuthData(token: string, expirationDate: Date) {
    localStorage.setItem('token', token);
    localStorage.setItem('expirationDate', expirationDate.toISOString());
  }

So what I have done here is I have received a expireIn value which is in seconds when the token will expire. Then I have set a timeout callback method which will be called when that time is reached. Here I've logged out in your case you can call the API for the refresh token & make desired changes as per your requirement. So coming back to the point, I have set the expiration date & time with respect to the current date-time of login with expiresIn duration. Also added a authStatusListener Subject which will listen to the state of the authentication till it expires.

Nothing extra to do in Interceptor/Guard for token expiration

For controlling the logout button in header just do like this:

 userIsAuthenticated = false;
  private authListenerSubs: Subscription;
  constructor(private authService: AuthService) { }

  onLogout() {
    this.authService.logout();
  }

  ngOnInit() {
    this.authListenerSubs = this.authService.getAuthStatusListener().subscribe(isAuthenticated => {
      this.userIsAuthenticated = isAuthenticated;
    });
  }

  ngOnDestroy() {
    this.authListenerSubs.unsubscribe();
  }

Use userIsAuthenticated in ngIf in the HTML.

For a real scenario you can take help of this github repo.

Upvotes: 0

Related Questions