Full Refresh Token System in Angular (Enterprise)

Full Refresh Token System in Angular (Enterprise)

This Angular setup includes:

  • ✅ Access Token + Refresh Token
  • ✅ HttpInterceptor
  • ✅ Auto refresh when 401 happens
  • ✅ Retry the original failed request
  • ✅ Prevent multiple refresh calls (queue system)
  • ✅ Works for multiple parallel API calls
  • ✅ Enterprise clean structure

Folder Structure

src/app/core/auth/
  auth.service.ts
  token.service.ts
  auth.interceptor.ts
  auth.guard.ts (optional)

1️⃣ TokenService (Enterprise)

Handles storing and retrieving tokens from localStorage.


import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class TokenService {
  private accessKey = 'access_token';
  private refreshKey = 'refresh_token';

  getAccessToken(): string | null {
    return localStorage.getItem(this.accessKey);
  }

  getRefreshToken(): string | null {
    return localStorage.getItem(this.refreshKey);
  }

  setTokens(accessToken: string, refreshToken: string) {
    localStorage.setItem(this.accessKey, accessToken);
    localStorage.setItem(this.refreshKey, refreshToken);
  }

  clearTokens() {
    localStorage.removeItem(this.accessKey);
    localStorage.removeItem(this.refreshKey);
  }
}

2️⃣ AuthService (Refresh Token API)


import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface RefreshTokenRequest { refreshToken: string; }
export interface RefreshTokenResponse { accessToken: string; refreshToken: string; }

@Injectable({ providedIn: 'root' })
export class AuthService {
  private baseUrl = 'https://localhost:5001/api/auth';

  constructor(private http: HttpClient) {}

  refreshToken(refreshToken: string): Observable {
    return this.http.post(
      `${this.baseUrl}/refresh-token`,
      { refreshToken }
    );
  }
}

3️⃣ AuthInterceptor (Enterprise Core)

Handles token injection, auto-refresh, retry, and queuing multiple requests.


import { Injectable } from '@angular/core';
import {
  HttpInterceptor, HttpRequest, HttpHandler,
  HttpEvent, HttpErrorResponse
} from '@angular/common/http';
import { Observable, BehaviorSubject, throwError } from 'rxjs';
import { catchError, filter, switchMap, take } from 'rxjs/operators';
import { TokenService } from './token.service';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshTokenSubject = new BehaviorSubject(null);

  constructor(private tokenService: TokenService, private authService: AuthService) {}

  intercept(req: HttpRequest, next: HttpHandler): Observable> {
    if (req.url.includes('/refresh-token')) return next.handle(req);

    const accessToken = this.tokenService.getAccessToken();
    const authReq = accessToken ? this.addToken(req, accessToken) : req;

    return next.handle(authReq).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) return this.handle401Error(authReq, next);
        return throwError(() => error);
      })
    );
  }

  private addToken(request: HttpRequest, token: string) {
    return request.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
  }

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

      const refreshToken = this.tokenService.getRefreshToken();
      if (!refreshToken) {
        this.tokenService.clearTokens();
        window.location.href = '/login';
        return throwError(() => new Error('Refresh token missing.'));
      }

      return this.authService.refreshToken(refreshToken).pipe(
        switchMap(res => {
          this.isRefreshing = false;
          this.tokenService.setTokens(res.accessToken, res.refreshToken);
          this.refreshTokenSubject.next(res.accessToken);
          return next.handle(this.addToken(request, res.accessToken));
        }),
        catchError(err => {
          this.isRefreshing = false;
          this.tokenService.clearTokens();
          window.location.href = '/login';
          return throwError(() => err);
        })
      );
    }

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

4️⃣ Register Interceptor


import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './core/auth/auth.interceptor';

@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  ]
})
export class AppModule {}

5️⃣ How it works

  • 1️⃣ Angular sends API request with Access Token
  • 2️⃣ If API returns 401 → interceptor calls refresh-token API
  • 3️⃣ New access token is received
  • 4️⃣ Original request is retried automatically
  • 5️⃣ Multiple parallel requests only trigger one refresh call; others wait in queue

6️⃣ Enterprise Add-ons

  • Prevent infinite refresh loop by skipping refresh-token URL
  • Logout and redirect on failed refresh
  • BehaviorSubject queues waiting requests efficiently

Comments

Popular posts from this blog

Debouncing & Throttling in RxJS: Optimizing API Calls and User Interactions

Promises in Angular

Comprehensive Guide to C# and .NET Core OOP Concepts and Language Features