Authenticate against R2 with temporary credentials
Last updated Oct 16, 2026 The following examples show how to generate R2 temporary credentials via both the Temporary Credentials API and local client-side signing, and how to use the resulting credentials with an S3 client.
Prerequisites
- A parent R2 API token with at least the permissions you plan to delegate. Never ship parent credentials to a client.
- Your Cloudflare account ID.
- An S3 client that supports session tokens. The examples below use aws4fetch ↗︎.
Generate via the Temporary Credentials API
Call the Temporary Credentials API from a trusted server, then use the returned credentials with any S3 client.
curl https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/r2/temp-access-credentials \
--header "Authorization: Bearer <PARENT_API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"bucket": "my-bucket",
"parentAccessKeyId": "<PARENT_ACCESS_KEY_ID>",
"permission": "object-read-only",
"ttlSeconds": 900,
"objects": ["reports/2026-q1.pdf"]
}'
The response wraps the credentials in a result object:
{
"result": {
"accessKeyId": "<accessKeyId>",
"secretAccessKey": "<secretAccessKey>",
"sessionToken": "<sessionToken>"
},
"errors": [],
"messages": [],
"success": true
}
Generate locally (client-side signing)
This example uses jose ↗︎ to sign the JWT and aws4fetch ↗︎ to issue signed requests.
npmyarnpnpmbun
npm i jose aws4fetch
yarn add jose aws4fetch
pnpm add jose aws4fetch
bun add jose aws4fetch
The following helper signs a JWT with your parent secret access key and derives the temporary secret access key and session token:
temp-credentials.tsts
import { SignJWT } from "jose";
type R2Scope =
| "object-read-only"
| "object-read-write"
| "admin-read-only"
| "admin-read-write";
// Permitted operations: set exactly one of `scope` or `actions`, never both.
type PermittedOperations =
| { scope: R2Scope; actions?: never }
| { actions: [string, ...string[]]; scope?: never };
export type TempCredentialOptions = PermittedOperations & {
// Time-to-live in seconds. Defaults to 1 hour.
ttlSeconds?: number;
// Optional: restrict access to specific prefixes or objects.
paths?: { prefixPaths?: string[]; objectPaths?: string[] };
};
export async function createTempCredentials(
endpoint: string,
accountId: string,
parentAccessKeyId: string,
parentSecretAccessKey: string,
bucket: string,
opts: TempCredentialOptions,
): Promise<{
accessKeyId: string;
secretAccessKey: string;
sessionToken: string;
}> {
const ttl = opts.ttlSeconds ?? 3600;
const claims: Record<string, unknown> = { bucket };
if (opts.actions !== undefined) {
claims.actions = opts.actions;
} else {
claims.scope = opts.scope;
}
if (opts.paths !== undefined) {
claims.paths = {
prefixPaths: opts.paths.prefixPaths ?? [],
objectPaths: opts.paths.objectPaths ?? [],
};
}
// Sign the JWT with the parent secret access key. R2 validates this signature.
const jwt = await new SignJWT(claims)
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setSubject(accountId)
.setIssuer(parentAccessKeyId)
.setAudience(new URL(endpoint).host)
.setIssuedAt()
.setExpirationTime(`${ttl}s`)
.sign(new TextEncoder().encode(parentSecretAccessKey));
// The temporary secret access key is the SHA-256 hex digest of the signed JWT.
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(jwt),
);
const secretAccessKey = Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return {
// Reuse the parent access key ID as the temporary access key ID.
accessKeyId: parentAccessKeyId,
secretAccessKey,
// The session token is base64("jwt/" + signed JWT).
sessionToken: btoa(`jwt/${jwt}`),
};
}
The following example returns a credential that is valid for 15 minutes and can only GetObject and HeadObject under the data/ prefix:
import { createTempCredentials } from "./temp-credentials";
const R2_URL = `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`;
const creds = await createTempCredentials(
R2_URL,
ACCOUNT_ID,
PARENT_ACCESS_KEY_ID,
PARENT_SECRET_ACCESS_KEY,
"my-bucket",
{
actions: ["GetObject", "HeadObject"],
ttlSeconds: 900,
paths: { prefixPaths: ["data/"] },
},
);
Use the credentials
Once you have a temporary credential, usage is the same regardless of how it was generated. Pass the three values to your S3 client and issue requests. The following example uses credentials scoped to the data/ prefix to demonstrate one permitted and one rejected request:
import { AwsClient } from "aws4fetch";
const R2_URL = `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`;
const client = new AwsClient({
accessKeyId: ACCESS_KEY_ID,
secretAccessKey: SECRET_ACCESS_KEY,
sessionToken: SESSION_TOKEN,
service: "s3",
});
// Allowed: object under the data/ prefix.
const ok = await client.fetch(`${R2_URL}/my-bucket/data/file.bin`);
console.log(ok.status); // 200
// Rejected with 403 AccessDenied because the object is outside the data/ prefix.
const denied = await client.fetch(`${R2_URL}/my-bucket/other/file.bin`);
console.log(denied.status); // 403
Related resources
- Temporary credentials: concept reference and scoping model.
- R2 API tokens: create the parent token.
- Error codes: authentication error reference.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/r2/examples/authenticate-r2-temp-credentials/#page","headline":"Authenticate against R2 with temporary credentials","description":"Authenticate against R2 with temporary credentials.","url":"https://developers.cloudflare.com/r2/examples/authenticate-r2-temp-credentials/","inLanguage":"en","image":"https://developers.cloudflare.com/r2/examples/authenticate-r2-temp-credentials/og.png?v=1938303a00674d0e","dateModified":"2026-10-16","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Use the aws CLI to interact with Cloudflare R2 via the S3-compatible API. title: aws CLI image: https://developers.cloudflare.com/r2/examples/aws/aws-cli/og.png?v=82fdae7e6d6a9d15
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/r2/llms.txt
Use this file to discover all available pages before exploring further.
Docs