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>

Playwright CLI Flags Tutorial

5 Playwright CLI Flags That Will Transform Your Testing Workflow

  • 0:56 --last-failed
  • 2:34 --only-changed
  • 4:27 --repeat-each
  • 5:15 --forbid-only
  • 5:51 --ui --headed --workers 1

Learn how these powerful command-line options can save you time, strengthen your test suite, and streamline your Playwright testing experience. Click on any timestamp above to jump directly to that section in the tutorial!

Watch Full Video 📹️

Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay