Skip to content
← Back to Functions
Code

Signed Download Link Validator

Validates temporary signed download URLs and rejects tampering or expiry.

Function signature

ogValidateSignedDownloadLink(params = array(), options = array())

Categories

  • APIs and Webhooks

Parameters

paramsSigned link query parameters. Recognized keys: `expires`, `file_id`, `nonce`, `sig`, `user_id`.optionsSecret, current_user_id, and seen_nonce options. Recognized keys: `current_user_id`, `secret`, `seen_nonces`.

Return value

Short public-safe status message.

  • valid
  • file_id
  • user_id
  • expires_at
  • nonce

Compatibility

Existing function name and call order preserved; metadata signature corrected to source.

Minimum PHP version: 7.4

Security notes

Validate request method, identity, permissions, policy arrays, URLs, signatures, and caller-owned allowlists before use; keep secrets and internal paths out of public output.

Code

<?php

/*
 * Copyright (c) 2026 Jeffery L. Paris <jparis@phpog.com>.
 * Free for personal and internal use. Paid project use requires visible credit
 * to Jeffery L. Paris. Corporate use requires a paid license fee unless a
 * separate written license states otherwise.
 */

/**
 * Validates a temporary HMAC-signed download link.
 *
 * The helper checks required fields, expiration, optional user binding, nonce
 * replay lists, and constant-time signature comparison.
 *
 * @param array $params Signed link query parameters.
 * @param array $options Secret, current_user_id, and seen_nonce options.
 * @return array Signed link validation result.
 */
function ogValidateSignedDownloadLink($params = array(), $options = array()) {
	$result = array(
		'success' => false,
		'message' => '',
		'data' => array()
	);

	if (!is_array($params)) {
		$result['message'] = 'Signed link parameters must be an array.';
		return $result;
	}

	if (!is_array($options)) {
		$options = array();
	}

	$secret = '';
	if (!empty($options['secret'])) {
		$secret = (string)$options['secret'];
	}
	if (empty($secret)) {
		$result['message'] = 'Signing secret is required.';
		return $result;
	}

	$file_id = '';
	if (!empty($params['file_id'])) {
		$file_id = preg_replace('/[^a-zA-Z0-9_.-]/', '', (string)$params['file_id']);
	}
	$user_id = '';
	if (!empty($params['user_id'])) {
		$user_id = preg_replace('/[^a-zA-Z0-9_.-]/', '', (string)$params['user_id']);
	}
	$expires_at = 0;
	if (!empty($params['expires'])) {
		$expires_at = (int)$params['expires'];
	}
	$nonce = '';
	if (!empty($params['nonce'])) {
		$nonce = preg_replace('/[^a-zA-Z0-9_.-]/', '', (string)$params['nonce']);
	}
	$signature = '';
	if (!empty($params['sig'])) {
		$signature = trim((string)$params['sig']);
	}

	if (empty($file_id) || empty($user_id) || empty($expires_at) || empty($nonce) || empty($signature)) {
		$result['message'] = 'Signed link is missing required fields.';
		return $result;
	}

	if ($expires_at < time()) {
		$result['message'] = 'Signed link has expired.';
		return $result;
	}

	if (!empty($options['current_user_id'])) {
		$current_user_id = preg_replace('/[^a-zA-Z0-9_.-]/', '', (string)$options['current_user_id']);
		if (!hash_equals($current_user_id, $user_id)) {
			$result['message'] = 'Signed link user mismatch.';
			return $result;
		}
	}

	if (!empty($options['seen_nonces']) && is_array($options['seen_nonces'])) {
		if (in_array($nonce, $options['seen_nonces'], true)) {
			$result['message'] = 'Signed link nonce has already been used.';
			return $result;
		}
	}

	$base = $file_id . '|' . $user_id . '|' . $expires_at . '|' . $nonce;
	$expected = hash_hmac('sha256', $base, $secret);
	if (!hash_equals($expected, $signature)) {
		$result['message'] = 'Signed link signature mismatch.';
		return $result;
	}

	$result['success'] = true;
	$result['message'] = 'Signed download link validated.';
	$result['data'] = array(
		'valid' => true,
		'file_id' => $file_id,
		'user_id' => $user_id,
		'expires_at' => $expires_at,
		'nonce' => $nonce
	);

	return $result;
}