Skip to content

Instantly share code, notes, and snippets.

@samuelfarkas
Last active September 6, 2022 03:59
Show Gist options
  • Save samuelfarkas/5ac59ef69ccad8f33ebff8aaf8637f1c to your computer and use it in GitHub Desktop.
Save samuelfarkas/5ac59ef69ccad8f33ebff8aaf8637f1c to your computer and use it in GitHub Desktop.
Express.js Signed URLs generator
import { createHmac, timingSafeEqual } from 'crypto';
import { addMinutes, isAfter } from 'date-fns';
import { Request } from 'express';
/*
* Generate and validate Signed Urls
* with 'express' and 'date-fns' as dependency.
* */
export default class SignedUrl {
protected url: URL;
/**
* Create a Signed Url
* @param {!string} routeName - The route for URL (e.g 'upload' or 'upload/images')
* @param {number=} [expirationInMinutes] - link expiration
* */
constructor(
private readonly routeName: string,
private readonly expirationInMinutes?: number,
) {
this.url = new URL(process.env.API_URL + routeName);
SignedUrl.sign(this.url, routeName, expirationInMinutes);
}
/**
* Returns full signed URL with API_URL
*
* @returns string;
* */
get signed(): string {
return this.url.href;
}
/**
* Compares signature from Express requests
* @param {Request} request - Express request
*
* @returns boolean;
* */
public static compareSignature(request: Request): boolean {
return (
SignedUrl.hasCorrectSignature(request) && SignedUrl.hasNotExpired(request)
);
}
/**
* Validates signature from URL.
* @param {Request} request - Express request
* @returns boolean;
* */
protected static hasCorrectSignature(request: Request): boolean {
const signature = request.query.signature as string;
const date = request.query.expires as string;
const compare = createHmac('sha256', process.env.API_SECRET)
.update([request.path.replace(/^\/|\/$/g, ''), date].join(';'))
.digest('hex');
return timingSafeEqual(Buffer.from(signature), Buffer.from(compare));
}
/**
* Validates expiration time from URL.
* @param {Request} request - Express request
* @returns boolean;
* */
protected static hasNotExpired(request: Request): boolean {
const expires = request.query.expires;
if (expires) {
return isAfter(Number(expires), new Date());
}
return true;
}
/**
* Signs Url
*
* @param {URL} url - URL object
* @param {!string} route - The route for URL (e.g 'upload' or 'upload/images')
* @param {number=} [expiration] - link expiration
*
* @returns void
* */
protected static sign(url: URL, route: string, expiration?: number): void {
const args = [route];
if (expiration && expiration > 0) {
args.push(addMinutes(new Date(), expiration).getTime().toString());
url.searchParams.set('expires', args[1]);
}
const hash = createHmac('sha256', process.env.API_SECRET)
.update(args.join(';'))
.digest('hex');
url.searchParams.set('signature', hash);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment