All files / app/http auth.interceptor.ts

89.09% Statements 49/55
89.74% Branches 35/39
90% Functions 9/10
89.13% Lines 41/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 922x         2x           2x     2x   6x 6x 6x       6x 6x       11x 11x 5x       11x 11x 9x 9x 6x   9x 3x   9x       11x 22x 11x 11x               11x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x   6x                 6x       6x 6x 6x 6x            
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponseBase } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { from, Observable, switchMap, tap } from 'rxjs';
import { ConfigService } from '../service/config.service';
import { Store } from '../store/store';
import { base64Bytes, signJwt } from '../util/jwt';
 
/**
 * The jasper-app electron window sends the per-launch JWT secret in this
 * header. The client nginx echoes it back in the response.
 */
export const JASPER_KEY_HEADER = 'X-Jasper-Key';
 
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
 
  private secret = '';
  private tokenSecret = '';
  private tokenUserTag = '';
  private minting?: Promise<string>;
 
  constructor(
    private config: ConfigService,
    private store: Store,
  ) {}
 
  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    const minting = this.updateToken();
    if (minting) return from(minting).pipe(switchMap(() => this.handle(request, next)));
    return this.handle(request, next);
  }
 
  private handle(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    const userTag = this.secret ? '' : this.store.local.selectedUserTag;
    if (!this.config.token && !userTag) return this.readSecret(next.handle(request));
    let headers = request.headers;
    if (this.config.token) {
      headers = headers.set('Authorization', 'Bearer ' + this.config.token);
    }
    if (userTag) {
      headers = headers.set('User-Tag', userTag);
    }
    return this.readSecret(next.handle(request.clone({ headers })));
  }
 
  private readSecret(events: Observable<HttpEvent<unknown>>) {
    return events.pipe(tap(event => {
      if (!(event instanceof HttpResponseBase)) return;
      const secret = event.headers.get(JASPER_KEY_HEADER);
      if (secret) this.secret = secret;
    }));
  }
 
  /**
   * Create a new token when the secret or user tag changes.
   */
  private updateToken() {
    if (!this.secret) return undefined;
    const secret = this.secret;
    const userTag = this.store.local.selectedUserTag;
    Iif (secret === this.tokenSecret && userTag === this.tokenUserTag) return this.minting;
    this.tokenSecret = secret;
    this.tokenUserTag = userTag;
    this.config.token = '';
    const minting = this.minting = this.getToken(secret, userTag).then(jwt => {
      Eif (this.minting === minting) {
        this.config.token = jwt;
        this.minting = undefined;
      }
      return jwt;
    }).catch(err => {
      console.error('Could not create token', err);
      if (this.minting === minting) {
        this.tokenSecret = '';
        this.minting = undefined;
      }
      return '';
    });
    return minting;
  }
 
  private async getToken(secret: string, userTag: string) {
    const tag = userTag || '+user';
    const roles = tag === '+user' || tag === '_user' ? ['ROLE_ADMIN'] : ['ROLE_ANONYMOUS'];
    if (tag.startsWith('_')) roles.push('ROLE_PRIVATE');
    return signJwt({
      sub: tag,
      auth: roles.join(','),
    }, base64Bytes(secret));
  }
}