Skip to main content
Version: 0.1.0

Adding relying parties

A relying party is an application that sends people to SAG to sign in. There are four ways to describe one, checked in this order.

1. Environment variables, for a handful

CLIENT_LEDGER_ID=ledger
CLIENT_LEDGER_REDIRECT_URIS=https://ledger.example.com/auth/callback
CLIENT_LEDGER_SECRET=... # omit for a public client
CLIENT_LEDGER_NAME=Ledger

LEDGER is a slug of your choosing; it only groups the variables together and never appears anywhere a person can see. Every field:

VariableMeaning
CLIENT_<SLUG>_IDThe client_id the application sends
CLIENT_<SLUG>_REDIRECT_URISComma or space separated. Matched exactly
CLIENT_<SLUG>_SECRETPresent means confidential, absent means public
CLIENT_<SLUG>_NAMEShown to the person: "Continue to Ledger"
CLIENT_<SLUG>_POST_LOGOUT_REDIRECT_URISWhere sign-out may return to
CLIENT_<SLUG>_JWKS / _JWKS_URIFor private_key_jwt authentication
CLIENT_<SLUG>_AUTH_METHODnone, client_secret_basic, client_secret_post, private_key_jwt
CLIENT_<SLUG>_SCOPESRestrict which scopes this client may ask for
CLIENT_<SLUG>_ACR_VALUESA floor, applied whether or not the client asks
CLIENT_<SLUG>_ID_TOKEN_SIGNED_RESPONSE_ALGWhich algorithm to sign its id_token with
CLIENT_<SLUG>_SESSION_SCOPEshared or rp, overriding the instance
CLIENT_<SLUG>_LOGOUT_CONFIRMauto, always or never
CLIENT_<SLUG>_TOS_URI / _POLICY_URILegal links shown on the sign-in pages
CLIENT_<SLUG>_SANITISE_PLUS_EMAILSWhether jamie+shop@ is jamie@ to this one client. Unset inherits SANITISE_PLUS_EMAILS
CLIENT_<SLUG>_REQUIRE_PKCEDefaults to true, and should stay true

Redirect URIs are matched exactly. OAuth 2.1 removed prefix and wildcard matching because every variant of it has been used to smuggle a code to somebody else's page. The only concession is the loopback port, which RFC 8252 requires to be ignored because a native application cannot know which port it will be given.

2. A client store, for many

JSON documents keyed by client id, in a directory, a Cloudflare KV namespace or an S3 bucket:

CLIENTS_STORE_BACKEND=file # a directory of files, Node and containers
CLIENTS_STORE_DIR=/data/clients

CLIENTS_STORE_BACKEND=cf-kv # Cloudflare
CLIENTS_STORE_KV_BINDING=SAG_CLIENTS

CLIENTS_STORE_BACKEND=s3 # AWS
CLIENTS_STORE_S3_BUCKET=example-sag-clients
CLIENTS_STORE_S3_REGION=eu-west-2
CLIENTS_STORE_PREFIX=clients/

The file backend is the one to use on a container or a single VM: one <client id>.json per relying party in a directory somebody can edit, re-read as it changes. It needs a filesystem, so it is available on Node and Lambda and not on Workers, where KV is the equivalent.

A record looks like this:

{
"client_name": "Ledger",
"redirect_uris": ["https://ledger.example.com/auth/callback"],
"client_secret_digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"acr_values": ["urn:sag:acr:federated-mfa"],
"subject_type": "pairwise",
"sector_identifier": "example.com",
"sanitise_plus_emails": false,
"tos_uri": "https://ledger.example.com/terms"
}

subject_type, sector_identifier and sanitise_plus_emails override the instance for this one relying party; leaving a field out inherits, which is not the same as setting it to false. What they mean is in ADR 0011.

Secrets are stored as sha256: digests, never in the clear - npm run generate-client-secret mints a high-entropy secret and its digest together - and the store is read-only from SAG's point of view: managing the records is somebody else's problem, which keeps the identity path free of a write path to break.

3. Client ID Metadata Documents, for no registration at all

The client id is an https URL that serves the client's own metadata. Nothing has to be registered anywhere: the document's URL is the identity, so only somebody controlling that origin can change what the client claims to be.

CLIENTS_CIMD_ENABLED=true # the default
CLIENTS_CIMD_ALLOWED_DOMAINS=example.com # empty means any
CLIENTS_CIMD_ALLOW_SUBDOMAINS=true
CLIENTS_CIMD_CACHE_TTL=300
CLIENTS_CIMD_MAX_BYTES=32768

SAG requires the document to claim its own URL as client_id, requires every redirect URI to share the document's origin, refuses redirects while fetching it, and caps its size. Those four rules are what stop a published document sending codes somewhere else.

Such a client is public by construction - the document is readable by anybody, so it can hold no secret - which is why PKCE is required of it regardless of what the document says. Publishing a jwks or jwks_uri instead is how one authenticates at the token endpoint, and the only way it stops being public.

A worked one, serving its own document and signing in with it, is in examples/relying-party:

EXAMPLE_USE_CIMD_AND_PUBLIC_CLIENT=1 npm run example

It runs as the fourth application in test/local-stack/, which is also the shortest demonstration of the one practical constraint: the client id has to resolve to the same place for the browser following a redirect and for SAG fetching the document.

4. Opaque clients

A GUID for a client id, with a hashed secret or a JWKS, held in the store. This is the shape for machine-registered relying parties. Set CLIENTS_OPAQUE_ENABLED=false to require every relying party to be either statically configured or self-describing.

Which authentication method to use

  • Public client (a single page app, a mobile app): no secret, PKCE mandatory. SAG will not accept a public client without PKCE.
  • Confidential client with a secret: client_secret_basic or client_secret_post.
  • Confidential client with a key: private_key_jwt, which never sends a shared secret over the wire at all and is the right answer for anything long-lived. The assertion's audience, lifetime and signature are all checked, and a reused assertion is refused.

Demanding a stronger sign-in

A relying party can ask for an authentication context, and SAG will refuse rather than quietly answering with something weaker:

acr_values=urn:sag:acr:federated-mfa

CLIENT_<SLUG>_ACR_VALUES sets a floor that applies whether or not the client asks, which is how an enterprise deployment stops one application from being the weak way in. See the acr and amr values in Configuration.