Verify calls to your target

Turn on signed calls, then check in your target that each call came from the scheduler and from a job you expect.

Anyone who knows your target’s address can call it. With signing on, every call carries a token that proves it came from the scheduler, so your target can refuse everything else.

Turn signing on

Set signing.enabled to true on the job. Each attempt then sends:

Authorization: Bearer <token>

You cannot set your own Authorization header on a job that has signing on.

The token

The token is a JSON Web Token signed with RS256, and it expires 10 minutes after it is made.

ClaimHolds
isshttps://scheduler-api.digitospace.com
audThe job’s audience: the target URL without its query, unless you set signing.audience on the same site
subprojects/{project}/jobs/{job}, the job that made the call
expWhen the token expires

The public keys are published at https://scheduler-api.digitospace.com/.well-known/jwks.json, with a discovery document at https://scheduler-api.digitospace.com/.well-known/openid-configuration.

Check a call

  1. Fetch the public keys

    Fetch the key set and cache it for about 5 minutes. Most JWT libraries do this for you.

  2. Check the signature

    Use the key named by the token’s kid header, and accept only RS256.

  3. Check the claims

    Check that iss is https://scheduler-api.digitospace.com, aud is your job’s audience, and exp has not passed.

  4. Check the job

    Read sub and accept only the jobs you expect. A token from another project or job is not meant for your target, even when its audience matches.

This example uses the jose library.

verify-scheduler-call.ts
import { createRemoteJWKSet, jwtVerify } from "jose"

const schedulerUrl = "https://scheduler-api.digitospace.com"
const keys = createRemoteJWKSet(
  new URL("/.well-known/jwks.json", schedulerUrl)
)

export async function verifySchedulerCall(request: Request, jobs: string[]) {
  const header = request.headers.get("authorization") ?? ""
  const token = header.replace(/^Bearer /, "")
  const { payload } = await jwtVerify(token, keys, {
    issuer: schedulerUrl,
    audience: "https://mail.example.com/tasks/report",
    algorithms: ["RS256"],
  })
  if (!payload.sub || !jobs.includes(payload.sub)) {
    throw new Error("This job may not call this target")
  }
  return payload.sub
}

Key rotation

The scheduler rotates its signing keys from time to time. A retired key stays published for 24 hours, so tokens already sent still check out. Fetch keys by kid rather than keeping one key in your code.

Last updated