cyberangles guide

Handling User Authentication in Angular Applications

User authentication is a cornerstone of modern web applications, ensuring that only authorized users can access sensitive data and functionality. In Single-Page Applications (SPAs) built with Angular, authentication involves managing user credentials, securing routes, maintaining sessions, and protecting against common security threats. Angular provides a robust ecosystem of tools—including services, route guards, HTTP interceptors, and reactive forms—to implement secure and scalable authentication flows. This blog will guide you through every step of building a complete authentication system in Angular, from setting up the project to handling token expiration and following best practices. Whether you’re building a small app or a large enterprise solution, this guide will help you implement authentication that is both user-friendly and secure.

Table of Contents

  1. Prerequisites
  2. Setting Up the Angular Project
  3. Authentication Flow Overview
  4. Storing Authentication Tokens Securely
  5. Creating an Authentication Service
  6. Protecting Routes with Route Guards
  7. Building Login and Registration Components
  8. Handling Token Expiration and Refresh Tokens
  9. Managing Authentication State
  10. Security Best Practices
  11. Testing Authentication
  12. Troubleshooting Common Issues
  13. Conclusion
  14. References

Prerequisites

Before diving in, ensure you have the following:

  • Basic knowledge of Angular (components, services, routing, RxJS).
  • Angular CLI installed (npm install -g @angular/cli).
  • A backend API that supports authentication (e.g., Node.js/Express, Firebase, or a third-party service like Auth0). For this guide, we’ll assume a REST API with endpoints like /api/login, /api/register, and /api/refresh-token.
  • Node.js and npm/yarn installed.

Setting Up the Angular Project

Let’s start by creating a new Angular project and installing dependencies:

# Create a new Angular app  
ng new angular-auth-demo --routing --style=css  

# Navigate to the project  
cd angular-auth-demo  

# Install required dependencies (e.g., for reactive forms, HTTP client)  
# Angular already includes HttpClientModule and ReactiveFormsModule by default, but ensure they’re imported.  

Next, enable routing by adding AppRoutingModule to app.module.ts (Angular CLI does this automatically if you use --routing).

Authentication Flow Overview

A typical authentication flow in an Angular app follows these steps:

  1. User submits credentials (username/email and password) via a login form.
  2. Angular sends credentials to the backend API via an HTTP POST request.
  3. Backend validates credentials and returns a JSON Web Token (JWT) or session token, along with a refresh token (optional).
  4. Angular stores the token securely (e.g., in HttpOnly cookies or localStorage).
  5. Token is attached to subsequent requests (via HTTP interceptors) to authenticate the user.
  6. Route guards protect sensitive routes by checking if the user has a valid token.
  7. On logout, the token is cleared, and the user is redirected to the login page.
  8. Token expiration is handled by refreshing the token using a refresh token before it expires.

Storing Authentication Tokens Securely

Storing tokens securely is critical to prevent unauthorized access. Here are the most common methods:

1. HttpOnly Cookies

  • How it works: The backend sets the token in an HttpOnly cookie via the Set-Cookie header. Browsers automatically include cookies in subsequent requests to the same domain.
  • Pros: Protected against XSS attacks (JavaScript cannot access HttpOnly cookies).
  • Cons: Requires backend configuration; may complicate CORS setup; less flexible for SPAs with separate frontend/backend domains.

2. localStorage/sessionStorage

  • How it works: Tokens are stored in the browser’s localStorage (persists across sessions) or sessionStorage (cleared when the tab closes).
  • Pros: Easy to implement; accessible via JavaScript for attaching to requests.
  • Cons: Vulnerable to XSS attacks (malicious scripts can steal tokens).

3. In-Memory Storage

  • How it works: Tokens are stored in a service variable (e.g., a BehaviorSubject).
  • Pros: Not accessible via JavaScript (safer than localStorage).
  • Cons: Lost on page refresh; requires re-authentication after reload.

Recommendation: Use HttpOnly cookies for production to mitigate XSS risks. For simplicity in this guide, we’ll use localStorage, but we’ll highlight security caveats.

Creating an Authentication Service

The authentication service centralizes logic for login, logout, token management, and checking authentication status. Let’s create one:

ng generate service auth  

Update auth.service.ts with the following:

import { Injectable } from '@angular/core';  
import { HttpClient } from '@angular/common/http';  
import { BehaviorSubject, Observable, throwError } from 'rxjs';  
import { catchError, tap } from 'rxjs/operators';  
import { environment } from 'src/environments/environment';  
import { Router } from '@angular/router';  

interface AuthResponse {  
  accessToken: string;  
  refreshToken: string;  
  user: { id: string; email: string; name: string };  
}  

@Injectable({ providedIn: 'root' })  
export class AuthService {  
  private currentUserSubject: BehaviorSubject<AuthResponse['user'] | null>;  
  public currentUser$: Observable<AuthResponse['user'] | null>;  

  constructor(private http: HttpClient, private router: Router) {  
    // Initialize with stored user data (if any)  
    const storedUser = localStorage.getItem('currentUser');  
    this.currentUserSubject = new BehaviorSubject<AuthResponse['user'] | null>(  
      storedUser ? JSON.parse(storedUser) : null  
    );  
    this.currentUser$ = this.currentUserSubject.asObservable();  
  }  

  // Get the current user (synchronously)  
  public get currentUserValue(): AuthResponse['user'] | null {  
    return this.currentUserSubject.value;  
  }  

  // Login user  
  login(email: string, password: string): Observable<AuthResponse> {  
    return this.http.post<AuthResponse>(`${environment.apiUrl}/login`, { email, password })  
      .pipe(  
        tap(response => this.handleAuthentication(response)),  
        catchError(error => {  
          // Handle errors (e.g., invalid credentials)  
          return throwError(() => new Error(error.error.message || 'Login failed'));  
        })  
      );  
  }  

  // Register user  
  register(name: string, email: string, password: string): Observable<AuthResponse> {  
    return this.http.post<AuthResponse>(`${environment.apiUrl}/register`, { name, email, password })  
      .pipe(  
        tap(response => this.handleAuthentication(response)),  
        catchError(error => throwError(() => new Error(error.error.message || 'Registration failed')))  
      );  
  }  

  // Logout user  
  logout(): void {  
    // Clear tokens and user data  
    localStorage.removeItem('accessToken');  
    localStorage.removeItem('refreshToken');  
    localStorage.removeItem('currentUser');  
    this.currentUserSubject.next(null);  
    this.router.navigate(['/login']);  
  }  

  // Check if user is authenticated  
  isAuthenticated(): boolean {  
    const token = localStorage.getItem('accessToken');  
    return !!token && !this.isTokenExpired(token); // Implement isTokenExpired()  
  }  

  // Helper: Store tokens and user data  
  private handleAuthentication(authResponse: AuthResponse): void {  
    const { accessToken, refreshToken, user } = authResponse;  
    localStorage.setItem('accessToken', accessToken);  
    localStorage.setItem('refreshToken', refreshToken);  
    localStorage.setItem('currentUser', JSON.stringify(user));  
    this.currentUserSubject.next(user);  
  }  

  // Helper: Check if token is expired (simplified example)  
  private isTokenExpired(token: string): boolean {  
    try {  
      const decoded: any = JSON.parse(atob(token.split('.')[1]));  
      return decoded.exp * 1000 < Date.now(); // exp is in seconds  
    } catch (error) {  
      return true; // Treat invalid tokens as expired  
    }  
  }  

  // Get access token for HTTP requests  
  getAccessToken(): string | null {  
    return localStorage.getItem('accessToken');  
  }  

  // Get refresh token  
  getRefreshToken(): string | null {  
    return localStorage.getItem('refreshToken');  
  }  
}  

Protecting Routes with Route Guards

Angular’s route guards control access to routes based on conditions (e.g., authentication status). We’ll use CanActivate to protect routes like /dashboard or /profile.

Step 1: Create an Auth Guard

ng generate guard auth  

Choose CanActivate when prompted. Update auth.guard.ts:

import { Injectable } from '@angular/core';  
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';  
import { AuthService } from './auth.service';  

@Injectable({ providedIn: 'root' })  
export class AuthGuard implements CanActivate {  
  constructor(private authService: AuthService, private router: Router) {}  

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {  
    if (this.authService.isAuthenticated()) {  
      return true; // Allow access to the route  
    }  

    // Redirect to login if not authenticated  
    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });  
    return false;  
  }  
}  

