Time-limited URL tokens

Make URLs expire after a configurable period.

VCL

Use this solution in your VCL service (click RUN below to test this solution or clone it to make changes):

Compute@Edge

Use this solution in your Compute@Edge service:

  1. Rust
#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
// Log initial setup
log_fastly::init_simple("my_log", log::LevelFilter::Info);
fastly::log::set_panic_endpoint("my_log")?;
// Extract expire time and signature from token
match verify_token(req) {
Ok(request) => {
// Token signature verification passed, send out the request to backend
Ok(request.send(BACKEND_NAME)?)
}
Err(e) => {
// Token signature verification failed
// Log the error message
log::info!("{}", e);
// Generate error response
Ok(Response::from_status(StatusCode::FORBIDDEN))
}
}
}
/// Verify the token of request object
fn verify_token(mut req: Request) -> Result<Request> {
// Find token in the query string and remove the token parameter from query string
let mut query: Vec<(String, String)> = req.get_query()?;
let token_index = query
.iter()
.position(|param| &param.0 == "token")
.ok_or_else(|| anyhow!("Token not found in query string"))?;
let token = query.remove(token_index).1;
// Update query without token
req.set_query(&query)?;
// Get timestamp and signature from token
// Token format: timstamp_signature
// example: 1893456000_39CNlxiAJLl96xmOHBGvSUyw-oY
let token_cap = Regex::new(r"^(\d+)_([a-zA-Z0-9_-]+)$")?
.captures(&token)
.ok_or_else(|| anyhow!("Token format is invalid"))?;
let expiry_time_str = token_cap
.get(1)
.ok_or_else(|| anyhow!("Failed to get token expire time"))?
.as_str();
let supplied_sig = token_cap
.get(2)
.ok_or_else(|| anyhow!("Failed to get token signature"))?
.as_str();
// Calculate signature
let message = format!(
"{}?{}{}{}",
req.get_path(),
req.get_query_str()
.ok_or_else(|| anyhow!("Failed get path and query"))?,
expiry_time_str,
header_val(req.get_header(header::USER_AGENT))
);
let hash = hmacsha1::hmac_sha1(SECRET, message.as_bytes());
let expected_sig = base64::encode_config(&hash, base64::URL_SAFE_NO_PAD);
// Check validity of the token
if secure_equal(supplied_sig.as_bytes(), expected_sig.as_bytes()) {
let expire_time: i64 = expiry_time_str.parse()?;
let now = chrono::Utc::now().timestamp();
if expire_time < now {
return Err(anyhow!(
"Token has expired, now {}, timestamp {}",
now,
expire_time
));
}
} else {
return Err(anyhow!(
"Token is incorrect, expected {}, actual {}, message signed \"{}\"",
expected_sig,
supplied_sig,
message
));
}
Ok(req)
}
/// Compare two byte slices with constant execution time. This provides some protection
/// against timing side-channel attacks.
fn secure_equal(a: &[u8], b: &[u8]) -> bool {
a.len() == b.len() && 0 == a.iter().zip(b).fold(0, |r, (a, b)| r | (a ^ b))
}