Authentication is a core part of most web applications. In this article, we’ll walk through how an authentication workflow works in Angular, including login handling, redirect logic, and route protection using guards.

Overview

A typical Angular auth flow looks like this:

UserLoginSavetokenRedirectGuardprotectsroutes

We will break this down into smaller pieces.


1. AuthService (Core of Authentication)

AuthService is responsible for managing login state.

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

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

  private TOKEN_KEY = 'token';

  login(token: string) {
    localStorage.setItem(this.TOKEN_KEY, token);
  }

  logout() {
    localStorage.removeItem(this.TOKEN_KEY);
  }

  isLoggedIn(): boolean {
    return !!localStorage.getItem(this.TOKEN_KEY);
  }

  getToken() {
    return localStorage.getItem(this.TOKEN_KEY);
  }
}

Key idea:

  • Store token in localStorage
  • Check login status via isLoggedIn()

2. Login Flow

After successful login, redirect user to dashboard:

onLogin() {
  this.authService.login('fake-token'); // token from API
  this.router.navigate(['/dashboard']);
}

What happens:

  1. Save token. What is token? I will discuss in another post.
  2. Redirect to protected route

3. Route Protection with Auth Guard

Angular provides Route Guards to control access.

Create Guard

ng generate guard auth

Implement Guard

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

export const authGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isLoggedIn()) {
    return true;
  }

  return router.createUrlTree(['/login']);
};

Behavior:

  • Logged in → allow access
  • Not logged in → redirect to /login

4. Guest Guard (Prevent Access When Logged In)

We also need to block logged-in users from accessing login pages.

export const guestGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isLoggedIn()) {
    return router.createUrlTree(['/dashboard']);
  }

  return true;
};

5. Routing Configuration

export const routes = [
  {
    path: '',
    component: HomeComponent,
    canActivate: [guestGuard]
  },
  {
    path: 'login',
    component: LoginComponent,
    canActivate: [guestGuard]
  },
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard]
  }
];

6. Full Authentication Flow

Case 1: User not logged in

  • Access /dashboard
  • Redirect → /login

Case 2: User logs in

  • Save token.
  • Redirect → /dashboard

Case 3: Page reload

  • Token still in localStorage
  • Guard allows access

Case 4: Logged-in user accesses /login

  • Redirect → /dashboard

7. Bonus: Redirect Back After Login

Improve UX by returning user to original page.

In Guard:

return router.createUrlTree(['/login'], {
  queryParams: { returnUrl: state.url }
});

In Login:

const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';
this.router.navigate([returnUrl]);

8. HTTP Interceptor (Attach Token & Handle Errors)

After setting up authentication and route guards, we still need a way to automatically attach the token to every API request. This is where Angular HTTP Interceptor comes in.

What is an Interceptor?

An interceptor sits between your app and every HTTP request:

RequestInterceptorBackendResponseInterceptorApp

It allows us to:

  • Attach authentication headers
  • Handle errors globally
  • Avoid repeating logic in every API call

Implement Auth Interceptor

import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
import { catchError, throwError } from 'rxjs';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  const token = authService.getToken();

  // Skip public APIs
  const isPublicApi =
    req.url.includes('/login') ||
    req.url.includes('/register');

  let clonedReq = req;

  // Attach token if exists
  if (token && !isPublicApi) {
    clonedReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
  }

  return next(clonedReq).pipe(
    catchError((error: HttpErrorResponse) => {

      // Handle unauthorized error
      if (error.status === 401) {
        authService.logout();

        if (!router.url.includes('/login')) {
          router.navigate(['/login']);
        }
      }

      return throwError(() => error);
    })
  );
};

Register Interceptor

In a standalone Angular app, interceptors are registered in main.ts:

import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './core/interceptors/auth.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor])
    )
  ]
});

How It Works

  1. Every HTTP request goes through the interceptor

  2. Token is automatically added to headers

  3. If the server returns 401 Unauthorized:

    • User is logged out
    • Redirected to login page

Why It Matters

Without an interceptor:

  • You must manually attach tokens to every request

With an interceptor:

  • Centralized logic
  • Cleaner code
  • Easier to maintain and scale

Updated Auth Flow

UserLoginSavetokenInterceptorattachtokenAPIGuardprotectsroutes

Conclusion

Angular provides a clean and powerful way to handle authentication using:

  • AuthService for state management
  • Router for navigation
  • Guards for route protection
  • HTTP Interceptor for handling API authentication

By combining these, you can build a scalable and production-ready authentication system.