oauth_as/metadata.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! RFC 8414 authorization server metadata: the discovery document served at
5//! `{issuer}/.well-known/oauth-authorization-server`.
6//!
7//! This document is the only thing a client is required to fetch before it can talk to us, so
8//! everything in it is load-bearing: an advertised endpoint that does not answer, or an
9//! advertised capability the server rejects, is a lie the client cannot recover from. The
10//! document is therefore DERIVED from [`ServerConfig`] rather than hand-written, and the
11//! capability lists are derived from what this crate actually implements.
12//!
13//! The type is pure data with a `serde` shape. Serving it is the host's job (or the optional
14//! `http` feature's); the library forces no HTTP stack on anyone.
15
16use serde::{Deserialize, Serialize};
17
18use crate::grant::DEVICE_CODE_GRANT_URN;
19use crate::server::ServerConfig;
20
21/// The well-known URI suffix RFC 8414 section 3 registers for this document.
22///
23/// This is the BARE form, correct only for an issuer with no path component. Use
24/// [`well_known_path`] to place the document for a given issuer: section 3.1 does not append the
25/// issuer's path to this string, it inserts this string between the host and that path.
26pub const WELL_KNOWN_PATH: &str = "/.well-known/oauth-authorization-server";
27
28/// The path component of an issuer identifier: `""` for `https://as.example`, `"/tenant1"` for
29/// `https://as.example/tenant1`.
30///
31/// Parsed by hand rather than with a URL crate because the whole shape needed is "everything from
32/// the first `/` after the authority", and this crate's dependency policy does not admit a URL
33/// parser for one line of string handling. A trailing slash is trimmed so
34/// `https://as.example/tenant1/` and `https://as.example/tenant1` agree.
35pub fn issuer_path(issuer: &str) -> &str {
36 let authority = match issuer.find("://") {
37 Some(i) => &issuer[i + 3..],
38 // A scheme-less string is not a shape RFC 8414 admits (section 2 requires an https URL),
39 // so it is read as a bare authority plus path rather than rejected here: the router is
40 // what refuses to serve an incoherent configuration.
41 None => issuer,
42 };
43 match authority.find('/') {
44 Some(i) => authority[i..].trim_end_matches('/'),
45 None => "",
46 }
47}
48
49/// Where this document lives for `issuer`, as an absolute path from the origin's root.
50///
51/// RFC 8414 section 3.1 is explicit and frequently got wrong: the well-known string is inserted
52/// BETWEEN the host and the issuer's path component. For issuer `https://as.example/tenant1` the
53/// document is at `https://as.example/.well-known/oauth-authorization-server/tenant1`, NOT at
54/// `https://as.example/tenant1/.well-known/...` and NOT at the bare well-known path.
55///
56/// Getting this wrong is a security matter and not only a routing one. Section 3.3 requires the
57/// client to check that the `issuer` member equals the URL the document was retrieved from, and
58/// that check is a mix-up countermeasure (RFC 9700 section 4.14). A document served where the
59/// check cannot pass teaches clients to skip it. In a multi-tenant deployment it is also a
60/// correctness bug outright: every tenant would collide on the one bare path.
61pub fn well_known_path(issuer: &str) -> String {
62 let path = issuer_path(issuer);
63 let mut out = String::with_capacity(WELL_KNOWN_PATH.len() + path.len());
64 out.push_str(WELL_KNOWN_PATH);
65 out.push_str(path);
66 out
67}
68
69/// An RFC 8414 authorization server metadata document.
70///
71/// Optional members are `Option` and are OMITTED when absent, never serialized as `null`: RFC
72/// 8414 defines member types, and `null` is not one of them.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74/// `#[non_exhaustive]`: five features (`par`, `jar`, `rar`, `mtls`, `resource-metadata`) each add a
75/// member, which is what an RFC 8414 document IS: the list of what this build can do. A host builds
76/// this with [`AuthorizationServerMetadata::from_config`], which is the only way to get a document
77/// that agrees with the server that will answer the requests it advertises; a literal written by
78/// hand is a document that describes a server nobody has to match. `Deserialize` is derived here
79/// and is unaffected, so a client-side or test-side consumer parsing one still works.
80#[non_exhaustive]
81pub struct AuthorizationServerMetadata {
82 /// REQUIRED (section 2). The AS's issuer identifier; must equal the URL this document was
83 /// retrieved from, minus the well-known path (section 3.3), and carries no query or fragment.
84 pub issuer: String,
85 /// REQUIRED for an AS supporting the authorization code grant (section 2).
86 pub authorization_endpoint: String,
87 /// REQUIRED unless only the implicit grant is supported, which OAuth 2.1 removes (section 2).
88 pub token_endpoint: String,
89 /// RFC 8628 section 4: how an AS advertises device grant support.
90 pub device_authorization_endpoint: String,
91 /// RFC 7662 section 2. Present ONLY when the host named
92 /// [`ServerConfig::introspection_endpoint`], and absent otherwise.
93 ///
94 /// # Why this one is opt-in and the others are not
95 ///
96 /// RFC 7662's primary consumer is a PROTECTED RESOURCE — the abstract defines the whole
97 /// document as "a method for a protected resource to query an OAuth 2.0 authorization server",
98 /// and section 1 as a protocol that "allows authorized protected resources to query the
99 /// authorization server". Through 0.9.1 this server had no channel for one: it
100 /// answered from a single arm, `Some(t) if t.client_id == client.client_id`, so a resource
101 /// server that did what the document told it to do was told, indistinguishably from the truth,
102 /// that every live token it held was dead. Advertising the endpoint unconditionally was
103 /// therefore the exact thing this module's opening rule forbids: an advertised capability the
104 /// server rejects.
105 ///
106 /// 0.9.2 BUILT THE CHANNEL. A resource server declared in
107 /// [`crate::ServerConfig::resource_servers`] now authenticates as an ordinary confidential
108 /// client and is answered about the tokens addressed to it.
109 ///
110 /// # Why this member stayed conditional anyway
111 ///
112 /// 0.9.1 recorded the intent that "when it lands this member becomes unconditional again", and
113 /// THAT INTENT IS DELIBERATELY NOT CARRIED OUT. Two reasons, and the second is the real one.
114 ///
115 /// The cheap reason is that going back to `String` is itself a breaking change, so the API
116 /// would take two breaking changes in consecutive releases to arrive where it started, and the
117 /// second one would be paid by every host that had already adapted to the first.
118 ///
119 /// The reason that actually decides it is that the capability is CONFIGURATION-DEPENDENT in a
120 /// way 0.9.1 did not anticipate when it wrote that sentence. The resource-server channel is
121 /// open only for a deployment that registered resource servers; one that registers none still
122 /// answers the token's own client and nobody else. Making the member unconditional would
123 /// therefore restore the original defect precisely for the deployments the 0.9.1 change was
124 /// made to protect -- the endpoint would be advertised to resource servers that this
125 /// particular deployment will never answer. The honest form of "advertise what you serve" is
126 /// for the host, which is the only party that knows whether it configured any, to say so.
127 ///
128 /// So: `Option`, and the host names the URL when it means the promise.
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub introspection_endpoint: Option<String>,
131 /// RFC 7009 section 2. Present when this server serves revocation.
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub revocation_endpoint: Option<String>,
134 /// RFC 7591 section 3 / RFC 8414 section 2. Present ONLY when the host enabled dynamic client
135 /// registration, and absent otherwise.
136 ///
137 /// The conditional is the whole point of the member. RFC 8414 section 2 makes it optional, and
138 /// a client reads its presence as "I may register here"; advertising it on a server that
139 /// refuses every registration would be an endpoint that 404s or 401s for reasons a client
140 /// cannot act on. The reverse is worse: RFC 7591 section 5 makes an unadvertised open endpoint
141 /// no safer than an advertised one, so this must not become the thing a host relies on to keep
142 /// registration private. It reports the configuration; it does not enforce it.
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub registration_endpoint: Option<String>,
145 /// RFC 8414 section 2. With the `jwt` feature, present only when the server issues signed (JWT)
146 /// access tokens; an AS with opaque tokens has no keys to publish and must not pretend
147 /// otherwise.
148 ///
149 /// WITHOUT that feature this crate signs nothing, so the member says only what
150 /// [`crate::server::ServerConfig::jwks_uri`] said: some other component holds the keys and
151 /// publishes them. Nothing in this crate serves that document, which is why the bundled `http`
152 /// service refuses to build when such a value points UNDER the issuer, where its own router
153 /// would answer the request with a 404.
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub jwks_uri: Option<String>,
156 /// OPTIONAL (section 2). Omitted when the host has not declared a scope catalogue, since an
157 /// empty array would claim the server supports no scopes at all.
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub scopes_supported: Option<Vec<String>>,
160 /// RFC 9126 section 5. Present ONLY when the host enabled PAR
161 /// ([`crate::server::ServerConfig::par`]): section 5 says its presence is sufficient for a
162 /// client to decide it may use PAR, so advertising an endpoint that is not served would be a
163 /// promise this server cannot keep.
164 #[cfg(feature = "par")]
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub pushed_authorization_request_endpoint: Option<String>,
167 /// RFC 9126 section 5. `Some(false)` states the default explicitly when PAR is offered;
168 /// omitted entirely when PAR is off, since section 5 gives an absent member the meaning
169 /// `false` and a server with no PAR endpoint has nothing to require.
170 #[cfg(feature = "par")]
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub require_pushed_authorization_requests: Option<bool>,
173 /// RFC 9101 section 4: the `alg` values this server will verify a request object with.
174 /// Present only when signed request objects are enabled.
175 #[cfg(feature = "jar")]
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub request_object_signing_alg_values_supported: Option<Vec<String>>,
178 /// RFC 9101 section 10.5, registered by its section 9.2. Present only when signed request
179 /// objects are enabled; `true` means a plain RFC 6749 authorization request is refused, which
180 /// is the downgrade that section describes.
181 #[cfg(feature = "jar")]
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub require_signed_request_object: Option<bool>,
184 /// REQUIRED (section 2). Always exactly `["code"]`: OAuth 2.1 removes the implicit grant.
185 pub response_types_supported: Vec<String>,
186 /// OPTIONAL (section 2). This server returns the code in the query string.
187 pub response_modes_supported: Vec<String>,
188 /// OPTIONAL (section 2), and worth stating: it is how a client learns the device grant is
189 /// available without probing.
190 pub grant_types_supported: Vec<String>,
191 /// OPTIONAL (section 2). Exactly the methods the token endpoint accepts.
192 pub token_endpoint_auth_methods_supported: Vec<String>,
193 /// RFC 8414 section 2. The signing algorithms the token endpoint accepts on an RFC 7523
194 /// client assertion.
195 ///
196 /// Section 2 makes this REQUIRED whenever `token_endpoint_auth_methods_supported` contains
197 /// `client_secret_jwt` or `private_key_jwt`, and the requirement is not bureaucratic: a client
198 /// cannot construct an assertion at all without knowing which algorithm the server will accept,
199 /// and guessing wrong is indistinguishable from a wrong key. Absent when this build does not
200 /// have the `client-assertion` feature, in which case neither method is advertised either.
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
203 /// RFC 9449 section 5.1: the JWS algorithms this server will verify a DPoP proof under.
204 ///
205 /// Its PRESENCE is how a client learns DPoP is available here at all, so it appears only when
206 /// this build can actually verify a proof. Advertising it on a server that would refuse every
207 /// proof is worse than omitting it, because a client that acts on it has no way to discover the
208 /// mistake except by failing to get a token.
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub dpop_signing_alg_values_supported: Option<Vec<String>>,
211 /// RFC 7636 / RFC 8414 section 2. Always exactly `["S256"]`: `plain` is not implemented, and
212 /// advertising it would invite a downgrade this server cannot honor.
213 pub code_challenge_methods_supported: Vec<String>,
214 /// OPTIONAL (section 2). A page of human-readable developer documentation.
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub service_documentation: Option<String>,
217 /// RFC 9728 section 4. The resource identifiers of the protected resources this AS
218 /// issues tokens for, so a client that fetched a resource's own RFC 9728 document can
219 /// cross-check that the AS agrees the relationship exists (section 7.6: an
220 /// `authorization_servers` entry is a claim made by the RESOURCE, and believing it
221 /// unchecked is how a resource points clients at an AS that never heard of it).
222 ///
223 /// OPTIONAL, and omitted rather than empty when the host declared none: an empty array
224 /// would state that this AS protects nothing, which is a different claim from silence.
225 #[cfg(feature = "resource-metadata")]
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub protected_resources: Option<Vec<String>>,
228 /// RFC 9396 section 10. The authorization details TYPES this server will accept, so a
229 /// client learns what it may ask for rather than discovering it from a refusal.
230 ///
231 /// Omitted rather than empty when the host declared none, exactly as `scopes_supported`
232 /// is: an empty array claims the server supports no types at all, which is a different
233 /// statement from silence and would be read as one. Note that the SERVER's behaviour for
234 /// the two is not different: an undeclared catalogue refuses every type (section 5), so
235 /// this member never overstates what the server will do.
236 #[cfg(feature = "rar")]
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub authorization_details_types_supported: Option<Vec<String>>,
239 /// draft-ietf-oauth-client-id-metadata-document-01 section 5 (section 6 in -02; see
240 /// [`crate::cimd`] for the renumbering). Whether this server dereferences an HTTPS URL used as
241 /// a `client_id`.
242 ///
243 /// DERIVED FROM [`ServerConfig::cimd`], never from the cargo feature. The feature compiles the
244 /// VALIDATOR in; it does not perform the fetch, because this crate performs no fetch at all
245 /// (see [`crate::cimd`]). So the only party who can answer this honestly is the host that
246 /// either wired the fetch or did not, and a member derived from `#[cfg]` would advertise a
247 /// capability a build might always refuse. Section 5's default when the member is absent is
248 /// `false`, and this publishes `false` explicitly rather than omitting it, because the member
249 /// is what a client checks before trying at all.
250 #[cfg(feature = "cimd")]
251 pub client_id_metadata_document_supported: bool,
252 /// RFC 9207 section 3. Always `true` from this server, and NOT an `Option`.
253 ///
254 /// The member exists so a client can decide whether it is allowed to REQUIRE the `iss`
255 /// authorization response parameter, which is the mix-up countermeasure RFC 9700 section 4.4
256 /// names. RFC 9207 section 3 says its default when absent is `false`, so omitting it would tell
257 /// every client that the countermeasure is unavailable here even though this server always
258 /// sends the parameter. Publishing a constant `true` is only honest because
259 /// [`crate::authorization::AuthorizationResponse`] and
260 /// [`crate::authorization::AuthorizationErrorRedirect`] both carry `iss` unconditionally: the
261 /// claim and the behaviour cannot drift apart, because neither type can express its absence.
262 ///
263 /// RFC 8707 (resource indicators), which this server also implements, registers NO metadata
264 /// member of its own, so there is deliberately nothing here to advertise it.
265 ///
266 /// `#[serde(default)]` on the way IN, and it is not a contradiction of the constant `true` on
267 /// the way out. Serialization is this server describing itself; deserialization is this type
268 /// reading someone else's document, and section 3 makes the member OPTIONAL there with a
269 /// default of `false`. Without the attribute a `bool` field is a REQUIRED member to serde, so
270 /// this type could not parse the document of any AS that omits it, which is every AS that does
271 /// not implement RFC 9207. `false` is also the fail-closed reading: a client that cannot see
272 /// the promise must not require the parameter.
273 #[serde(default)]
274 pub authorization_response_iss_parameter_supported: bool,
275 /// RFC 8705 section 3.3. Always `true` in a build with the `mtls` feature, and absent
276 /// entirely without it, which is the same honesty rule `jwks_uri` follows.
277 ///
278 /// Constant rather than configurable because the CODE PATH is: with the feature compiled in,
279 /// an access token issued for a request whose certificate the host passed in through
280 /// [`crate::server::ClientCredential::certificate`] is ALWAYS bound to it (RFC 8705 section 3).
281 /// There is no `ServerConfig` field that turns that off. Section 3.3's default when the member
282 /// is absent is `false`, so a build without the feature says nothing and means nothing, which
283 /// is correct.
284 ///
285 /// READ WHAT THE MEMBER THEREFORE MEANS, because it is narrower than it looks and the gap is
286 /// reachable. It says this server BINDS a certificate it is given; it does not and cannot say
287 /// that every token this deployment issues is bound, because whether a certificate arrives at
288 /// all is the host's affair. This crate never terminates TLS. In particular the bundled `http`
289 /// feature's router is handed an already-parsed request and passes `certificate: None` on every
290 /// credential it builds (see the comment on `Credentials::credential` in `crate::http`), so a
291 /// deployment whose only front door is that router publishes `true` here and binds nothing. A
292 /// host offering RFC 8705 has to reach the server through its own handler with the certificate
293 /// its terminator verified; a host that is not doing that should not compile the `mtls` feature
294 /// in, because this member is the promise a client acts on when it decides to present one.
295 ///
296 /// `#[serde(default)]` for the reason its neighbour above carries one, and the case is not
297 /// hypothetical here: a build WITHOUT this feature omits the member entirely, so an `mtls`
298 /// build reading a non-`mtls` build's own document failed outright with `missing field
299 /// "tls_client_certificate_bound_access_tokens"` until 0.9.1. Section 3.3's absent-means-false
300 /// is both the RFC's answer and the fail-closed one: a client that cannot see the promise must
301 /// not assume its token is bound.
302 #[cfg(feature = "mtls")]
303 #[serde(default)]
304 pub tls_client_certificate_bound_access_tokens: bool,
305}
306
307/// Join an issuer and an absolute path without producing a double slash.
308fn under_issuer(issuer: &str, path: &str) -> String {
309 format!("{}{}", issuer.trim_end_matches('/'), path)
310}
311
312/// The `jwks_uri` to advertise, which RFC 8414 section 2 ties to keys this server actually signs
313/// with.
314///
315/// With the `jwt` feature the truth about whether anything is signed lives in
316/// [`ServerConfig::access_token_format`], so it, and not the bare `jwks_uri` field, decides. Both
317/// halves of that matter: advertising a key set for an AS whose tokens are opaque points every
318/// resource server at keys that verify nothing, and signing without advertising leaves them no way
319/// to verify at all (RFC 9068 section 4 expects the key to be discoverable).
320#[cfg(feature = "jwt")]
321fn advertised_jwks_uri(config: &ServerConfig) -> Option<String> {
322 match &config.access_token_format {
323 crate::jwt::AccessTokenFormat::Opaque => None,
324 // `JwtConfig::with_jwks_uri` is the specific statement, so it wins; the `jwks_uri` field
325 // remains the fallback for a host that configured it there before enabling signing.
326 crate::jwt::AccessTokenFormat::Jwt(jwt) => jwt
327 .jwks_uri()
328 .map(str::to_string)
329 .or_else(|| config.jwks_uri.clone()),
330 }
331}
332
333/// Without the `jwt` feature this crate signs nothing, so the only possible source is the host's
334/// own declaration: it is publishing keys some other component holds, and serving that document
335/// is entirely its own affair.
336///
337/// "Its own affair" has a boundary, and the 0.9.1 audit found it: a host on the bundled `http`
338/// service does NOT serve this path, because every branch that routes a key set there is behind
339/// the `jwt` feature. So a value under the issuer in such a build advertises an endpoint that
340/// router can only 404, and `crate::http::ServiceBuilder::build` refuses it
341/// (`ServiceError::JwksNotServable`) rather than publishing the promise. A host serving its own
342/// listener is unaffected: this function reports the configuration and routes nothing.
343#[cfg(not(feature = "jwt"))]
344fn advertised_jwks_uri(config: &ServerConfig) -> Option<String> {
345 config.jwks_uri.clone()
346}
347
348impl AuthorizationServerMetadata {
349 /// Derive the document from the server's configuration.
350 ///
351 /// Endpoints the host did not override default to conventional paths under the issuer, so a
352 /// host that configures only an issuer still publishes a coherent, self-consistent document.
353 pub fn from_config(config: &ServerConfig) -> Self {
354 let iss = config.issuer.trim_end_matches('/').to_string();
355 // Exactly the grants AuthorizationServer::token will honor: advertising a grant this
356 // build does not implement, or this CONFIGURATION cannot serve, is the lie this document
357 // exists to avoid, so the list is built rather than written out once for each feature set.
358 let mut grant_types_supported = vec!["authorization_code".to_string()];
359 // RFC 6749 s6 is only reachable for a client that HOLDS a refresh token, and
360 // `ServerConfig::issue_refresh_tokens` decides whether this server ever mints one: with it
361 // off, `issue` refuses the refresh half of every issuance, so no client can ever arrive at
362 // the token endpoint with something to redeem. Advertising the grant there would be the
363 // same defect as advertising an endpoint that 404s — a capability a client reads as
364 // available, plans an implementation around, and can only discover is absent by failing.
365 // Conditional for exactly the reason `token-exchange` is conditional three lines below.
366 if config.issue_refresh_tokens {
367 grant_types_supported.push("refresh_token".to_string());
368 }
369 grant_types_supported.push("client_credentials".to_string());
370 grant_types_supported.push(DEVICE_CODE_GRANT_URN.to_string());
371 // RFC 8693 s2.1 registers the URN; RFC 8414 s2 is what makes advertising it the way a
372 // client learns the grant is available without probing.
373 #[cfg(feature = "token-exchange")]
374 grant_types_supported.push(crate::grant::TOKEN_EXCHANGE_GRANT_URN.to_string());
375 let endpoint = |override_: &Option<String>, path: &str| {
376 override_
377 .clone()
378 .unwrap_or_else(|| under_issuer(&iss, path))
379 };
380 AuthorizationServerMetadata {
381 authorization_endpoint: endpoint(&config.authorization_endpoint, "/authorize"),
382 token_endpoint: endpoint(&config.token_endpoint, "/token"),
383 device_authorization_endpoint: endpoint(
384 &config.device_authorization_endpoint,
385 "/device_authorization",
386 ),
387 // NOT `endpoint(...)`: the host's own value or nothing at all. See the field's doc for
388 // why this member alone is opt-in, and `crate::http::ServiceBuilder::build` for why
389 // the ROUTE is not.
390 introspection_endpoint: config.introspection_endpoint.clone(),
391 revocation_endpoint: Some(endpoint(&config.revocation_endpoint, "/revoke")),
392 registration_endpoint: config
393 .registration
394 .as_ref()
395 .map(|r| endpoint(&r.registration_endpoint, "/register")),
396 jwks_uri: advertised_jwks_uri(config),
397 scopes_supported: config.scopes_supported.clone(),
398 #[cfg(feature = "par")]
399 pushed_authorization_request_endpoint: config
400 .par
401 .as_ref()
402 .map(|par| par.endpoint(&iss)),
403 #[cfg(feature = "par")]
404 require_pushed_authorization_requests: config
405 .par
406 .as_ref()
407 .map(|par| par.require_pushed_authorization_requests),
408 // Every algorithm on this list is ES256, so the list is honest only where an ES256
409 // signature can be checked. Without the built-in backend that is not something a
410 // `&ServerConfig` can answer, so the member is omitted here and
411 // `es256_verification_is_available` puts it back when the server resolves a verifier.
412 // `require_signed_request_object` below stays the signal that RFC 9101 is configured
413 // at all, which is what that method keys off.
414 #[cfg(all(feature = "jar", feature = "jwt-p256"))]
415 request_object_signing_alg_values_supported: config.jar.as_ref().map(|_| {
416 crate::par::REQUEST_OBJECT_SIGNING_ALGS
417 .iter()
418 .map(|alg| alg.to_string())
419 .collect()
420 }),
421 #[cfg(all(feature = "jar", not(feature = "jwt-p256")))]
422 request_object_signing_alg_values_supported: None,
423 #[cfg(feature = "jar")]
424 require_signed_request_object: config
425 .jar
426 .as_ref()
427 .map(|jar| jar.require_signed_request_object),
428 issuer: iss,
429 response_types_supported: vec!["code".to_string()],
430 response_modes_supported: vec!["query".to_string()],
431 // Exactly the grants AuthorizationServer::token will honor.
432 grant_types_supported,
433 token_endpoint_auth_methods_supported: {
434 // `mut` only matters under `client-assertion`, which is what the allow is for. The
435 // alternative is two copies of the list, which is how two lists drift apart.
436 #[allow(unused_mut)]
437 let mut methods = vec![
438 "client_secret_basic".to_string(),
439 "client_secret_post".to_string(),
440 // RFC 8414 s2: the registered value a public client uses. This server accepts
441 // public clients, so omitting it would understate what it does.
442 "none".to_string(),
443 ];
444 // RFC 7523 s2.2, and the two methods do NOT have the same requirements.
445 // `client_secret_jwt` is HS256 over the secret the registration already holds, so
446 // the feature alone makes it true. `private_key_jwt` is ES256, and since the
447 // signing seam landed a build can have this feature and no way to check an ES256
448 // signature at all (`client-assertion = ["jwt"]`, which does not pull `jwt-p256`),
449 // in which case every such assertion is refused. So it is advertised here only
450 // with the built-in backend, and `es256_verification_is_available` adds it when
451 // the host installed a verifier of its own.
452 #[cfg(feature = "client-assertion")]
453 {
454 methods.push(crate::client_assertion::CLIENT_SECRET_JWT.to_string());
455 #[cfg(feature = "jwt-p256")]
456 methods.push(crate::client_assertion::PRIVATE_KEY_JWT.to_string());
457 }
458 // RFC 8705 s2.1.1 and s2.2.1 register these two, advertised exactly when this
459 // build can actually check a certificate. Both halves matter: advertising a
460 // method the endpoint rejects is a lie a client cannot recover from, and staying
461 // silent about one it accepts is how a client ends up sending a shared secret it
462 // did not need to have.
463 #[cfg(feature = "mtls")]
464 {
465 methods.push(crate::mtls::TLS_CLIENT_AUTH.to_string());
466 methods.push(crate::mtls::SELF_SIGNED_TLS_CLIENT_AUTH.to_string());
467 }
468 methods
469 },
470 // The same split, one member along: HS256 is checkable in every build with the
471 // feature, ES256 only where there is a backend to check it with.
472 #[cfg(all(feature = "client-assertion", feature = "jwt-p256"))]
473 token_endpoint_auth_signing_alg_values_supported: Some(
474 crate::client_assertion::ASSERTION_SIGNING_ALGS
475 .iter()
476 .map(|a| a.to_string())
477 .collect(),
478 ),
479 #[cfg(all(feature = "client-assertion", not(feature = "jwt-p256")))]
480 token_endpoint_auth_signing_alg_values_supported: Some(vec!["HS256".to_string()]),
481 #[cfg(not(feature = "client-assertion"))]
482 token_endpoint_auth_signing_alg_values_supported: None,
483 // RFC 9449 s5.1: a proof is an ES256 JWS this server VERIFIES, so with no backend
484 // there is no algorithm a client could send one under.
485 #[cfg(all(feature = "dpop", feature = "jwt-p256"))]
486 dpop_signing_alg_values_supported: Some(
487 crate::dpop::DPOP_SIGNING_ALG_VALUES_SUPPORTED
488 .iter()
489 .map(|a| a.to_string())
490 .collect(),
491 ),
492 #[cfg(all(feature = "dpop", not(feature = "jwt-p256")))]
493 dpop_signing_alg_values_supported: None,
494 #[cfg(not(feature = "dpop"))]
495 dpop_signing_alg_values_supported: None,
496 code_challenge_methods_supported: vec!["S256".to_string()],
497 service_documentation: config.service_documentation.clone(),
498 #[cfg(feature = "resource-metadata")]
499 protected_resources: config.protected_resources.clone(),
500 #[cfg(feature = "rar")]
501 authorization_details_types_supported: config
502 .authorization_details_types_supported
503 .clone(),
504 // The HOST's claim, not the build's: see the field's docs.
505 #[cfg(feature = "cimd")]
506 client_id_metadata_document_supported: config.cimd.is_some(),
507 authorization_response_iss_parameter_supported: true,
508 #[cfg(feature = "mtls")]
509 tls_client_certificate_bound_access_tokens: true,
510 }
511 }
512
513 /// Add back every advertisement that is honest only when an ES256 VERIFIER exists.
514 ///
515 /// [`AuthorizationServerMetadata::from_config`] cannot see one: `jwt-p256` compiles the
516 /// built-in backend in, and a host may install its own with
517 /// [`crate::AuthorizationServer::with_es256_verifier`], and neither is reachable from a
518 /// `&ServerConfig`. So `from_config` advertises the ES256-dependent members only when the
519 /// built-in backend is compiled in, and [`crate::AuthorizationServer::metadata`] calls this
520 /// when the server resolves a verifier.
521 ///
522 /// IDEMPOTENT, and it has to be: with `jwt-p256` on, `from_config` has already added all of
523 /// this, and a document that named `private_key_jwt` twice would be a defect of its own.
524 #[cfg(any(feature = "client-assertion", feature = "jar", feature = "dpop"))]
525 pub(crate) fn es256_verification_is_available(&mut self) {
526 #[cfg(feature = "client-assertion")]
527 {
528 let method = crate::client_assertion::PRIVATE_KEY_JWT.to_string();
529 if !self.token_endpoint_auth_methods_supported.contains(&method) {
530 self.token_endpoint_auth_methods_supported.push(method);
531 }
532 let algs = self
533 .token_endpoint_auth_signing_alg_values_supported
534 .get_or_insert_with(Vec::new);
535 if !algs.iter().any(|a| a == "ES256") {
536 algs.push("ES256".to_string());
537 }
538 }
539 #[cfg(feature = "jar")]
540 {
541 // Only for a server that HAS signed request objects enabled. `from_config` derives
542 // `require_signed_request_object` from `config.jar` and nothing else, so its presence
543 // is the one signal here for "RFC 9101 is configured" that does not depend on the
544 // very thing this method is adjusting.
545 if self.require_signed_request_object.is_some() {
546 self.request_object_signing_alg_values_supported = Some(
547 crate::par::REQUEST_OBJECT_SIGNING_ALGS
548 .iter()
549 .map(|alg| alg.to_string())
550 .collect(),
551 );
552 }
553 }
554 #[cfg(feature = "dpop")]
555 {
556 self.dpop_signing_alg_values_supported = Some(
557 crate::dpop::DPOP_SIGNING_ALG_VALUES_SUPPORTED
558 .iter()
559 .map(|a| a.to_string())
560 .collect(),
561 );
562 }
563 }
564}
565
566#[cfg(test)]
567#[path = "tests/metadata.rs"]
568mod tests;