Step 2: Apply the Guard to Routes

Update app-routing.module.ts to protect sensitive routes:

import { NgModule } from '@angular/core';  
import { RouterModule, Routes } from '@angular/router';  
import { LoginComponent } from './login/login.component';  
import { RegisterComponent } from './register/register.component';  
import { DashboardComponent } from './dashboard/dashboard.component';  
import { AuthGuard } from './auth.guard';  

const routes: Routes = [  
  { path: 'login', component: LoginComponent },  
  { path: 'register', component: RegisterComponent },  
  { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] },  
  { path: '', redirectTo: '/login', pathMatch: 'full' }  
];  

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }  

Building Login and Registration Components

Let’s create login and registration forms using Angular’s Reactive Forms for validation and state management.

Login Component

ng generate component login  

Update login.component.ts:

import { Component } from '@angular/core';  
import { FormBuilder, FormGroup, Validators } from '@angular/forms';  
import { AuthService } from '../auth.service';  
import { Router, ActivatedRoute } from '@angular/router';  

@Component({  
  selector: 'app-login',  
  templateUrl: './login.component.html',  
  styleUrls: ['./login.component.css']  
})  
export class LoginComponent {  
  loginForm: FormGroup;  
  errorMessage = '';  
  returnUrl: string;  

  constructor(  
    private fb: FormBuilder,  
    private authService: AuthService,  
    private router: Router,  
    private route: ActivatedRoute  
  ) {  
    // Initialize form with validators  
    this.loginForm = this.fb.group({  
      email: ['', [Validators.required, Validators.email]],  
      password: ['', [Validators.required, Validators.minLength(6)]]  
    });  

    // Get return URL from query params (e.g., /dashboard)  
    this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard';  
  }  

  // Submit login form  
  onSubmit(): void {  
    if (this.loginForm.invalid) return;  

    const { email, password } = this.loginForm.value;  
    this.authService.login(email, password).subscribe({  
      next: () => {  
        this.router.navigate([this.returnUrl]); // Redirect to return URL  
      },  
      error: (err) => {  
        this.errorMessage = err.message; // Show error to user  
      }  
    });  
  }  
}  

Update login.component.html:

<div class="login-container">  
  <h2>Login</h2>  
  <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">  
    <div class="form-group">  
      <label>Email</label>  
      <input type="email" formControlName="email" class="form-control">  
      <div *ngIf="loginForm.get('email')?.invalid && loginForm.get('email')?.touched">  
        <small *ngIf="loginForm.get('email')?.errors?.['required']">Email is required</small>  
        <small *ngIf="loginForm.get('email')?.errors?.['email']">Invalid email format</small>  
      </div>  
    </div>  

    <div class="form-group">  
      <label>Password</label>  
      <input type="password" formControlName="password" class="form-control">  
      <div *ngIf="loginForm.get('password')?.invalid && loginForm.get('password')?.touched">  
        <small *ngIf="loginForm.get('password')?.errors?.['required']">Password is required</small>  
        <small *ngIf="loginForm.get('password')?.errors?.['minLength']">Password must be at least 6 characters</small>  
      </div>  
    </div>  

    <button type="submit" [disabled]="loginForm.invalid" class="btn btn-primary">Login</button>  
    <div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>  
  </form>  
  <p>Don't have an account? <a routerLink="/register">Register</a></p>  
</div>  

Registration Component

Follow a similar pattern for the registration component, adding fields for name and confirming the password.

Handling Token Expiration and Refresh Tokens

JWT tokens have an expiration time (exp claim). To avoid forcing users to log in repeatedly, use a refresh token to fetch a new access token.

Step 1: Create an HTTP Interceptor

HTTP interceptors modify outgoing requests (e.g., add tokens) and incoming responses (e.g., handle 401 errors).

ng generate interceptor auth  

