Getting Authenticated Images in Angular

Getting authenticated images in Angular

Let's imagine the following situation: We are working on an application which handles contracts between clients and sales representatives from a particular company.

For example, an insurance company could require us to develop a secure application where damage reports can be uploaded, and then the insurance agent, who has access rights to deal with such reports, can check the uploaded photos. These uploaded photos can be photos of damaged cars where the license plate is, and photos of the car's registration papers. These photos can contain sensitive data.

So the application is almost ready. One of the last features is to display these uploaded photos to everybody who has access to that particular insurance report. The application is set up in a way that allows us to download the images from a specific endpoint: /api/reports/{reportId}/{imageId}. We have set up our layout, have generated URLs for all the images that we need to request, and put them into the img tags.

However, they don't show up, because when they are fetched, they never hit our HttpInterceptor that sets the Authorization header on the request.

Introducing @this-dot/ng-utils UseHttpImageSource pipe

We came up with the idea of using a pipe to solve the above problem. We decided to include it as the first element in a collection of useful utilities for Angular. We called it @this-dot/ng-utils.

How to solve the problem?

If we send our request using Angular's HttpClient, it will hit our HttpInterceptor, which will, in turn, attach the Authorization header to the request. To avoid putting this logic into our services and/or components, we are going to implement a pipe.

@Pipe({
  name: 'useHttpImgSrc',
  pure: false,
})
export class UseHttpImageSourcePipe implements PipeTransform {

  constructor(private httpClient: HttpClient,
              private domSanitizer: DomSanitizer,
              private cdr: ChangeDetectorRef) {
  }

  transform(imagePath: string): string | SafeUrl {
    // our logic will come here
    return imagePath;
  }

}

We immediately know that we are going to need the HttpClient for getting the image, the DomSanitizer, so we will be able to use the bypassSecurityTrustUrl method to allow the returned blob to be displayed.

We do know that the endpoints we call will return safe images, so that is why we trust the returned values. We will also need the ChangeDetectorRef because this process is async, and we are going to trigger change detection manually. The pipe itself is not pure, because the returned value will change eventually, so we need to get the value from the transform on every change detection cycle.

We also need to keep in mind that whenever the input value changes, a new request needs to be sent out. That is why we are going to use a BehaviorSubject as the base of our async subscription. Let's also use the ngOnDestroy lifecycle hook to tear down the subscription when the component that has our pipe instance destroys.

@Pipe({
  name: 'useHttpImgSrc',
  pure: false,
})
export class UseHttpImageSourcePipe implements PipeTransform, OnDestroy {
  private subscription = new Subscription();
  private transformValue = new BehaviorSubject<string>('');

  private latestValue!: string | SafeUrl;

  constructor(private httpClient: HttpClient,
              private domSanitizer: DomSanitizer,
              private cdr: ChangeDetectorRef) {
    // every pipe instance will set up their subscription
    this.setUpSubscription();
  }

  // ...

  transform(imagePath: string): string | SafeUrl {
    // we emit a new value
    this.transformValue.next(imagePath);

    // we always return the latest value
    return this.latestValue;
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

  private setUpSubscription(): void {
    const transformSubscription = this.transformValue
      .asObservable()
      .pipe(
        // we filter out empty strings and falsy values
        filter((v): v is string => !!v),
        // we don't emit if the input hasn't changed
        distinctUntilChanged(),
        // Our HttpClient logic will come here
        // 
        tap((imagePath: string | SafeUrl) => {
        // we set the latestValue property of the pipe
        this.latestValue = imagePath;
        // and we mark the DOM changed, so the pipe's transform method is called again
        this.cdr.markForCheck();
      })
      )
      .subscribe();
    this.subscription.add(transformSubscription);
  }
}

Let's walk through what happens. When the pipe is initialised, it sets up the subscription to our transformValue, BehaviorSubject. This subscription will only be initialised once per pipe instance, and we unsubscribe from it in the ngOnDestroy() lifecycle hook.

The logic will happen mainly inside our setUpSubscription() method, where we filter out falsy values. We use the distinctUntilChanged() operator to check if the current transformValue is different from the previous one. If it is, it emits the new value. And finally, the transform method returns the latestValue, which is stored on the pipe.

We are going to implement our HttpClient logic there. Right now it just sets the latestValue property and triggers change detection. Let's get our images using the HttpClient.

@Pipe({
  name: 'useHttpImgSrc',
  pure: false,
})
export class UseHttpImageSourcePipe implements PipeTransform, OnDestroy {
  // ...
  private setUpSubscription(): void {
    const transformSubscription = this.transformValue
      .asObservable()
      .pipe(
        filter((v): v is string => !!v),
        distinctUntilChanged(),
        // we use switchMap, so the previous subscription gets torn down 
        switchMap((imagePath: string) => this.httpClient
          // we get the imagePath, observing the response and getting it as a 'blob'
          .get(imagePath, { observe: 'response', responseType: 'blob' })
          .pipe(
            // we map our blob into an ObjectURL
            map((response: HttpResponse<Blob>) => URL.createObjectURL(response.body)),
            // we bypass Angular's security mechanisms
            map((unsafeBlobUrl: string) => this.domSanitizer.bypassSecurityTrustUrl(unsafeBlobUrl)),
            // we trigger it only when there is a change in the result
            filter((blobUrl) => blobUrl !== this.latestValue),
          )
        ),
        tap((imagePath: string | SafeUrl) => {
          this.latestValue = imagePath;
          this.cdr.markForCheck();
        })
      )
      .subscribe();
    this.subscription.add(transformSubscription);
  }
}

We subscribe to our httpClient.get() method inside a switchMap operator, so we unsubscribe from the previous subscription if the image path passed into our transform method changes. We set up our get request to return a blob which we then convert into an ObjectURL, and we bypass the safety mechanisms of Angular. When the change detection cycle is triggered, this sanitised blob will be returned as the latestValue, and the image gets displayed. And when we check the network tab of the dev tools, we can see that our Authorization header is set on the image request.

Our pipe in our template:

  <img width="200px" [src]="'assets/images/success.png' | useHttpImgSrc" />

With authorization header

Some user experience improvements

Although our images now display on the page, we got a requirement to make them stateful. A loading image should be displayed before the actual image is loaded, and if the request has an error, another image should be displayed. Let's set up the loading image logic in our pipe, using an optional parameter in the transform() method.

@Pipe({
  name: 'useHttpImgSrc',
  pure: false,
})
export class UseHttpImageSourcePipe implements PipeTransform, OnDestroy {
  private subscription = new Subscription();
  private loadingImagePath!: string;
  private latestValue!: string | SafeUrl;
  private transformValue = new BehaviorSubject<string>('');

  // ...

  transform(
    imagePath: string,
    loadingImagePath?: string
  ): string | SafeUrl {
    this.setLoadingImagePath(loadingImagePath);
    // ...

    this.transformValue.next(imagePath);
    // return the loading image while there is no value present
    return this.latestValue || this.loadingImagePath;
  }

  // ...

  private setLoadingImagePath(
    loadingImagePath?: string
  ): void {
    // if it is already set we do nothing
    if (this.loadingImagePath) {
      return;
    }
    this.loadingImagePath = loadingImagePath;
  }

}

The latestValue property is only falsy before the actual image arrives. We created the setLoadingImagePath() method, which if provided, sets up a path for the loading placeholder image. Let's set up our template for two images, one which would surely fail, the other one will be loaded, but both of them will display the loading screen.

  <img width="200px" [src]="'assets/images/success.png' | useHttpImgSrc:'assets/images/loading.png'" />
  // ...
  <img width="200px" [src]="'assets/images/notfound.png' | useHttpImgSrc:'assets/images/loading.png'" />

Images are loading

Let's make the error image working as well. We are going to pass it as a second optional parameter. Let's update our template first.

  <img width="200px" [src]="'assets/images/success.png' | useHttpImgSrc:'assets/images/loading.png':'assets/images/error.png'" />
  // ...
  <img width="200px" [src]="'assets/images/notfound.png' | useHttpImgSrc:'assets/images/loading.png':'assets/images/error.png'" />

Let's update our pipe's implementations as well.

@Pipe({
  name: 'useHttpImgSrc',
  pure: false,
})
export class UseHttpImageSourcePipe implements PipeTransform, OnDestroy {
  private subscription = new Subscription();
  private loadingImagePath!: string;
  private errorImagePath!: string;
  private latestValue!: string | SafeUrl;
  private transformValue = new BehaviorSubject<string>('');

  // ...

  transform(
    imagePath: string,
    loadingImagePath?: string,
    errorImagePath?: string
  ): string | SafeUrl {
    this.setLoadingAndErrorImagePaths(loadingImagePath, errorImagePath);
    if (!imagePath) {
      return this.errorImagePath;
    }

    this.transformValue.next(imagePath);
    return this.latestValue || this.loadingImagePath;
  }

  // ...

