Sunday, August 18, 2019

Creating a custom HttpInterceptor to handle 'withCredentials' in Angular 6

Been back at doing some Angular stuff after a long hiatus and I'm writing up a few issues that I ran into while updating some older projects over the last couple of days. I'm writing down the resolutions for my own future reference in a few short posts.
For this post, I needed to create and hook up a custom HttpInterceptor in Angular 6. There's lots of information from previous versions of Angular, but with the new HTTP subsystem in Angular 6, things changed once again so things work a little bit differently and that was one of the things that broke authentication in my application.

Use Case

In my use case I have a simple SPA application that relies on server side Cookie authentication. Basically the application calls a server side login screen which authenticates the user and sets a standard HTTP cookie. That cookie is passed down to the client and should be pushed back up to the server with each request.

WithCredentials - No Cookies for You!

This used to just work, but with added security functionality in newer browsers plus various frameworks clamping down on their security settings, XHR requests in Angular by default do not pass cookie information with each request. What this means is by default Angular doesn't pass Cookies captured on previous requests back to the server which effectively logs out the user.
In order for that to work the HttpClient has to set the withCredentials option.
return this.httpClient.get<Album[]>(this.config.urls.url("albums"),{ withCredentials: true })
                    .pipe(
                        map(albumList => this.albumList = albumList),
                        catchError( new ErrorInfo().parseObservableResponseError)
                    );
It's simple enough to do, but... it's a bit messy and more importantly, it's easy to forget to add the header explicitly. And once you forget it in one place the cookie isn't passed, and subsequent requests then don't get it back. In most application that use authentication this way - or even when using bearer tokens - you need to essentially pass the cookie or token on every request and adding it to each and every HTTP request is not very maintainable.

 CORS - Allow-Origin-With-Credentials

In addition to the client side withCredentials header, if you are going cross domain also make sure that the Allow-Origin-With-Credentials header is set on the server. If this header is not set the client side withCredentials also has no effect on cross-domain calls causing cookies and auth headers to not be sent.

HttpInterceptor to intercept every Requests

To help with this problem, Angular has the concept of an HttpInterceptor that you can register and that can then intercept every request and inject custom headers or tokens and other request information.
There are two things that need to be done:
  • Create the HttpInterceptor class
  • Hook it up in the AppModule as a Provider configuration

Creating an HttpInterceptor

Creating the Interceptor involves subclassing the HttpInterceptor class so I create a custom class HttpRequestInterceptor.ts:
import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';

import { Observable } from 'rxjs';

/** Inject With Credentials into the request */
@Injectable()
export class HttpRequestInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    
      // console.log("interceptor: " + req.url);
      req = req.clone({
        withCredentials: true
      });
      
      return next.handle(req);
  }
}
This is some nasty code if you had to remember it from scratch, but luckily most of this boilerplate code comes from the Angular docs. What we want here is to the set the request's withCredentials property, but that property happens to be read-only so you can't change it directly. Instead you have to explicitly clone the request object and explicitly apply the withCredentials property in the clone operation.
Nasty - all of that, but it works.

Hooking up the Interceptor

To hook up the interceptor open up app.module.ts and assign the interceptor to the providers section.
Make sure to import the HTTP_INTERCEPTORS at the top:
import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http';   // use this
and then add the interceptor(s) to the providers section:
providers: [            
    // Http Interceptor(s) -  adds with Client Credentials
    [
        { provide: HTTP_INTERCEPTORS, useClass: HttpRequestInterceptor, multi: true }
    ],
],

No comments:

Post a Comment

How to register multiple implementations of the same interface in Asp.Net Core?

 Problem: I have services that are derived from the same interface. public interface IService { } public class ServiceA : IService { ...