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:
- 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 objectfn 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| ¶m.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))}