DEV Community

Cover image for Using ColdFusion to Generate Pre-Signed Wasabi Download URL
James Moberg
James Moberg

Posted on

1

Using ColdFusion to Generate Pre-Signed Wasabi Download URL

There was an internal decision to use Wasabi Cloud Storage instead of Amazon S3 and I needed to use ColdFusion to generate a pre-signed URL to allow access to AI-generated content for a limited time. I had used the Sv4Util.cfc and aws-cfml libraries before with Amazon and thought it was just as simple, but I got confused somewhere along the way and it just wasn't working.

The Wasabi documention listed several approaches to generate a valid pre-signed URL...

  • Using the AWS CLI
  • Using the AWS Tools for Powershell
  • Using the S3 Browser
  • Using Wasabi Explorer
  • Using pre-signed S3 URLs for temporary, automated access in your application code
    • Python and Boto3
    • aws-sdk for Nodejs
    • AWS SDK for PHP (V2)

... but none of these solutions were very helpful for my environment and I didn't want to have to fallback to using the command line.

I thought it'd be an easy CFML function for AI to generate, but the results still weren't working.

After some additional searches on Google, I came across an Amazon API reference regarding Authenticating Requests: Using Query Parameters (AWS Signature Version 4) and it outlined a step-by-step approach with detail instructions, detailed descriptions and static example (with example output). Whenever I'm working with a third-party API, I always look for basic CURL examples so that I can see all the explicit settings and this example was perfect.

I was able to quickly debug & identify the issues that caused the calculation to be wrong. After finding out how to do it right, I took another look at the aws-cfml library and shared the cfml example with Wasabi (but I don't think they'll update their webpage to include links to AWS documentation or CFML examples.)

While I'm not currently using this generateS3PresignedUrl UDF in production, I thought I'd share it in case other developers can benefit from it.

Source Code

https://gist.github.com/JamoCA/bbdb652e4390898ea27eee489923ede3

