> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://uploadcare.com/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://uploadcare.com/docs/_mcp/server.

# Access control with signed URLs

> Control access to Uploadcare files with time-limited signed URLs.

Signed delivery keeps CDN files private until your backend grants access. Use it when files should not be reachable by public CDN URL alone.

## Prerequisites

To use signed URLs, enable them in your [project settings](https://app.uploadcare.com/projects/-/settings/#delivery) under **Delivery → CDN domain names → Enable signed URLs**:

1. Enable the secure subdomain for your project.
2. Generate a signing secret. You can have up to two secrets at a time: the second is for rotation.
3. Set up URL signing on your backend using the secret.
4. Once your integration is ready, disable the public subdomain to close unsigned access.

Signed URLs also work on [custom CNAMEs](/docs/delivery/cdn/#settings). Contact [support](mailto:help@uploadcare.com) to configure a branded domain for signed delivery.

> **Warning**
>
> **Warning:** If your project has a legacy `ucarecdn.com` domain, you must disable it to fully close unsigned access. This action is irreversible: once disabled, the legacy domain cannot be re-enabled.

## How it works \[#authenticated-urls]

When a client requests a file from your secure subdomain or a custom CNAME configured for signed delivery, the CDN validates the `token` query parameter before serving the file. Your backend generates this token with your signing secret and appends it to the delivery URL before returning or redirecting the client to it.

```mermaid
sequenceDiagram
    participant Client as Client
    participant Backend as Your backend
    participant CDN as Uploadcare CDN

    Client->>Backend: Request access to a file
    Backend->>Backend: Decide whether to grant access
    Backend->>Backend: Generate token (exp + acl + hmac)<br />using signing secret
    Backend-->>Client: Return or redirect signed CDN URL
    Client->>CDN: GET signed URL (?token=exp=...~acl=...~hmac=...)
    alt Token valid
        CDN-->>Client: 200 File
    else Token missing, expired, or mismatched
        CDN-->>Client: 403 Forbidden
    end
```

If the token is missing, expired, or does not match the requested path, the CDN returns `403 Forbidden`.

The CDN reads a `token` query parameter appended to your delivery URL. The token has three fields:

```
?token=exp={timestamp}~acl={acl}~hmac={digest}
```

| Field             | Description                                                                |
| ----------------- | -------------------------------------------------------------------------- |
| `exp={timestamp}` | Unix timestamp (seconds) after which the token is invalid                  |
| `acl={acl}`       | Access Control List: the path or path pattern this token grants access to  |
| `hmac={digest}`   | HMAC-SHA256 of `exp={timestamp}~acl={acl}`, keyed with your signing secret |

### ACL

The `acl` parameter defines which paths the token grants access to. It supports `*` as a *suffix* wildcard.

| ACL value                | Access granted                                                                      |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `/*`                     | Any file in the project                                                             |
| `/{uuid}/`               | Original file only                                                                  |
| `/{uuid}/*`              | Original file and all its variants (transformations, adaptive video segments, etc.) |
| `/{uuid}/-/resize/640x/` | One specific transformed version                                                    |

### Digest \[#digest]

The `hmac` field is a hex-encoded HMAC-SHA256 digest of the token body.

Token body is a string constructed from the expiry timestamp and ACL in this exact order:

```text
exp={timestamp}~acl={acl}
```

To calculate value of the `hmac` field use the signing secret as the HMAC key to compute an HMAC-SHA256 digest of the token body.

> **Warning**
>
> **Warning:** Most integration failures happen when your backend signs a different byte sequence than the CDN validates:
>
> * Hex-decode the signing secret before using it as the HMAC key.
> * Sign the ACL exactly as it appears in the token body. Do not URL-encode it before calculating the digest.

## Token playground

## Token generation libraries

These libraries implement the same HMAC construction and produce compatible signed-delivery tokens:

* [Node.js](https://github.com/akamai/EdgeAuth-Token-Node)
* [Python](https://github.com/akamai/EdgeAuth-Token-Python)
* [Ruby](https://github.com/akamai/EdgeAuth-Token-Ruby)
* [Java](https://github.com/akamai/EdgeAuth-Token-Java)
* [Go](https://github.com/mobilerider/EdgeAuth-Token-Golang)
* [PHP](https://github.com/uploadcare/uploadcare-php/#secure-delivery)
* [C#](https://github.com/BookBeat/EdgeAuth-Token-CSharp)

## File Uploader signing proxy \[#use-with-file-uploader]

File Uploader loads image previews from CDN URLs. With signed delivery enabled, unsigned preview URLs return `403 Forbidden`, and the browser cannot sign them because your signing secret must stay on your backend.

A signing proxy is your application endpoint that receives a File Uploader preview URL, checks whether the requester can access the file, signs the CDN path, and redirects to the signed CDN URL.

Use it with [`secureDeliveryProxy`](/docs/file-uploader/options/#secure-delivery-proxy) or [`secureDeliveryProxyUrlResolver`](/docs/file-uploader/options/#secure-delivery-proxy-url-resolver) File Uploader configuration options.

This follows the general signed delivery flow described in How it works, with the signing proxy endpoint acting as your backend.

Typical flow:

```text
File Uploader requests:
https://app.example.com/uc-preview?url=https%3A%2F%2F{subdomain}.ucarecd.net%2F{uuid}%2F

Your backend redirects to:
https://{subdomain}.s.ucarecd.net/{uuid}/?token=exp={timestamp}~acl={acl}~hmac={digest}
```

Your endpoint must:

1. Validate that preview URL points to one of your Uploadcare CDN hosts.
2. Check the requester's credentials and access to the requested file or CDN path.
3. Generate a time-limited token for the URL path.
4. Redirect to the signed URL.

The example below shows the signing proxy pattern. Replace `authenticateRequest` and `canViewUploadcareFile` with your application's own checks.

```javascript
import express from "express";
import EdgeAuth from "akamai-edgeauth";

const app = express();

const secureCdnOrigin = new URL("https://{subdomain}.s.ucarecd.net");

// File Uploader may pass either CDN host in the preview URL.
const allowedPreviewHosts = new Set([
  "{subdomain}.ucarecd.net",
  "{subdomain}.s.ucarecd.net",
]);

const signingSecret = process.env.UPLOADCARE_SIGNING_SECRET;
const durationSeconds = 500;
const tokenName = "token";

const edgeAuth = new EdgeAuth({
  key: signingSecret,
  windowSeconds: durationSeconds,
  tokenName,
  escapeEarly: true,
});

app.get("/uc-preview", async (req, res) => {
  const user = await authenticateRequest(req);
  if (!user) {
    return res.sendStatus(401);
  }

  let previewUrl;
  try {
    previewUrl = new URL(String(req.query.url || ""));
  } catch {
    return res.status(400).send("Invalid preview URL");
  }

  if (previewUrl.protocol !== "https:" || !allowedPreviewHosts.has(previewUrl.hostname)) {
    return res.status(400).send("Unsupported preview URL");
  }

  if (!(await canViewUploadcareFile(user, previewUrl.pathname))) {
    return res.sendStatus(403);
  }

  const token = edgeAuth.generateACLToken(previewUrl.pathname);
  const signedUrl = new URL(previewUrl.pathname + previewUrl.search, secureCdnOrigin);

  signedUrl.searchParams.set(tokenName, token);

  res.redirect(signedUrl.toString());
});

async function authenticateRequest(req) {
  // Authenticate the request and return the user object.
  return null;
}

async function canViewUploadcareFile(user, pathname) {
  // Check that `user` can access the file UUID or CDN path in `pathname`.
  return false;
}

app.listen(3000);
```

## Test environment

Use these credentials to test token generation without production keys:

* **Hostname:** `sectest.ucarecdn.com`
* **Secret:** `73636b61519adede42191efe1e73f02a67c7b692e3765f90c250c230be095211`

> **Warning**
>
> **Warning:** Do not use these credentials in production. This secret is publicly known.

## Billing

Signed URLs are available on all plans, including free.