All files / src/auth-strategies hmac.auth.strategy.ts

23.81% Statements 10/42
0% Branches 0/15
0% Functions 0/6
20% Lines 8/40

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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 1091x 1x 1x   1x 1x   1x                                   1x     1x                                                                                                                                                                
import { AuthenticationBindings, AuthenticationMetadata, AuthenticationStrategy } from '@loopback/authentication';
import { inject } from '@loopback/core';
import { HttpErrors, RedirectRoute, Request, } from '@loopback/rest';
 
import { AuthenticationStrategyBindings, HmacAuthenticationStrategyOptions } from './keys';
import { securityId } from '@loopback/security';
 
export class AuthError extends HttpErrors.Unauthorized {
  constructor(message: string) {
    super(message);
    this.code = 'ERR_HMAC_AUTH_INVALID';
    Error.captureStackTrace(this, this.constructor);
  }
}
 
export interface HmacPrincipal extends RedirectRoute {
  keyId: string;
  nonce: string;
  created: number;
  signature: string;
  url: string;
  method: string;
  options: HmacAuthenticationStrategyOptions
}
 
export class HmacAuthenticationStrategy implements AuthenticationStrategy {
  name = 'hmac-auth';
  @inject(AuthenticationStrategyBindings.HMAC_DEFAULT_OPTIONS)
  options: HmacAuthenticationStrategyOptions;
 
  constructor(
    @inject(AuthenticationBindings.METADATA)
    private metadata: AuthenticationMetadata[],
  ) { }
 
  /**
   * Authenticate request
   * @param request The request
   */
  async authenticate(request: Request): Promise<HmacPrincipal> {
    return this.extractCredentials(request);
  }
 
  /**
   * Extract a token from the request
   * @param request The request
   */
  private extractCredentials(request: Request): HmacPrincipal {
    const { headers, url, method } = request;
    if (!headers.authorization) {
      throw new AuthError(`Authorization header not found.`);
    }
    const authHeaderValue = headers.authorization;
    if (!authHeaderValue.startsWith('Signature')) {
      throw new AuthError(`Authorization header is not of type 'Signature'.`);
    }
 
    const parts = authHeaderValue.split(' ');
    if (parts.length !== 2) {
      throw new AuthError(
        `Authorization header value has too many parts. It must follow the pattern: 'Signature keyId="xxx",nonce="yyy",created="zzz",signature="zzz"' is a valid HMAC signature.`,
      );
    }
 
    return this.extractCredentialsFromParts(parts[1], url, method);
  }
 
  private extractCredentialsFromParts(
    part: string,
    url: string,
    method: string,
  ): HmacPrincipal {
    const [keyIdValue, nonceValue, createdValue, signatureValue] = part.split(',');
    const keyIdMatched = this.extractValueFromPart(keyIdValue, 'keyId');
    const nonceMatched = this.extractValueFromPart(nonceValue, 'nonce');
    const createdMatched = this.extractValueFromPart(createdValue, 'created');
    const signatureMatched = this.extractValueFromPart(signatureValue, 'signature');
 
    if (isNaN(+createdMatched[1])) {
      throw new AuthError(`created was not a valid unix timestamp`);
    }
 
    const timeDiff = Math.floor(Date.now() / 1000) - Math.floor(+createdMatched[1] / 1000);
    if (timeDiff > this.options.maxInterval) {
      throw new AuthError(`The time difference between generated and requested time is too great`);
    }
    return {
      [securityId]: keyIdMatched[1],
      keyId: keyIdMatched[1],
      nonce: nonceMatched[1],
      created: +createdMatched[1],
      signature: signatureMatched[1],
      url,
      method,
      options: this.options
    } as unknown as HmacPrincipal;
  }
 
  private extractValueFromPart(part: string, name: string): RegExpMatchArray {
    const regex = new RegExp(`^${name}="(.{1,})"$`, 'g');
    const matched = regex.exec(part);
    if (!Array.isArray(matched) || matched.length !== 2 || typeof matched[1] !== 'string') {
      throw new AuthError(`${name} was not present in header`);
    }
 
    return matched;
  }
}