Skip to main content
The Angular 18 Archetype implements a modern authentication system using Angular signals for reactive state management. The system includes type-safe user models, route guards, HTTP interceptors, and automatic token management.

Authentication architecture

1

User logs in

Authentication credentials are sent to the backend API
2

Token received

Backend returns a UserAccessToken containing user data and access token
3

State updated

AuthStore saves the token to localStorage and updates signal state
4

Token attached

authInterceptor automatically adds the Bearer token to all HTTP requests
5

Routes protected

authGuard prevents unauthorized access to protected routes

Type definitions

The authentication system uses strongly-typed models:

User type

// src/app/shared/domain/user.type.ts
export type User = {
  id: number;
  username: string;
  email: string;
  terms: boolean;
};

export const NULL_USER: User = {
  id: 0,
  username: '',
  email: '',
  terms: false,
};

UserAccessToken type

// src/app/shared/domain/userAccessToken.type.ts
import { NULL_USER, User } from './user.type';

export type UserAccessToken = {
  user: User;
  accessToken: string;
};

export const NULL_USER_ACCESS_TOKEN: UserAccessToken = {
  user: NULL_USER,
  accessToken: '',
};
The null object pattern (NULL_USER_ACCESS_TOKEN) provides a safe default state and eliminates null checks throughout the application.

AuthStore: Signal-based state management

The AuthStore service manages authentication state using Angular signals:
// src/app/shared/services/state/auth.store.ts
import { Injectable, Signal, WritableSignal, computed, inject, signal } from '@angular/core';
import { User } from '@domain/user.type';
import { NULL_USER_ACCESS_TOKEN, UserAccessToken } from '@domain/userAccessToken.type';
import { LocalRepository } from '@services/utils/local.repository';

@Injectable({
  providedIn: 'root',
})
export class AuthStore {
  #localRepository = inject(LocalRepository);

  // Private writable signal with localStorage persistence
  #state: WritableSignal<UserAccessToken> = signal<UserAccessToken>(
    this.#localRepository.load('userAccessToken', NULL_USER_ACCESS_TOKEN),
  );

  // Public computed signals
  userId: Signal<number> = computed(() => this.#state().user.id);
  user: Signal<User> = computed(() => this.#state().user);
  accessToken: Signal<string> = computed(() => this.#state().accessToken);
  isAuthenticated: Signal<boolean> = computed(() => this.accessToken() !== '');
  isAnonymous: Signal<boolean> = computed(() => this.accessToken() === '');

  setState(userAccessToken: UserAccessToken): void {
    this.#state.set(userAccessToken);
    this.#localRepository.save('userAccessToken', userAccessToken);
  }
}

Available signals

Returns the current user’s ID. Automatically updates when authentication state changes.
const userId = authStore.userId(); // Access current value
Returns the complete user object with id, username, email, and terms acceptance.
const user = authStore.user();
console.log(user.email);
Returns the JWT access token string. Empty string when not authenticated.
const token = authStore.accessToken();
Returns true if user has a valid access token, false otherwise.
if (authStore.isAuthenticated()) {
  // User is logged in
}
Inverse of isAuthenticated. Returns true when no access token exists.
if (authStore.isAnonymous()) {
  // Show login prompt
}

Authentication guard

The authGuard protects routes from unauthorized access:
// src/app/core/providers/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { environment } from '@env/environment';
import { AuthStore } from '@services/state/auth.store';

export const authGuard: CanActivateFn = () => {
  if (environment.securityOpen) return true;
  const authStore = inject(AuthStore);
  if (authStore.isAuthenticated()) return true;
  const router = inject(Router);
  return router.createUrlTree(['/auth', 'login']);
};

How it works

  1. Development bypass: If environment.securityOpen is true, all routes are accessible
  2. Check authentication: Uses authStore.isAuthenticated() signal to check login status
  3. Allow or redirect: Returns true for authenticated users, or redirects to /auth/login

Using the guard

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './core/providers/auth.guard';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./pages/dashboard.component'),
    canActivate: [authGuard], // Protected route
  },
  {
    path: 'auth/login',
    loadComponent: () => import('./pages/login.component'),
    // Public route - no guard
  },
];

Authentication interceptor

The authInterceptor automatically adds authentication headers to HTTP requests and handles auth errors:
// src/app/core/providers/auth.interceptor.ts
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { NULL_USER_ACCESS_TOKEN } from '@domain/userAccessToken.type';
import { AuthStore } from '@services/state/auth.store';
import { NotificationsStore } from '@services/state/notifications.store';
import { catchError, throwError } from 'rxjs';

const AUTH_ERROR_CODE = 401;

export const authInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => {
  const authStore = inject(AuthStore);
  const notificationsStore = inject(NotificationsStore);
  const router = inject(Router);

  // Add Authorization header
  const accessToken = authStore.accessToken();
  const authorizationHeader = accessToken ? `Bearer ${accessToken}` : '';
  req = req.clone({
    setHeaders: {
      Authorization: authorizationHeader,
    },
  });

  // Handle errors
  return next(req).pipe(
    catchError((error) => {
      if (error.status === AUTH_ERROR_CODE) {
        authStore.setState(NULL_USER_ACCESS_TOKEN);
        router.navigate(['/auth', 'login']);
      }
      notificationsStore.addNotification({ message: error.message, type: 'error' });
      return throwError(() => error);
    }),
  );
};

Interceptor responsibilities

1

Attach Bearer token

Reads accessToken from AuthStore and adds Authorization: Bearer <token> header to all requests
2

Handle 401 errors

When server returns 401 Unauthorized:
  • Clears authentication state
  • Redirects to login page
3

Display error notifications

Adds error messages to NotificationsStore for user feedback

Registering the interceptor

// src/app/app.config.ts
import { ApplicationConfig, provideHttpClient, withInterceptors } from '@angular/core';
import { authInterceptor } from './core/providers/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
  ]
};

Authentication flow example

Here’s how to implement a login flow:
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { AuthStore } from '@services/state/auth.store';
import { UserAccessToken } from '@domain/userAccessToken.type';

@Injectable({ providedIn: 'root' })
export class AuthService {
  #http = inject(HttpClient);
  #authStore = inject(AuthStore);
  #router = inject(Router);

  login(username: string, password: string) {
    return this.#http.post<UserAccessToken>('/api/auth/login', { username, password })
      .subscribe({
        next: (token) => {
          // Save to store (automatically persists to localStorage)
          this.#authStore.setState(token);
          // Navigate to dashboard
          this.#router.navigate(['/dashboard']);
        },
        error: (err) => console.error('Login failed', err)
      });
  }

  logout() {
    // Clear authentication state
    this.#authStore.setState(NULL_USER_ACCESS_TOKEN);
    // Redirect to login
    this.#router.navigate(['/auth', 'login']);
  }
}

Using auth state in components

import { Component, inject } from '@angular/core';
import { AuthStore } from '@services/state/auth.store';

@Component({
  selector: 'app-profile',
  template: `
    @if (authStore.isAuthenticated()) {
      <h1>Welcome, {{ authStore.user().username }}!</h1>
      <p>Email: {{ authStore.user().email }}</p>
      <button (click)="logout()">Logout</button>
    } @else {
      <p>Please log in</p>
    }
  `
})
export class ProfileComponent {
  authStore = inject(AuthStore);

  logout() {
    // Implementation here
  }
}
Angular signals automatically trigger change detection when the authentication state updates, ensuring your UI stays synchronized.

Build docs developers (and LLMs) love