<cfscript>
/**
* generateS3PresignedUrl: Generates a pre-signed Wasabi URL (ColdFusion 2016+ compatible)
* documentation https://docs.wasabi.com/v1/docs/how-do-i-generate-pre-signed-urls-for-temporary-access-with-wasabi
* How to calculate: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
* @displayname generateS3PresignedUrl
* @author James Moberg http://sunstarmedia.com, @sunstarmedia
* @version 1
* @lastUpdate 3/20/2025
* @gist https://gist.github.com/JamoCA/bbdb652e4390898ea27eee489923ede3
* @blog https://dev.to/gamesover/using-coldfusion-to-generate-pre-signed-wasabi-download-url-1fe0
* @twitter https://x.com/gamesover/status/1902748921388269703
* @LinkedIn https://www.linkedin.com/posts/jamesmoberg_using-coldfusion-to-generate-pre-signed-wasabi-activity-7308517169632288770-vu4J
* @param accessKey Wasabi access key
* @param secretKey Wasabi secret key
* @param bucketName Wasabi bucket name
* @param objectKey Object (file) path
* @param region Wasabi region (e.g., us-west-1)
* @param expiresIn Expiration time in seconds (1 hour default)
*/
public string function generateS3PresignedUrl(
required string accessKey,
required string secretKey,
required string bucketName,
required string objectKey,
string region = "us-west-1",
numeric expiresIn = 3600
) hint="Function to generate a presigned Wasabi URL" {
local.endpoint = "https://" & arguments.bucketname & ".s3." & lcase(arguments.region) & ".wasabisys.com";
local.objectKey = (len(trim(arguments.objectKey))) ? arguments.objectKey : "/";
if (left(local.objectKey, 1) neq "/") {
local.objectKey = "/" & local.objectKey;
}
// Current timestamp in ISO 8601 format (e.g., 20250314T185200Z)
local.utcTime = dateconvert("local2Utc", now());
local.amzDate = dateformat(local.utcTime, "yyyymmdd") & "T" & timeformat(local.utcTime, "HHmmss") & "Z";
local.dateStamp = tostring(dateformat(local.utcTime, "yyyymmdd"));
// canonical URI
local.canonicalQueryString = [
"X-Amz-Algorithm=AWS4-HMAC-SHA256"
,"X-Amz-Credential=" & arguments.accessKey & "%2F" & local.dateStamp & "%2F" & lcase(arguments.region) & "%2Fs3%2Faws4_request"
,"X-Amz-Date=" & local.amzDate
,"X-Amz-Expires=" & abs(val(arguments.expiresIn))
,"X-Amz-SignedHeaders=host"
];
// Canonical request
local.canonicalRequest = [
"GET"
,local.objectKey
,arraytolist(local.canonicalQueryString, "&")
,"host:" & rereplacenocase(local.endpoint, "https?:\/\/", "")
,""
,"host"
,"UNSIGNED-PAYLOAD"
];
// String to sign
local.stringToSign = [
"AWS4-HMAC-SHA256"
,local.amzDate
,local.dateStamp & "/" & lcase(arguments.region) & "/s3/aws4_request"
,lcase(hash(arraytolist(local.canonicalRequest, chr(10)), "SHA-256"))
];
// Generates signing key for AWS Signature V4
local.kSecret = charsetdecode("AWS4" & arguments.secretKey, "UTF-8");
local.kDate = binarydecode(hmac(left(local.dateStamp,8), local.kSecret, "HMACSHA256", "utf-8"), "hex");
local.kRegion = binarydecode(hmac(lcase(arguments.region), local.kDate, "HMACSHA256", "utf-8"), "hex");
local.kService = binarydecode(hmac("s3", local.kRegion, "HMACSHA256", "utf-8"), "hex");
local.kSigning = binarydecode(hmac("aws4_request", local.kService, "HMACSHA256", "utf-8"), "hex");
local.signature = lcase(hmac(arraytolist(local.stringToSign, chr(10)), local.kSigning, "HMACSHA256", "utf-8"));
// Final presigned URL
local.presignedUrl = [
local.endpoint
,local.objectKey
,"?"
,arraytolist(local.canonicalQueryString, "&")
,"&X-Amz-Signature="
,local.signature
];
return arraytolist(local.presignedUrl, "");
}
// Example usage
args = [
"accessKey": "my-access-key"
,"secretKey": "my-secret-key"
,"bucketName": "my-bucket-name"
,"objectKey": "mydir/myfile.mp3"
,"region": "us-west-1"
,"expiresIn": 3600
];
// writedump(var=args, label="args");
signedUrl = generateS3PresignedUrl(argumentcollection=args);
</cfscript>
<cfoutput>
<div><a href="#signedUrl#" target="_blank">New Window</a></div>
<div><textarea style="height:90px; width:95%">#signedUrl#</textarea></div>
<iframe src="#signedUrl#" style="height:200px; width:95%"></iframe>
</cfoutput>

Here's an example using the aws-cfml library.

https://gist.github.com/JamoCA/d2d38c5eaacba400decb21010124b159

<cfscript>
// configure awscfml CFC for Wasabi S3
// https://github.com/jcberquist/aws-cfml
initConfig = {
"awskey": #accessKeyId#
,"awsSecretKey": #secretAccessKey#
,"constructorArgs": [
"s3": [
"host": "s3.#region#.wasabisys.com"
]
]
};
aws = new awscfml.aws(argumentCollection=initConfig);
// identify & read local file
filePath = "d:\files\temporaryFile.zip";
zipFileData = fileReadBinary(filePath);
remoteObjectKey = "temp/MyZipFile.zip";
// configure file data for upload
uploadArgs = [
"bucket": #bucketName#
,"objectKey": remoteObjectKey
,"fileContent": zipFileData
,"ContentType": fileGetMimeType(filePath)
];
// perform upload
apiResponse = aws.s3.putObject(argumentcollection=args);
// dump API response
writedump(var=apiResponse, label="S3 putObject apiResponse");
</cfscript>

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

👋 Kindness is contagious

DEV is better (more customized, reading settings like dark mode etc) when you're signed in!

Okay