Authentication

Configure JWT access-token validation for the Rad HTTP API.

Rad can validate JWT access tokens from one OAuth2 or OIDC authorization server. Rad can also validate Cloudflare Access application tokens. Authentication is disabled by default.

Start Rad with JWT authentication

Set one issuer and one audience:

rad serve \
  --auth jwt \
  --auth-issuer https://auth.example.com/ \
  --auth-audience rad-production \
  --auth-query-scopes rad:read

Rad reads the OIDC discovery document from <issuer>/.well-known/openid-configuration. The discovery document must contain the exact configured issuer. Rad then loads the configured JWKS.

Use --auth-jwks-url to skip discovery:

rad serve \
  --auth jwt \
  --auth-issuer https://auth.example.com/ \
  --auth-audience rad-production \
  --auth-jwks-url https://auth.example.com/keys \
  --auth-query-scopes rad:read

The issuer and JWKS URL must use HTTPS. They must not contain credentials or a fragment. The issuer must not contain a query.

Rad accepts RS256, ES256, and EdDSA signatures. Each token must contain a key ID. Rad rejects token headers that supply a key or a key URL.

JWT mode uses the rfc9068 validation profile by default. This profile requires these values:

  • A typ header with at+jwt or application/at+jwt.
  • An exact iss string.
  • An aud string or array that contains the configured audience.
  • Numeric exp and iat claims.
  • Non-empty sub, client_id, and jti strings.
  • A valid numeric nbf claim when it is present.

Rad uses a fixed 60-second clock allowance. It rejects an iat value that is more than 60 seconds in the future.

Use the compatible profile when the authorization server does not issue RFC 9068 access tokens:

rad serve \
  --auth jwt \
  --auth-profile compatible \
  --auth-issuer https://auth.example.com/ \
  --auth-audience rad-production \
  --auth-query-scopes rad:read

The matching environment variable is RAD_AUTH_PROFILE=compatible. This profile accepts an absent typ header and the values JWT, at+jwt, and application/at+jwt. It does not require client_id, iat, or jti. It keeps the signature, key, issuer, audience, expiry, subject, and optional not-before checks. Rad does not change profiles in response to token content.

Use Cloudflare Access

Use the cloudflare-access profile when a Cloudflare Access application protects the Rad endpoint:

rad serve \
  --auth jwt \
  --auth-profile cloudflare-access \
  --auth-issuer https://team.cloudflareaccess.com \
  --auth-audience application-audience-tag \
  --auth-jwks-url https://team.cloudflareaccess.com/cdn-cgi/access/certs \
  --auth-query-scopes authenticated \
  --auth-mutate-scopes authenticated

The Worker or proxy must send the Cloudflare Access application token as the Rad bearer token. Cloudflare supplies this token in the Cf-Access-Jwt-Assertion request header. Do not send a Managed OAuth opaque token to Rad.

This profile accepts only RS256 signatures. It requires an application token with matching issuer and audience values. It accepts a JWT type header or no type header. It requires exp, iat, type, and sub claims. The type claim must be app. A user token uses sub as the principal. A service-token assertion has an empty sub; it must contain a non-empty common_name, which Rad uses as the principal.

Cloudflare Access applies the application policy before it sends a request to the origin. The profile supplies one authorization value named authenticated. Each Rad capability that this application grants must contain only this value:

RAD_AUTH_QUERY_SCOPES=authenticated
RAD_AUTH_MUTATE_SCOPES=authenticated
RAD_AUTH_CATALOG_SCOPES=authenticated
RAD_AUTH_ADMIN_SCOPES=authenticated

An omitted setting denies that capability. One Access application therefore represents one Rad role. Use separate Access applications or private endpoints for different roles. Keep the administration listener on a private network in production.

Rad does not call the Cloudflare identity endpoint on the request path. It loads the Access signing keys at startup. It refreshes the cached keys after five minutes or when a token contains an unknown key ID.

Restrict operations by scope

For the rfc9068 and compatible profiles, Rad maps the standard JWT scope claim to four capabilities. JWT mode requires one or more scope settings:

RAD_AUTH_QUERY_SCOPES="rad:read rad:admin"
RAD_AUTH_MUTATE_SCOPES="rad:write rad:admin"
RAD_AUTH_CATALOG_SCOPES="rad:catalog rad:admin"
RAD_AUTH_ADMIN_SCOPES="rad:admin"

The matching flags use the same values:

rad serve \
  --auth jwt \
  --auth-issuer https://auth.example.com/ \
  --auth-audience rad-production \
  --auth-query-scopes "rad:read rad:admin" \
  --auth-mutate-scopes "rad:write rad:admin" \
  --auth-catalog-scopes "rad:catalog rad:admin" \
  --auth-admin-scopes "rad:admin"

Each setting is a space-separated OAuth scope list. Scope values are case-sensitive. Do not use commas. Any configured value grants its capability. The same value can grant more than one capability.

The capabilities control these operations:

  • query controls query programs, read-only HTTP operations, and /metrics.
  • mutate controls create, update, and delete statements for table data.
  • catalog controls catalog statements and catalog HTTP operations.
  • admin controls the administration UI and its private API.

A program must have every capability that its statements require. Rad checks the complete program before it applies an effect.

Rad rejects JWT configuration when no scope setting is present. Each capability without a configured scope list is denied. Full authority requires explicit scope settings for all four capabilities. A token without a scope claim authenticates but has no capabilities. The scope claim must be a space-separated JSON string.

An authenticated request without a required capability receives HTTP 403 with code: forbidden and reason: insufficient_scope. The response includes this challenge:

WWW-Authenticate: Bearer realm="rad", error="insufficient_scope"

This format follows the scope syntax in RFC 6749, the JWT scope claim in RFC 9068, and the insufficient_scope response in RFC 6750.

Send an access token

Send the token in the HTTP authorization header:

Authorization: Bearer <access-token>

All public HTTP operations and /metrics require the token. /startupz, /readyz, /livez, and HTTP OPTIONS do not require the token.

For CLI commands, put the token in a file:

rad schema --access-token-file /run/secrets/rad-token status
rad doctor --access-token-file /run/secrets/rad-token

You can also set RAD_ACCESS_TOKEN_FILE. Rad reads the file before each request. This behavior supports projected token rotation. Project configuration does not store the token.

The Go client accepts a fixed token:

client, err := rad.Dial(endpoint, rad.WithBearerToken(token))

Use rad.WithHTTPClient with a custom transport when the client must refresh tokens automatically.

Authority and network requirements

The process role can further restrict data writes. The catalog mode can further restrict catalog changes.

JWT mode does not support the PostgreSQL frontend. JWT mode applies the same JWT validation to the public and administration listeners. The administration listener also requires a configured administration scope.

Rad serves plain HTTP. Put Rad behind a TLS proxy and use rads:// when bearer authentication is enabled.