Project Summary
The RFC proposes a lightweight, industry-standard authentication layer for Forge app developers using Web Triggers. This ensures that functions invoked by publicly accessible Web Trigger URLs are only called by trusted parties.
-
Publish: 6 Aug 2026
-
Discuss:
-
Resolved:
Problem
The Forge Web Trigger capability lets developers create a public URL. When called via HTTP, that URL invokes a backend function in their Forge app. Developers can create these URLs explicitly via the CLI, or programmatically at runtime using the webTrigger module from @forge/api.
Currently, Forge Web Triggers lack native authentication or authorization. Developers must build their own solutions at the application layer, which is error-prone and a common source of security vulnerabilities.
In addition to solving the specific problem of simple, shared-secret authentication, this feature also lays the groundwork to provide a wider variety of more complex built-in authentication mechanisms in the future for developers and admins.
Proposal
We are proposing to support HMAC Authentication for Forge Webtriggers.
This proposal closes 3 key security gaps in the default Web Trigger capability:
-
Authentication: Only clients with a pre-shared secret key can call the URL.
-
Body Tampering: Signatures are computed at the client and re-computed at the server to ensure data integrity.
-
Replay Attacks: Shared secret keys expire and can be rotated. A leaked URL cannot be called indefinitely.
This will be an optional authentication solution, implemented at the platform layer and enabled via modules.webtrigger.request.authentication in your app’s manifest.yml:
# Example manifest.yml
modules:
webtrigger:
- key: hmac-auth-webtrigger
function: auth-webtrigger-fn
urlFormat: v2 # required for this feature
request:
authentication: hmacSharedSecret # valid values include: hmacSharedSecret | none`*
For backwards compatibility, the request field will be optional. Webtriggers that do not have this field defined will continue to work.
| Feature | Details |
|---|---|
| URL format | Authenticated URLs will use a new v2 format with an /auth/ path. |
| Key generation | Secret keys (base64 encoded) are provided via the Forge CLI (piped or interactive) or programmatically via the @forge/api runtime. |
| Authentication header | Requests must include the x-hub-signature header containing an HMAC SHA256 signature of the body, signed with a base64 decoded secret key. |
| Key management | Supports up to 2 keys per installation for rotation. Keys expire after 6 months. Decoded keys must be between 32 and 256 bits. |
# Example Authenticated URL
https://15314aab-9e4a-482e-9db5-fac2eaa17e60.webtrigger.atlassian.app/auth/dUye775SIX5AnlZPckfl2PF5cRs
Note: userPath segments are still supported for authenticated URLs.
URL Generation via CLI
Developers can supply the base64-encoded secret key via an interactive prompt or by piping it to the forge webtrigger create command.
# URL creation via piped input
echo -n "<encoded-shared-secret-key>" | forge webtrigger create --readSecretKey
## OR
# Url creation via interactive input
forge webtrigger create
? Select an installation: my-site.atlassian.net
? Select a web trigger: hmac-auth-webtrigger
? Enter the secret key for this HMAC web trigger: [input is hidden]
<developer types or pastes encoded secret key>
URL Generation via Runtime
The webTrigger module in @forge/api will be updated to accept the encoded shared secret key.
import { webTrigger } from "@forge/api"
async function createHmacAuthWebTrigger() {
const forceCreate = true;
// You can still force create urls with the existing API
// const url = await webTrigger.getUrl('simple-webtrigger', forceCreate);
const url = await webTrigger.getUrl('hmac-auth-webtrigger', {
forceCreate,
secretKey: '<encoded-shared-secret-key>'
});
return url;
}
URL HTTP Authentication
The x-webtrigger-signature will be required for requests to webtriggers that use authentication: hmacSharedSecret.
We chose x-webtrigger-signature because:
-
Using the
Authorizationheader could conflict with existing Web Triggers. -
It conforms to the
x-hub-signatureheader shape used by contemporary industry solutions, but clearly linking the header to the Forge Web Trigger capability. -
Atlassian already uses this approach in product webhooks such as Jira.
Clients compute a HMAC SHA256 signature of the HTTP body, using their decoded secret key. This signature is sent in the x-webtrigger-signature header. The server recomputes the signature and compares it to the header value. Requests only succeed when the signatures match.
# Example curl request
signature=$(echo -n <request-body> | openssl dgst -sha256 -hmac <your-decoded-secret-key> | sed -E 's/^.*= /sha256=/')
# The above 'openssl dgst -sha256 -hmac' command produces a signature of the shape: SHA2-256(stdin)= 8adf40a8b889640b2235c626e988ad3c2c328bd4e6a02860a7418f162e300c92
# The header value provided with the request should replace the prefix 'SHA2-256(stdin)= ' with the standard prefix 'sha256='.
# So, the value provided is 'sha256=8adf40a8b889640b2235c626e988ad3c2c328bd4e6a02860a7418f162e300c92'
curl -X POST \
https://15314aab-9e4a-482e-9db5-fac2eaa17e60.webtrigger.atlassian.app/auth/dUye775SIX5AnlZPckfl2PF5cRs \
-H "x-webtrigger-signature: $signature" \
-d <request-body>
Replay Prevention
Keys expire after 6 months once registered. During this period, if an attack can get ahold of an inflight request body and signature header, they can repeat this request until the key used to compute the signature expires.
Timestamps
Optionally, HTTP invocations can provide an additional header x-webtrigger-timestamp, with the RFC 3339 formatted timestamp of when the request was performed. If the request contains this header, the signature should be computed with the timestamp and body of the request, formatted as <timestamp>.<body>,
At the server, for requests that provide the x-webtrigger-timestamp header, the server will recompute the signature using the same format. It will also ensure that the timestamp provided is within 10 minutes, either side. This ensures that leaked requests are only valid for 10 minutes once made.
Requests without Data
GET requests, or any request with no body, should compute the digest using an empty string. The server uses the same approach when authenticating these requests. It is recommended that these requests, if possible, use the Timestamps feature to prevent indefinite replay attacks of their request (until key expiry).
Secret Keys
A webtrigger module can have at most 2 secret keys registered per product installation. This supports key rotation.
Creating a new URL with the same context (module, installation, app, and environment) and a new secret key will replace the oldest stored key. Both registered keys are tried at invocation time when comparing signatures.
Security
Decoded secret keys must be between 32 and 64 bytes long. This range ensures secure signature generation. Key length has no effect on signature generation time, at either the client or server, however providing keys longer than 64 bytes is useless as they will be truncated to 64 bytes during computation.
Expiry
Keys expire 6 months after registration. Once a key is within 30 days of expiry and is used to compute a signature for an authenticated request, the response of that HTTP request will contain a header x-webtrigger-key-expires with RFC 3339 standard date of the key expiry as the value.
# Example
x-webtrigger-key-expires: 2026-08-13T16:40:05Z #format: YYYY-MM-DDTHH:MM:SSZ
Hash Function Enforcement
x-webtrigger-signature header value must be prefixed with sha256=. The server will use this to determine the cryptographic hash function to perform the signature generation with. As part of this proposal, only sha256 is supported.
Asks
- Like or comment on this RFC if this feature would benefit you or your apps.
- Do you have feedback on the design or the proposed APIs?
- Is there anything we are missing that we could reasonably include?
- Does anyone require a method to determine the age of their registered keys?
RFC Update Log
13 Aug 2026
- Updated minimum bit length of decoded secret keys; 32 bits → 32 bytes.
- Updated maximum bit length of decoded secret keys; 256 bits → 64 bytes.
- Added optional
timestampfeature, to strengthen replay attack prevention. - Added expiry header in HTTP response, to indicate a key is close to expiry.
- Added
sha256=prefix to signature header value. - Consolidated all
headervalues with prefixx-webtrigger-*.