Repository navigation
Accept Client ID Metadata Documents as client IDs - #284
Merged
Merged
Conversation
With Client ID Metadata Documents (draft-ietf-oauth-client-id-metadata-document-02) a client uses the https URL of a JSON document it publishes about itself as its client_id, and lico fetches that document instead of keeping a registration. The authorization server then needs neither a pre-registration nor dynamic client registration for such a client, and clients use it when the authorization server announces support. It removes the last thing a user has to enter when dynamic registration is off. The new repeatable --client-id-metadata-document-host enables it. An exact host, with its port if it has one, matches as listed, *.example.com matches subdomains and * any public host. Wildcards never match an address, a single-label name, an explicit port or a name only meaningful inside a local network, such as localhost, local, internal, home.arpa, lan or test. There is no separate switch: listing a host enables the feature, and the metadata then announces client_id_metadata_document_supported, which together with none among the token endpoint auth methods is what lets a public client choose it. identity/clients/cimd.go checks the client_id URL against section 3 of the draft before any network access, being https with a path and without userinfo, query, fragment, dot segments or a trailing dot, and against the host list. It then makes one GET with Accept application/json and a 5 second timeout, follows no redirect (section 5), takes only a 200 with a JSON content type and reads at most 5 KB (section 8.7). The dialer checks the address it actually connects to against the RFC 6890 and IANA special-use blocks (section 8.6), so a listed name resolving inward is refused as well. No proxy is used, because a proxy would dial past that check. The document's client_id must equal the URL, and it may carry neither client_secret nor client_secret_expires_at. token_endpoint_auth_method must be absent or none: no shared secret is possible and private_key_jwt is not implemented, so anything else is refused rather than downgraded. Clients list grant types lico does not implement, such as urn:ietf:params:oauth:grant-type:jwt-bearer, and refusing them would lock such clients out, so grant types are read as a request per RFC 7591 section 3.2.1: unknown ones are dropped and only a document without authorization_code is refused. response_types must include code when present. Every redirect URI which is not loopback must be https on the client_id's host. Loopback redirects alone make a native client, https alone a web client, and a mix is refused. The redirect URI checks of Register are extracted into prepareRedirectURIs and run here too. The resulting registration is public, requires PKCE and is never trusted. Its Name is the host of the client_id, which nobody can fake, and the document's client_name travels beside it as ClaimedName. Details gains unverified, for dynamic and metadata document clients alike, and claimed_name, for the consent screen to use in the next change. Documents are cached as their Cache-Control says, parsed with github.com/pquerna/cachecontrol, which was already in the module graph and is now a direct dependency. The default is 5 minutes, bounded to between 1 minute and 24 hours. Concurrent lookups share one fetch, detached from the caller's context. Errors and invalid documents are never cached (section 5.2). A definite answer, a 4xx or an invalid document, drops the cached entry. A network error, a 5xx or a 429 keeps serving the last good document for up to 24 hours past its expiry, because the token endpoint looks the client up on every refresh. Registry.Lookup and Get try the static registry first, so an operator can still pre-register a URL client_id, then a metadata document for an https client_id when enabled, then dynamic clients. A failed fetch is the answer, never the implicit trust of a redirect to the issuer. NewRegistry takes the host list. The README lists the draft among the standards and describes it in the client registration section, with app.example.com as the example host. The API spec lists the draft in its info section, documents client_id values which are metadata document URLs, adds such clients to those which must use PKCE, and gains the metadata field. cimd_test.go covers the client_id URL rules, the host list including wildcards and local names, and the address check. A web and a native client document, in the shapes public clients publish, are fixtures alongside documents to be refused. The fetcher runs against a TLS test server for caching, a shared concurrent fetch, stale serving while the host fails and its limit, a 404 dropping the entry, uncached errors, a redirect, an oversized and a non-JSON document and an unlisted host never fetched, and the production client is shown to refuse a loopback address. A registry test drives Lookup with loopback redirects of a native client. Verified against a live public metadata document host listed with --client-id-metadata-document-host: the metadata announces support, both of its client IDs pass client lookup at the token endpoint and fail only on the bogus code with invalid_grant, a foreign redirect URI and an unlisted host fail the client lookup, and the host's max-age=300 sets the cache lifetime.
The previous change made two kinds of client possible which nobody
vouches for, dynamically registered ones and those described by a
Client ID Metadata Document, and requests can be bound to a protected
resource. The consent screen did not say who was asking or for what,
so it now does.
The web app passes the authorization request's resource to hello. The
identifier looks it up in the resource registry, which it now takes
as an optional manager, and answers resource with id and name on a
consent answer. The sentence becomes "app.example.com wants to use
<resource> to:" through the new key
konnect.consent.messageWithResource, falling back to the id when the
registration has no name. That name comes from the operator's
registration, so unlike the client's it is one the server vouches for.
An unknown resource never reaches the screen, because the
authorization endpoint refuses it with invalid_target first.
A client nobody vouches for chooses its own name, so that name alone
tells the user nothing. A client marked unverified is therefore named
by its display_name, which for a metadata document client is the host
of its client_id, the host its metadata document and its redirect URIs
live on, as section 8.5 of the draft asks. The document's
self-asserted claimed_name is shown beside it as "It identifies itself
as Example App.", never instead of it, and not at all when it only
repeats the host.
For such clients the redirect destination is stated on the page rather
than behind the info icon. A local destination, loopback or the
private-use scheme of a dynamically registered native client, is
stated as an application on this device and comes with a warning to
continue only when the user started the sign-in from that application,
since any application on the device can listen on localhost.
Registered clients keep the previous screen exactly.
The consent sentence naming the client is rendered with
react-i18next's <Trans> component, whose translations address the
client name element by position. Both variants of it, with and without
a resource, are now built in one const ahead of the JSX, so a single
biome-ignore keeps the formatter from reflowing them. The resource
variant carries explicit defaults, because i18next-parser would
otherwise extract the JSX expression literally instead of the
{{resource}} interpolation. All six shipped locales are translated, and
the catalogs were regenerated with make pot and merge, leaving no fuzzy
entries.
The API spec documents hello's resource request member, the resource
answer member as ResourceDetails, and unverified and claimed_name on
ClientDetails, and says that dynamically registered clients are shown
as unverified on the consent screen.
TestHelloNamesTheResource expects the resource named in canonical
form, and none without a resource or with an unknown one; it fails
without the change. Vitest cases cover the unverified host and claimed
name, the stated destination, the local warning for loopback and
private-use schemes, and the resource sentence with and without a
name. Every locale renders the resource and unverified notices without
unmapped indices or unresolved placeholders. The built web app served
by licod was also checked with stubbed hello answers for a web and a
native unverified client.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
With a Client ID Metadata Document (draft-ietf-oauth-client-id-metadata-document-02), a client uses the
httpsURL of a JSON document it publishes about itself as itsclient_id. The authorization server fetches the document instead of keeping a registration. Such a client needs neither a pre-registration nor dynamic client registration, which removes the last thing a user has to enter when dynamic registration is off.It is off unless enabled. The new repeatable
--client-id-metadata-document-hostlists the hosts whose documents lico fetches, and listing one is what enables it.Changes
Host list
*.example.commatches subdomains, and*matches any public host.localhost,local,internal,home.arpa,lanortest.client_id_metadata_document_supported.Fetching (
identity/clients/cimd.go)client_idURL is checked against section 3 of the draft before any network access.Cache-Controlsays: 5 minutes by default, bounded to between 1 minute and 24 hours. Concurrent lookups share one fetch.Validation
client_idmust equal its URL. It carries no secret, andtoken_endpoint_auth_methodmust be absent ornone.authorization_codeis refused.response_typesmust includecodewhen present.httpson theclient_id's own host (a web client) or loopback (a native client), never both. The redirect URI checks of dynamic registration are shared with this.client_id, then the metadata document, then dynamic clients.Consent screen
client_id, which it cannot fake, as section 8.5 of the draft asks. Its self-chosenclient_nameis shown only beside the host.Tests and docs
cimd_test.gocovers the URL rules, the host list, the address check, and the fetcher against a TLS test server: caching, a shared fetch, stale serving and its limit, refusals.TestHelloNamesTheResourceand Vitest cases cover the consent screen.client_idform, and the new consent fields.