The resource identifier is validated at construction on four axes — scheme, userinfo, fragment, and the query grammar (requireScheme, requireNoUserinfo, requireNoFragment, requireValidQuery in ProtectedResourceMetadata). The authority and the path are not.
Both are spliced verbatim into the same two places the query is:
- the
resource_metadata parameter of the 401 WWW-Authenticate challenge;
- the
resource member of the RFC 9728 document, which is served to unauthenticated callers.
WwwAuthenticate.escapeQuotedString strips control characters and escapes \ and ", and nothing else. So the reasoning that justified gating the query applies unchanged to these two components:
- a raw non-ASCII octet reaches a header field RFC 9110 §5.5 confines to US-ASCII;
- a character
java.net.URI rejects (a space, ", \, |, ^, {, }, <, >, or a malformed percent-escape) clears construction and then throws out of the 401 response path — a 500 where the challenge should be.
The query gate was added first because that was the component the change adding it touched. Closing the other two is the same shape of fix: validate against the RFC 3986 grammar at the same four boundaries, reject rather than rewrite, and say which component failed.
Worth checking before implementing: whether an authority gate should also cover the internationalized-domain case, where the identifier is valid to a browser and to a servlet container but is not a URI.
Three axes, not one — do not scope this to grammar
Review feedback on the change that added the query gate pointed out that "authority and path" understates what is missing, and that the realistic failure is not an exotic character.
1. Whitespace and control characters, anywhere in the identifier. The four construction gates do not reject them outside a query, so https://api.example.com/m cp constructs — and so does a resource read from a ConfigMap or an env var with a trailing \n, which is the case an operator actually hits. URI.create then throws from prmPath(), path() and normalizeRequestUrl(), so every 401 challenge and every DPoP-bound request 500s. This is the same 500-out-of-401 shape the query gate was added to close, on a more likely input.
2. A malformed port. A separate axis from the authority's character grammar and worth its own check.
3. The authority and path character productions — the original scope above.
This is catching up, not leading
cs-sdk has already shipped axes 1 and 2, and its implementation is the reference:
src/Authplane/Internal/ResourceIdentifiers.cs — ThrowIfWhitespaceOrBackslash covers whitespace, C0/DEL and backslash. Its doc notes it must run ahead of the absoluteness gate, because Uri.TryCreate trims surrounding whitespace before parsing and would otherwise accept an untrimmed identifier. That ordering detail is easy to miss and is the whole reason the gate works.
- the same file carries
ThrowIfMalformedPort as its own axis.
Read both before implementing, and match the ordering rather than the message.
The resource identifier is validated at construction on four axes — scheme, userinfo, fragment, and the query grammar (
requireScheme,requireNoUserinfo,requireNoFragment,requireValidQueryinProtectedResourceMetadata). The authority and the path are not.Both are spliced verbatim into the same two places the query is:
resource_metadataparameter of the 401WWW-Authenticatechallenge;resourcemember of the RFC 9728 document, which is served to unauthenticated callers.WwwAuthenticate.escapeQuotedStringstrips control characters and escapes\and", and nothing else. So the reasoning that justified gating the query applies unchanged to these two components:java.net.URIrejects (a space,",\,|,^,{,},<,>, or a malformed percent-escape) clears construction and then throws out of the 401 response path — a 500 where the challenge should be.The query gate was added first because that was the component the change adding it touched. Closing the other two is the same shape of fix: validate against the RFC 3986 grammar at the same four boundaries, reject rather than rewrite, and say which component failed.
Worth checking before implementing: whether an authority gate should also cover the internationalized-domain case, where the identifier is valid to a browser and to a servlet container but is not a URI.
Three axes, not one — do not scope this to grammar
Review feedback on the change that added the query gate pointed out that "authority and path" understates what is missing, and that the realistic failure is not an exotic character.
1. Whitespace and control characters, anywhere in the identifier. The four construction gates do not reject them outside a query, so
https://api.example.com/m cpconstructs — and so does a resource read from a ConfigMap or an env var with a trailing\n, which is the case an operator actually hits.URI.createthen throws fromprmPath(),path()andnormalizeRequestUrl(), so every 401 challenge and every DPoP-bound request 500s. This is the same 500-out-of-401 shape the query gate was added to close, on a more likely input.2. A malformed port. A separate axis from the authority's character grammar and worth its own check.
3. The authority and path character productions — the original scope above.
This is catching up, not leading
cs-sdkhas already shipped axes 1 and 2, and its implementation is the reference:src/Authplane/Internal/ResourceIdentifiers.cs—ThrowIfWhitespaceOrBackslashcovers whitespace, C0/DEL and backslash. Its doc notes it must run ahead of the absoluteness gate, becauseUri.TryCreatetrims surrounding whitespace before parsing and would otherwise accept an untrimmed identifier. That ordering detail is easy to miss and is the whole reason the gate works.ThrowIfMalformedPortas its own axis.Read both before implementing, and match the ordering rather than the message.