Skip to main content

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;