Securing Image With Signed URL in Golang

insomnius logo 2Muhammad Arief Rahman

November 16, 2024

4 min read

Feature WIP


A digital illustration of a blue, open padlock in the foreground symbolizing unlocked data. In the background, binary code and alphanumeric sequences are visible, representing cybersecurity and data access concepts. The overall color scheme is blue, giving a tech-focused impression

Securing user data is a critical concern for many businesses. A friend of mine at a legal services startup recently expressed his concerns about their KYC process, especially regarding how they manage users' identity card images.

Signed URLs are a simple yet powerful tool that provides secure, time-limited access to resources, helping reduce the risk of unauthorized exposure. Essentially, they let you control how long a resource is accessible and to whom.

Without signature:

https://storage.googleapis.com/example-bucket/cat.jpeg

With signature:

https://storage.googleapis.com/example-bucket/cat.jpeg?X-Goog-Algorithm=
GOOG4-RSA-SHA256&X-Goog-Credential=example%40example-project.iam.gserviceaccount
.com%2F20181026%2Fus-central-1%2Fstorage%2Fgoog4_request&X-Goog-Date=20181026T18
1309Z&X-Goog-Expires=900&X-Goog-SignedHeaders=host&X-Goog-Signature=247a2aa45f16
9edf4d187d54e7cc46e4731b1e6273242c4f4c39a1d2507a0e58706e25e3a85a7dbb891d62afa849
6def8e260c1db863d9ace85ff0a184b894b117fe46d1225c82f2aa19efd52cf21d3e2022b3b868dc
c1aca2741951ed5bf3bb25a34f5e9316a2841e8ff4c530b22ceaa1c5ce09c7cbb5732631510c2058
0e61723f5594de3aea497f195456a2ff2bdd0d13bad47289d8611b6f9cfeef0c46c91a455b94e90a
66924f722292d21e24d31dcfb38ce0c0f353ffa5a9756fc2a9f2b40bc2113206a81e324fc4fd6823
a29163fa845c8ae7eca1fcf6e5bb48b3200983c56c5ca81fffb151cca7402beddfc4a76b13344703
2ea7abedc098d2eb14a7

Common architectures that use storage buckets as components may look like this.

This process ensures secure, temporary access to a private resource (e.g., an image) stored in a storage bucket

StepDescription
1. Retrieve Image URL from the Storage BucketThe process begins with obtaining the image URL (http://your.website/cat.png) from the storage bucket.
2. Generate Signed URL in Application CodeThe application code creates a signed URL using:
  • The image URL.
  • A secret key (secret) for signature generation.
  • An expiration time (e.g., 10 minutes).
3. Retrieve the Generated URLThe application code outputs the signed URL, which includes the signature and expiration time (e.g., http://your.website/cat.png?signature=...&expired_at=...).
4. Return Signed URL from APIIf the signed URL is generated by an API, it returns the signed URL to the client application.
5. Request with Signed URLThe browser or client uses the signed URL to request the resource.
6. Verify and Retrieve Image URLThe server verifies the signed URL's signature and expiration. If valid, it allows access to the image.
7. Image Rendered in BrowserThe image (cat.png) is securely displayed in the browser.

This workflow ensures secure, temporary access to a private resource in a storage system.

Signed URLs work by using an algorithm and secret key that are not exposed to the client. Both of these must be the same when retrieving and getting the image. Fortunately, this is already supported by many popular frameworks, such as Laravel and Django.

Now, let's implement this in Go. For this example, we'll focus on retrieving an image, assuming the system is already connected to the database. Below is the API to fetch the image:

secretCode := "super secret code"

engine.GET("/users", func(c *gin.Context) {

  expiresAt := time.Now().Add(15 * time.Second).Unix()
  imageName := "image.jpg"

  signature := aurelia.Hash(secretCode, fmt.Sprintf("%d%s", expiresAt, imageName))

  encoder := json.NewEncoder(c.Writer)
  encoder.SetEscapeHTML(false)
  _ = encoder.Encode(gin.H{
   "data": gin.H{
    "id":        1,
    "name":      "Smitty Werben Men Jensen",
    "image_url": fmt.Sprintf("http://localhost:8080/avatar/image.jpg?signature=%s&expires_at=%d", signature, expiresAt),
   },
  })

  c.Status(200)
  c.Writer.Header().Set("Content-Type", "application/json")
 })

We use gin for the HTTP engine, and aurelia to automatically generate hash with salt. In that code assume that we already have image stored in our storage before, we also render expires_at within the image url.

Next step is to create other handler to retrieve the image.

 engine.GET("/avatar/image.jpg", func(c *gin.Context) {
  signature := c.Request.URL.Query().Get("signature")
  expiresAt := c.Request.URL.Query().Get("expires_at")

  if signature == "" || expiresAt == "" {
   c.JSON(400, gin.H{
    "message": "signature and expires_at cannot be empty",
   })
   return
  }

  expiresAtUnix, err := strconv.Atoi(expiresAt)
  if err != nil {
   c.JSON(400, gin.H{
    "message": "invalid format of expires_at",
   })
   return
  }

  if aurelia.Authenticate(secretCode, fmt.Sprintf("%d%s", expiresAtUnix, "image.jpg"), signature) == false {
   c.JSON(403, gin.H{
    "message": "unauthorized",
   })
   return
  }

  if time.Now().After(time.Unix(int64(expiresAtUnix), 0)) {
   c.JSON(404, gin.H{
    "message": "image not found",
   })
   return
  }

  c.File("./image.jpg")
 })

In this code we first check the signature and expired at params from the query parameter, after that we authenticate the hash with the secret code that we have before. If the hash is invalid, then we can return that the request is unauthorized. If the hash is valid, then the next step is to check whether the link is already expired, if it's then we could return 404 response to the users.

For full code example, you can check this link.

Conclusion

Signed URLs offer a level of protection by ensuring that data is accessible only for a limited time. However, the decision to implement them should depend on your specific use case and the level of security required for your application. As Uncle Bob wisely put it in his Scribe's Oath, "*The real software engineer is the one who puts the right thing in the right place." *Implementing signed URLs correctly ensures that you're doing just that, securing user data where it belongs.

Resource

The scribe's oath by uncle Bob. Seriously, watch this!!

AWS Cloudfront private content signed urls

GCS Storage Access Control with Signed URLS