Hi folks! Long time no see!
In this short article, I’ll show you just how simple Cookie Authentication can be in Angular, all in five easy steps!
Personally, I find Cookies to be the ideal choice for most applications. They keep the front-end clean and transparent, while leaving the heavy lifting where it belongs: on the server. That said, this isn’t always the best fit for every scenario. Depending on your needs, you might opt for a hybrid setup, a fully token-based approach (like JWTs), or even OAuth 2.0 with OIDC.
If you want to dive deeper, my short e-book covers everything in detail, complete with Angular examples and mock back-ends for hands-on learning. Feel free to check it out!
AccademiaDev: text-based web development courses!
I believe in providing concise, valuable content without the unnecessary length and filler found in traditional books. Drawing from my years as a consultant and trainer, these resources—designed as interactive, online courses—deliver practical insights through text, code snippets, and quizzes, providing an efficient and engaging learning experience.
Before we begin: the server
I've already created a back-end for educational purposes: you'll find it here.
- Download the code (or clone the repo)
 - Open a terminal
 - Position yourself inside the project with 
cd - Install the dependencies with 
npm install - Run the back-end with 
npm run dev 
If all goes well, by visiting http://localhost:3000 on your browser you should see a message. We're good to go!
The server assumes that your front-end will be on
localhost:4200, if it's not the case, create an.envfile inside the project and setFRONTEND_URLto the full URL of your front-end. This is for CORS purposes.
Step 1: Getting the CSRF Token
A CSRF Token is a special value generated by the server (as a cookie) and stored on the browser which prevents any kind of CSRF vulnerability (eg. cross-origin same-site requests, login CSRF). It must be sent to the server (as a header) with each mutating request: this will be done automatically by Angular.
As soon as the app starts, we must get the CSRF token from the server. Let's use provideAppInitializer for that.
import { PLATFORM_ID } from '@angular/core';
import { isPlatformServer } from '@angular/common'; 
// ...
bootstrapApplication(App, {
  providers: [
    provideAppInitializer(() => {
      const http = inject(HttpClient);
      const platformId = inject(PLATFORM_ID);
        // Skip on the server (when using SSR), otherwise the request will be
        // cached and not repeated on the client-side, thus not grabbing the token.
        if (isPlatformServer(platformId)) return Promise.resolve();
        return firstValueFrom(http.get(`${env.apiUrl}/csrf-token`)
    }),
    provideHttpClient(),
  ],
});
Angular automatically saves this token and sends it with every request: as long as the Cookie name is XSRF-TOKEN, Angular will save it, and it will send it as a Header with the name X-XSRF-TOKEN for every request.
Should your server use different names, you can override them:
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withXsrfConfiguration({
        cookieName: 'YOUR-COOKIE-NAME',
        headerName: 'YOUR-HEADER-NAME',
      }),
    ),
  ]
};
Warning: Angular doesn't send this to absolute URLs. For this reason, when working on
localhost, I suggest using an URL like this://localhost:3000. This bypasses Angular's rules, likely due to a bug, but it's useful for development.
  
  
  Step 2: Sending the Cookie in localhost
The beautiful thing about cookies is that they are automatically sent to the server, but only to the server which created them, and only if it's on the same domain. However, during development, our server will be on a different domain (due to having a different port). For this, let's just create a simple interceptor to send them manually:
export const apiInterceptor: HttpInterceptorFn = (
  req: HttpRequest<any>,
  next: HttpHandlerFn
) => {
  // Include the `apiUrl` in your environment, like `//localhost:3000`
  if (req.url.includes(environment.apiUrl)) {
    req = req.clone({ withCredentials: true });
  }
  return next(req);
}
And provide it:
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([apiInterceptor])
    ),
    // ...
  ]
};
Step 3: Creating a service
Create a service to communicate with the server and store the user's data.
// For the sake of the exercise
type User = any;
@Injectable({ providedIn: 'root' })
export class AuthService {
  http = inject(HttpClient);
  router = inject(Router);
  user = signal<User | null>(null);
}
We need fetchUser method to fetch the user's data:
  /**
   * Gets the User's info from server and populates the state.
   * This is _the_ way to check if the user is still logged in.
   * If we already have it, skip the call.
   */
  fetchUser(forceReload = false): Observable<User> {
    const user = this.user();
    if (!!user && !forceReload) return of(user);
    return this.http.get<any>(`${env.apiUrl}/me`, {}).pipe(
      tap(u => this.user.set(u))
    );
  }
Then we need register and a login methods:
  register(credentials: {
    email: string,
    password: string,
    name: string,
    surname: string
  }) {
    return this.http.post<boolean>(`${env.apiUrl}/register`, credentials);
  }
  login(email: string, password: string) {
    return this.http.post<any>(`${env.apiUrl}/login`, { email, password }).pipe(
      switchMap(() => this.fetchUser()),
    );
  }
Finally, a logout method:
  logout() {
    this.http.get<any>(`${env.apiUrl}/logout`).subscribe(() => {
      this.user.set(null);
      this.router.navigateByUrl('/login');
    });
  }
Believe it or not, we're almost done!
Step 4: Detect an expired cookie
As soon as we get a 401 request, it means that we're no longer authenticated and we should be redirected to the login page. Let's create another interceptor. This is just an example, feel free to tweak it!
export const authInterceptor: HttpInterceptorFn = (
  req: HttpRequest<any>,
  next: HttpHandlerFn
) => {
  const authService = inject(AuthService);
  return next(req).pipe(
    tap({
      error: error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          // Clears the cookies and redirects to login
          authService.logout();
        }
      }
    })
  );
}
And provide it:
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([apiInterceptor, authInterceptor])
    ),
    // ...
  ]
};
Step 5: Guard your pages!
Let's create a guard to check if the user is authenticated, and if not, redirect to the login page:
export const authGuard = () => {
  const authService = inject(AuthService);
  const router = inject(Router);
  return authService.fetchUser().pipe(
    map(() => true),
    catchError(() => {
      router.navigateByUrl('/login')
      return [false];
    });
  )
}
And apply it to the pages you want to protect, just make sure not to use it on the login and register pages!
export const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    loadChildren: () => import('./pages/home/home.routes'),
    canMatch: [authGuard]
  },
  // ...
]
Use it!
We're done! Just create some pages to accomodate a login and a register form, and show the user's info!
You must register like this:
this.authService.register(credentials).subscribe(() => {
  this.router.navigateByUrl('/login');
})
You login like this:
this.authService.login(email, password).subscribe(() => {
  this.router.navigateByUrl('/');
})
Bonus: SSR Quirks
If you don't use SSR (server-side rendering), you can stop here. But if you do, there are some quirks to be aware of.
We already made sure to skip the CSRF Token call on the server, that is because if we did make that request, Angular would cache it and wouldn't repeat it on the client: we don't want that.
There's one more thing we must take care of with SSR: forwarding the Cookies.
In a normal app with client-side rendering, all API requests start from the user's browser where the cookies are stored: as a result, they're always sent automatically by the browser.
With SSR though, requests could start from the server, without the user's cookies. This means that each request which requires authentication (eg. /me to get the user profile) will fail on the server. As a result, the server will render the page as if you were not authenticated (eg. showing login buttons, redirecting to the login page if there's an auth guard…), then the client-side app will take over and modify the HTML: that's a terrible user experience.
Angular does not forward the cookies for us. In order to fix this, we must grab the user's cookies from the initial NodeJS Express Request and attach them to every request manually.
First, you need to provide the Express Request object to Angular (and while we're at it, let's pass the Response, too). In order to do so, create some InjectionTokens and provide them when the app is rendered.
Put these in a file, let's call it express-tokens.ts:
import { InjectionToken } from "@angular/core";
import { Request, Response } from "express";
export const REQUEST = new InjectionToken<Request>('Express REQUEST');
export const RESPONSE = new InjectionToken<Response>('Express RESPONSE');
Provide them in server.ts:
    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
        providers: [
          { provide: APP_BASE_HREF, useValue: baseUrl },
          // Add these lines
          { provide: REQUEST, useValue: req },
          { provide: RESPONSE, useValue: res }
        ],
      })
      .then((html) => res.send(html))
      .catch((err) => next(err));
Then, use this interceptor to retrieve the cookies from the Request object and add them as headers:
import { isPlatformServer } from '@angular/common';
import { HttpHandlerFn, HttpHeaders, HttpRequest } from '@angular/common/http';
import { PLATFORM_ID, inject } from '@angular/core';
import { REQUEST } from '../../express-tokens';
export function cookieInterceptor(
  req: HttpRequest<unknown>,
  next: HttpHandlerFn
) {
  const location = inject(PLATFORM_ID);
  const serverRequest = inject(REQUEST, { optional: true });
  if (isPlatformServer(location)) {
    let headers = new HttpHeaders();
    const cookies = serverRequest?.headers.cookie;
    headers = headers.set('cookie', cookies ?? '');
    const cookiedRequest = req.clone({
      headers,
    });
    return next(cookiedRequest);
  }
  return next(req);
}
You'll get an error though, something among the lines of "Refused to set unsafe header 'cookie'": that's because using "cookie" as a header name is forbidden by the spec. In order for this to work, we have to disable this error with a little hack inside server.ts:
// @ts-ignore
import * as xhr2 from 'xhr2';
// HACK - enables setting cookie header
xhr2.prototype._restrictedHeaders.cookie = false;
Done!
If you're providing the HttpClient with the
withFetch()configuration, this unfortunately won't work. Follow this open issue for more info.


    
Top comments (0)