Update auth.interceptor.ts:

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

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

  constructor(private authService: AuthService) {}  

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {  
    const token = this.authService.getAccessToken();  
    if (token) {  
      request = this.addToken(request, token);  
    }  

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

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

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

      const refreshToken = this.authService.getRefreshToken();  
      if (refreshToken) {  
        return this.authService.refreshToken(refreshToken).pipe(  
          switchMap((token: any) => {  
            this.isRefreshing = false;  
            this.refreshTokenSubject.next(token.accessToken);  
            return next.handle(this.addToken(request, token.accessToken));  
          }),  
          catchError((err) => {  
            this.isRefreshing = false;  
            this.authService.logout(); // Logout on refresh token failure  
            return throwError(() => err);  
          })  
        );  
      }  
    }  

    // Wait for the refresh token to be available  
    return this.refreshTokenSubject.pipe(  
      filter(token => token !== null),  
      take(1),  
      switchMap(token => next.handle(this.addToken(request, token)))  
    );  
  }  
}  

Step 2: Add Refresh Token Logic to AuthService

Update auth.service.ts to include a refreshToken method:

// Add to AuthService  
refreshToken(refreshToken: string): Observable<{ accessToken: string }> {  
  return this.http.post<{ accessToken: string }>(`${environment.apiUrl}/refresh-token`, { refreshToken })  
    .pipe(  
      tap(response => {  
        localStorage.setItem('accessToken', response.accessToken);  
      })  
    );  
}  

Step 3: Register the Interceptor

Add AuthInterceptor to app.module.ts providers:

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

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

Managing Authentication State

For larger apps, use a state management library like NgRx. For smaller apps, a BehaviorSubject in AuthService (as shown earlier) suffices. The currentUser$ observable allows components to react to authentication state changes:

// In a component (e.g., header)  
this.authService.currentUser$.subscribe(user => {  
  this.isLoggedIn = !!user;  
  this.userName = user?.name;  
});  

Security Best Practices

  • Use HTTPS: Always transmit tokens over HTTPS to prevent interception.
  • Avoid localStorage for tokens: Prefer HttpOnly cookies to mitigate XSS risks.
  • Short-lived access tokens: Use short expiration times (e.g., 15 minutes) and refresh tokens for re-authentication.
  • CSRF protection: If using cookies, enable CSRF tokens (Angular’s HttpClient supports CSRF with withCredentials).
  • Input validation: Validate all user inputs on both client and server.
  • Sanitize inputs: Prevent injection attacks by sanitizing user-generated content.
  • Rate limiting: Implement rate limiting on login/register endpoints to prevent brute-force attacks.

Testing Authentication

Test auth services, guards, and interceptors using Angular’s testing utilities:

// Example: Testing AuthService login method  
import { TestBed } from '@angular/core/testing';  
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';  
import { AuthService } from './auth.service';  

describe('AuthService', () => {  
  let service: AuthService;  
  let httpMock: HttpTestingController;  

  beforeEach(() => {  
    TestBed.configureTestingModule({  
      imports: [HttpClientTestingModule],  
      providers: [AuthService]  
    });  
    service = TestBed.inject(AuthService);  
    httpMock = TestBed.inject(HttpTestingController);  
  });  

  it('should login and store tokens', () => {  
    const mockResponse = {  
      accessToken: 'test-token',  
      refreshToken: 'refresh-token',  
      user: { id: '1', name: 'Test User', email: '[email protected]' }  
    };  

    service.login('[email protected]', 'password123').subscribe(response => {  
      expect(response).toEqual(mockResponse);  
      expect(localStorage.getItem('accessToken')).toBe('test-token');  
    });  

    const req = httpMock.expectOne(`${environment.apiUrl}/login`);  
    expect(req.request.method).toBe('POST');  
    req.flush(mockResponse);  
  });  
});  

Troubleshooting Common Issues

  • CORS errors: Ensure the backend allows requests from your Angular app’s origin (set Access-Control-Allow-Origin).
  • Token not attached to requests: Verify the interceptor is registered in app.module.ts.
  • Guard not working: Check if AuthGuard is correctly added to the route’s canActivate array.
  • Refresh token loops: Ensure the backend returns a 401 only for invalid/expired tokens, not refresh tokens.

Conclusion

Implementing user authentication in Angular requires careful attention to security, token management, and user experience. By following this guide, you’ve learned how to:

  • Set up secure token storage and transmission.
  • Protect routes with guards.
  • Handle token expiration with refresh tokens.
  • Secure API requests with interceptors.
  • Follow best practices to mitigate common security threats.

Remember that authentication is a shared responsibility between frontend and backend—always validate credentials server-side and stay updated on security vulnerabilities.

References