shootismoke/backend

View on GitHub
src/util/hawk.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
93%
import Hawk from '@hapi/hawk';
import { ApolloError } from 'apollo-server-micro';
import { Request } from 'express';

import { IS_PROD } from './constants';
import { logger } from './logger';

interface Credentials {
    algorithm: 'sha256';
    key: string;
}

/**
 * Mapping of id->key credentials. Note: for now, this mapping holds both
 * staging and production ids. For example, `shootismoke-default` should never
 * be a valid id for production.
 */
export const CREDENTIALS: Record<string, Credentials> = {
    'shootismoke-default': {
        algorithm: 'sha256',
        key: process.env.HAWK_KEY_1_5_0 as string,
    },
    'shootismoke-development': {
        algorithm: 'sha256',
        key: process.env.HAWK_KEY_1_5_0 as string,
    },
    'shootismoke-production-v1.5': {
        algorithm: 'sha256',
        /**
         * This is the Hawk key introduced in version 1.5.0.
         */
        key: process.env.HAWK_KEY_1_5_0 as string,
    },
};

// Credentials lookup function
function credentialsFunc(id: string): Credentials {
    if (id.startsWith('shootismoke-production-v')) {
        return CREDENTIALS['shootismoke-production-v1.5'];
    }

    if (!CREDENTIALS[id]) {
        const e = new Error(`Invalid Hawk id: ${id}`);
        logger.error(e);
        throw e;
    }

    return {
        algorithm: 'sha256',
        key: CREDENTIALS[id].key,
    };
}

/**
 * Successful result from `Hawk.server.authenticate` function.
 */
export interface HawkResult {
    credentials: Credentials;
}

/**
 * A function to test if the request is hawk-authenticated.
 * @see https://hapi.dev/family/hawk
 *
 * @param req - The incoming express request
 */
export async function hawk(req: Request): Promise<HawkResult> {
    try {
        // Authenticate incoming request
        const result: HawkResult = await Hawk.server.authenticate(
            req,
            credentialsFunc,
            {
                // The client constructs the port as 443, because in production we use
                // https. But somehow, in the `req`, object, the port is 80, even in
                // production. Here we just force the port to 443 in production.
                port: IS_PROD ? 443 : undefined,
            }
        );

        return result;
    } catch (error) {
        // Add the attributes of the error (e.g. offset in case of stale timestamp)
        // in the GraphQL error extensions
        throw new ApolloError(
            `Hawk: ${error.message as string}`,
            'UNAUTHENTICATED',
            error.output &&
                error.output.payload &&
                error.output.payload.attributes
        );
    }
}