RFC-141: HMAC Authenticated Web Triggers

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:

  1. Authentication: Only clients with a pre-shared secret key can call the URL.

  2. Body Tampering: Signatures are computed at the client and re-computed at the server to ensure data integrity.

  3. 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-hub-signature will be required for requests to webtriggers that use authentication: hmacSharedSecret.

We chose x-hub-signature because:

  • Using the Authorization header could conflict with existing Web Triggers.

  • The x-hub-signature header is the industry standard for this pattern.

  • Atlassian already uses it 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-hub-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/^SHA2-256\(stdin\)= //g')
# 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 omit the prefix 'SHA2-256(stdin)= '
# So, the value provided is just '8adf40a8b889640b2235c626e988ad3c2c328bd4e6a02860a7418f162e300c92'

curl -X POST \ 
https://15314aab-9e4a-482e-9db5-fac2eaa17e60.webtrigger.atlassian.app/auth/dUye775SIX5AnlZPckfl2PF5cRs \
-H "x-hub-signature: $signature" \
-d <request-body>

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.

Secret Key Security

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.

Decoded secret keys must be between 32 and 256 bits long. This range ensures secure signature generation. Key length has no effect on signature generation time, at either the client or server.

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?

Thanks for the RFC — the HMAC approach looks solid, and the 2-key rotation window is a good foundation.

One thing I’d like to raise: as far as I can tell, generating or replacing a secret key always requires the Forge CLI or the @forge/api runtime. Web Triggers are very commonly used for integrations between systems, and those integrations would benefit a lot from being able to rotate keys automatically — specifically, using a currently valid key to authorize the creation of the next one, without a human running the CLI or a separate privileged runtime call.

Concretely: could the platform support an authenticated rotation flow where a request signed with an existing valid key is allowed to register a new key (which then replaces the oldest, as already described)? That would let two systems roll their shared secret on a schedule with no manual intervention, while still honoring the 2-key limit and 6-month expiry you’ve outlined.

This connects to your open question about determining the age of registered keys — key-age visibility is really a prerequisite for automated rotation. To roll keys safely on a schedule, a system needs to know how old each key is and when it’s due to expire, so it can rotate ahead of the 6-month cutoff rather than reactively. So I’d say yes, an API to read key age (and ideally expiry) would be valuable, and it pairs naturally with the auto-rotation flow above.

Is auto-rotation something you’d consider supporting, either in this RFC or as a fast follow?

That sounds a lot like an OAuth refresh token flow :thinking:

True, token-refresh or auto-key-rotation are common patterns.

Hi there,

thanks for this - authentication at the platform layer rather than in every app is very welcome. Mostly questions.

Key length: the floor is 32 bits, and the RFC says the range “ensures secure signature generation”. Have I read that right? RFC 2104 §3 suggests at least the hash output length, and NIST SP 800-224 ipd puts the minimum at 128. Since key length doesn’t affect signing time, could the floor go up?

Signature format: Jira sends X-Hub-Signature: sha256=<hex> per WebSub; here the prefix is dropped. Deliberate? It leaves no non-breaking path to another algorithm, and existing x-hub-signature libraries reject a bare digest on length before they verify.

Replay: would an optional timestamp in the signed string, Stripe-style, be in scope? With an empty body the digest is constant per key, so on GET the header is effectively a fixed token.

Expiry and failures: what happens at six months: hard fail, grace period, warning? And does a bad signature respond differently from a bad URL token?

Runs on Atlassian: my reading is that this is orthogonal, since the RoA rule keys off response.type and inbound auth isn’t in the criteria. Is that right? And would request.authentication ever become a criterion?

Small one: the curl example passes the decoded key as a shell argument to openssl -hmac - a key containing a NUL byte is silently truncated and the signature comes out wrong with no error.

Cheers,
paul