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
typheader withat+jwtorapplication/at+jwt. - An exact
issstring. - An
audstring or array that contains the configured audience. - Numeric
expandiatclaims. - Non-empty
sub,client_id, andjtistrings. - A valid numeric
nbfclaim 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:
querycontrols query programs, read-only HTTP operations, and/metrics.mutatecontrols create, update, and delete statements for table data.catalogcontrols catalog statements and catalog HTTP operations.admincontrols 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.