  private setUpSubscription(): void {
    const transformSubscription = this.transformValue
      .asObservable()
      .pipe(
        filter((v): v is string => !!v),
        switchMap((imagePath: string) => this.httpClient
          .get(imagePath, { observe: 'response', responseType: 'blob' })
          .pipe(
            map((response: HttpResponse<Blob>) => URL.createObjectURL(response.body)),
            map((unsafeBlobUrl: string) => this.domSanitizer.bypassSecurityTrustUrl(unsafeBlobUrl)),
            filter((blobUrl) => blobUrl !== this.latestValue),
            // if the request errors out we return the error image's path value
            catchError(() => of(this.errorImagePath))
          )
        ),
        tap((imagePath: string | SafeUrl) => {
          this.latestValue = imagePath;
          this.cdr.markForCheck();
        })
      )
      .subscribe();
    this.subscription.add(transformSubscription);
  }

  // ...

  private setLoadingAndErrorImagePaths(
    loadingImagePath: string,
    errorImagePath: string
  ): void {
    if (this.loadingImagePath && this.errorImagePath) {
      return;
    }
    this.loadingImagePath = loadingImagePath;
    this.errorImagePath = errorImagePath;
  }

}

Finally, when the image is not loaded, our error image will be displayed.

When the image loads and an error happens

But what about developer experience?

Adding the loading and error image paths becomes a tedious job when you have more than one place where you need to set those up. We would like to keep that functionality if we ever need to override default values on another page. Let's set up our pipe's container module in a way that we can set default values in the app's root module.

First, we create the injectors:

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

export const THIS_DOT_LOADING_IMAGE_PATH = new InjectionToken<string>('THIS_DOT_LOADING_IMAGE_PATH');
export const THIS_DOT_ERROR_IMAGE_PATH = new InjectionToken<string>('THIS_DOT_ERROR_IMAGE_PATH');

Then, we create our forRoot method:

@NgModule({
  imports: [CommonModule],
  declarations: [UseHttpImageSourcePipe],
  exports: [UseHttpImageSourcePipe],
})
export class UseHttpImageSourcePipeModule {
  static forRoot(
    config: { loadingImagePath?: string; errorImagePath?: string } = {}
  ): ModuleWithProviders<UseHttpImageSourcePipeModule> {
    return {
      ngModule: UseHttpImageSourcePipeModule,
      providers: [
        // set up the providers
        {
          provide: THIS_DOT_LOADING_IMAGE_PATH,
          useValue: config.loadingImagePath || null,
        },
        {
          provide: THIS_DOT_ERROR_IMAGE_PATH,
          useValue: config.errorImagePath || null,
        },
      ],
    };
  }
}

And in our app.module.ts file:

@NgModule({
  // ...
  imports: [
    BrowserModule,
    HttpClientModule,
    UseHttpImageSourcePipeModule.forRoot({
      loadingImagePath: 'assets/images/loading.png',
      errorImagePath: 'assets/images/error.png',
    }),
    // ...
  ],
  // ...
})
export class AppModule {}

With this setup, we can inject the THIS_DOT_LOADING_IMAGE_PATH and the THIS_DOT_ERROR_IMAGE_PATH in our pipe, and update the logic so it defaults to that.

@Pipe({
  name: 'useHttpImgSrc',
  pure: false
})
export class UseHttpImageSourcePipe implements PipeTransform, OnDestroy {
  // ...
  constructor(
    private httpClient: HttpClient,
    private domSanitizer: DomSanitizer,
    private cdr: ChangeDetectorRef,
    @Inject(THIS_DOT_LOADING_IMAGE_PATH) private defaultLoadingImagePath: string,
    @Inject(THIS_DOT_ERROR_IMAGE_PATH) private defaultErrorImagePath: string
  ) {
  }

  transform(
    imagePath: string,
    loadingImagePath?: string,
    errorImagePath?: string
  ): string | SafeUrl {
    this.setLoadingAndErrorImagePaths(loadingImagePath, errorImagePath);
    // ...
  }

  // ...

  private setLoadingAndErrorImagePaths(
    loadingImagePath: string = this.defaultLoadingImagePath,
    errorImagePath: string = this.defaultErrorImagePath
  ): void {
    if (this.loadingImagePath && this.errorImagePath) {
      return;
    }
    this.loadingImagePath = loadingImagePath;
    this.errorImagePath = errorImagePath;
  }
}

With that, we can update our templates.

  <img width="200px" [src]="'assets/images/success.png' | useHttpImgSrc" />
  // ...
  <img width="200px" [src]="'assets/images/notfound.png' | useHttpImgSrc" />

And everything works as before. We can still override the loading and error images by passing properties to the pipe, but with Angular's dependency injection, we made it simpler to use default values. Using the injection tokens we can also override the values on component level if we ever need that.


This Dot Labs is a development consultancy focused on providing staff augmentation, architectural guidance, and consulting to companies.

We help implement and teach modern web best practices with technologies such as React, Angular, Vue, Web Components, GraphQL, Node, and more.

You might also like