Skip to main content

notedthat_core/
auth.rs

1//! Bearer token verification/extraction per RFC 6750 and Basic auth helpers per RFC 7617,
2//! and the [`Authenticator`] every surface resolves its [`Principal`] through.
3
4use crate::access::{Principal, UserIdentity};
5use async_trait::async_trait;
6use base64::Engine as _;
7use http::header::AUTHORIZATION;
8use http::{HeaderMap, HeaderValue};
9use std::sync::Arc;
10use subtle::ConstantTimeEq;
11
12/// Verify a Bearer token in constant time.
13///
14/// # Token length leakage
15///
16/// This function short-circuits on length mismatch (returns `false` immediately
17/// without invoking the constant-time comparison). This leaks the length of the
18/// expected token, which is an **acceptable limitation** for a static Bearer token
19/// per D21 (the token length is small and enumerable).
20pub fn verify_bearer_token(provided: &str, expected: &str) -> bool {
21    if provided.len() != expected.len() {
22        return false;
23    }
24    if expected.is_empty() {
25        return false;
26    }
27    provided.as_bytes().ct_eq(expected.as_bytes()).into()
28}
29
30/// Extract the Bearer token value from an `Authorization` header value.
31///
32/// The scheme match is **case-insensitive** per RFC 6750 §2.1. The token
33/// is separated from the scheme by exactly one space; tabs and multiple
34/// consecutive spaces are rejected.
35///
36/// Returns `None` if the header value is malformed or missing a token.
37pub fn extract_bearer_from_header(value: &str) -> Option<&str> {
38    let (scheme, rest) = value.split_once(' ')?;
39    if !scheme.eq_ignore_ascii_case("bearer") {
40        return None;
41    }
42    if rest.is_empty() || rest.starts_with(' ') {
43        return None;
44    }
45    Some(rest)
46}
47
48/// Verify HTTP Basic credentials in constant time.
49///
50/// Empty usernames/passwords are rejected. Length mismatches are rejected before
51/// comparison, matching the accepted Bearer-token length leakage precedent.
52pub fn verify_basic_credentials(
53    provided_username: &str,
54    provided_password: &str,
55    expected_username: &str,
56    expected_password: &str,
57) -> bool {
58    if provided_username.is_empty()
59        || provided_password.is_empty()
60        || expected_username.is_empty()
61        || expected_password.is_empty()
62    {
63        return false;
64    }
65
66    if provided_username.len() != expected_username.len()
67        || provided_password.len() != expected_password.len()
68    {
69        return false;
70    }
71
72    let username_match = provided_username
73        .as_bytes()
74        .ct_eq(expected_username.as_bytes());
75    let password_match = provided_password
76        .as_bytes()
77        .ct_eq(expected_password.as_bytes());
78    let both_match = username_match & password_match;
79    bool::from(both_match)
80}
81
82/// Extract a non-empty username and password from an HTTP Basic `Authorization` header.
83///
84/// The scheme match is case-insensitive. Credentials are decoded with the
85/// standard Base64 alphabet and split on the first colon so passwords may
86/// contain colons.
87pub fn extract_basic_from_header(value: &str) -> Option<(String, String)> {
88    let (scheme, rest) = value.split_once(' ')?;
89    if !scheme.eq_ignore_ascii_case("basic") {
90        return None;
91    }
92    if rest.starts_with(' ') {
93        return None;
94    }
95
96    let decoded = base64::engine::general_purpose::STANDARD
97        .decode(rest)
98        .ok()?;
99    let decoded_str = String::from_utf8(decoded).ok()?;
100    let (username, password) = decoded_str.split_once(':')?;
101    if username.is_empty() || password.is_empty() {
102        return None;
103    }
104    Some((username.to_string(), password.to_string()))
105}
106
107/// A credential was supplied and did not verify.
108///
109/// Distinct from "no credential", which is [`Principal::Anyone`] and may still
110/// be granted access — the whole point of the distinction is that a supplied
111/// credential never silently downgrades.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct CredentialRefused;
114
115/// Which `Authorization` schemes a surface accepts.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum Schemes {
118    /// `Bearer` only — the HTTP API, the browse pages and MCP.
119    Bearer,
120    /// `Basic` as well — `WebDAV`, whose clients prompt for a username and password.
121    BasicOrBearer,
122}
123
124/// Why a bearer token an identity provider was asked about did not verify.
125///
126/// Carries a reason for the log line and nothing else: never the token, never
127/// its claims.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct TokenRejected {
130    /// A short operator-facing reason, safe to log.
131    pub reason: String,
132}
133
134impl TokenRejected {
135    /// Build a rejection from its reason.
136    pub fn new(reason: impl Into<String>) -> Self {
137        Self {
138            reason: reason.into(),
139        }
140    }
141}
142
143/// Something that can turn a bearer token into the identity behind it.
144///
145/// The one seam between the surfaces and an identity provider: the server
146/// implements it over OIDC discovery and a JWKS, and tests implement it over a
147/// map. Consulted only after the service token failed to match.
148#[async_trait]
149pub trait TokenVerifier: Send + Sync {
150    /// Verify `token` and return the identity it vouches for.
151    async fn verify(&self, token: &str) -> Result<UserIdentity, TokenRejected>;
152}
153
154/// The RFC 9728 protected-resource metadata this deployment publishes.
155///
156/// Served at `/.well-known/oauth-protected-resource` and named in the
157/// `WWW-Authenticate` challenge on `401`s, which is how an MCP client finds
158/// the authorization server to obtain a token from.
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct ProtectedResource {
161    /// The resource identifier: this deployment's public URL.
162    pub resource: String,
163    /// The issuers that mint tokens for it.
164    pub authorization_servers: Vec<String>,
165    /// Where the metadata document itself is served.
166    pub metadata_url: String,
167}
168
169impl ProtectedResource {
170    /// The metadata document, as JSON.
171    pub fn document(&self) -> serde_json::Value {
172        serde_json::json!({
173            "resource": self.resource,
174            "authorization_servers": self.authorization_servers,
175            "bearer_methods_supported": ["header"],
176        })
177    }
178}
179
180/// The single definition of the credential rules, shared by every surface.
181///
182/// Absent header → [`Principal::Anyone`]. Exactly one `Authorization` header
183/// carrying the service token, the `WebDAV` Basic pair (where accepted), or a
184/// bearer token the [`TokenVerifier`] vouches for → [`Principal::SignedIn`].
185/// Anything else — a second header, an unknown scheme, a token nobody
186/// recognises — is [`CredentialRefused`], never a downgrade to anonymous.
187pub struct Authenticator {
188    service_token: String,
189    basic: Option<(String, String)>,
190    verifier: Option<Arc<dyn TokenVerifier>>,
191    protected_resource: Option<ProtectedResource>,
192}
193
194impl std::fmt::Debug for Authenticator {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        f.debug_struct("Authenticator")
197            .field("service_token", &"<redacted>")
198            .field(
199                "basic",
200                &self.basic.as_ref().map(|(user, _)| (user, "<redacted>")),
201            )
202            .field("verifier", &self.verifier.as_ref().map(|_| "<configured>"))
203            .field("protected_resource", &self.protected_resource)
204            .finish()
205    }
206}
207
208impl Authenticator {
209    /// An authenticator that recognises only `service_token` as a bearer.
210    pub fn new(service_token: impl Into<String>) -> Self {
211        Self {
212            service_token: service_token.into(),
213            basic: None,
214            verifier: None,
215            protected_resource: None,
216        }
217    }
218
219    /// Also accept this `Basic` username and password, on surfaces that allow it.
220    #[must_use]
221    pub fn with_basic(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
222        self.basic = Some((username.into(), password.into()));
223        self
224    }
225
226    /// Also accept bearer tokens `verifier` vouches for.
227    #[must_use]
228    pub fn with_token_verifier(mut self, verifier: Arc<dyn TokenVerifier>) -> Self {
229        self.verifier = Some(verifier);
230        self
231    }
232
233    /// Publish RFC 9728 metadata and name it in bearer challenges.
234    #[must_use]
235    pub fn with_protected_resource(mut self, resource: ProtectedResource) -> Self {
236        self.protected_resource = Some(resource);
237        self
238    }
239
240    /// Whether bearer tokens other than the service token can verify at all.
241    pub fn accepts_identity_tokens(&self) -> bool {
242        self.verifier.is_some()
243    }
244
245    /// The published protected-resource metadata, if configured.
246    pub fn protected_resource(&self) -> Option<&ProtectedResource> {
247        self.protected_resource.as_ref()
248    }
249
250    /// The `WWW-Authenticate` value a bearer-only surface adds to a `401`.
251    ///
252    /// Only present when metadata is published: a bare `Bearer` challenge tells
253    /// a client nothing it did not know, and browsers ignore it, so there is
254    /// nothing to gain from emitting one.
255    pub fn bearer_challenge(&self) -> Option<HeaderValue> {
256        let resource = self.protected_resource.as_ref()?;
257        HeaderValue::from_str(&format!(
258            "Bearer resource_metadata=\"{}\"",
259            resource.metadata_url
260        ))
261        .ok()
262    }
263
264    /// Resolve the principal behind a request's `Authorization` headers.
265    ///
266    /// # Errors
267    ///
268    /// [`CredentialRefused`] when an `Authorization` header is present and does
269    /// not carry exactly one credential this authenticator accepts.
270    pub async fn resolve(
271        &self,
272        headers: &HeaderMap,
273        accept: Schemes,
274    ) -> Result<Principal, CredentialRefused> {
275        let mut values = headers.get_all(AUTHORIZATION).iter();
276        let Some(header) = values.next() else {
277            return Ok(Principal::Anyone);
278        };
279        if values.next().is_some() {
280            return Err(CredentialRefused);
281        }
282        let header = header.to_str().map_err(|_| CredentialRefused)?;
283
284        if let Some(token) = extract_bearer_from_header(header) {
285            return self.resolve_bearer(token).await;
286        }
287        if accept == Schemes::BasicOrBearer
288            && let Some((username, password)) = extract_basic_from_header(header)
289            && let Some((expected_user, expected_pass)) = &self.basic
290            && verify_basic_credentials(&username, &password, expected_user, expected_pass)
291        {
292            return Ok(Principal::service_token());
293        }
294        Err(CredentialRefused)
295    }
296
297    async fn resolve_bearer(&self, token: &str) -> Result<Principal, CredentialRefused> {
298        // The service token is checked first and in constant time; the verifier
299        // is only consulted for something that is not it, so a deployment
300        // without an identity provider never changes behaviour.
301        if verify_bearer_token(token, &self.service_token) {
302            return Ok(Principal::service_token());
303        }
304        let Some(verifier) = &self.verifier else {
305            return Err(CredentialRefused);
306        };
307        match verifier.verify(token).await {
308            Ok(identity) => Ok(Principal::SignedIn(crate::access::Identity::User(identity))),
309            Err(rejected) => {
310                tracing::debug!(reason = %rejected.reason, "bearer token rejected");
311                Err(CredentialRefused)
312            }
313        }
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_verify_bearer_token_matching() {
323        assert!(verify_bearer_token("secret-token", "secret-token"));
324    }
325
326    #[test]
327    fn test_verify_bearer_token_different_same_length() {
328        assert!(!verify_bearer_token("aaaabbbb", "aaaacccc"));
329    }
330
331    #[test]
332    fn test_verify_bearer_token_empty_provided() {
333        assert!(!verify_bearer_token("", "some-token"));
334    }
335
336    #[test]
337    fn test_verify_bearer_token_empty_expected() {
338        assert!(!verify_bearer_token("some-token", ""));
339    }
340
341    #[test]
342    fn test_verify_bearer_token_different_lengths() {
343        assert!(!verify_bearer_token("short", "much-longer-token"));
344    }
345
346    #[test]
347    fn test_verify_bearer_token_case_sensitive() {
348        assert!(!verify_bearer_token("AbC", "abc"));
349    }
350
351    #[test]
352    fn test_verify_bearer_token_whitespace_sensitive() {
353        assert!(!verify_bearer_token("tok", "tok "));
354    }
355
356    #[test]
357    fn test_extract_bearer_lowercase_scheme() {
358        assert_eq!(extract_bearer_from_header("Bearer abc123"), Some("abc123"));
359    }
360
361    #[test]
362    fn test_extract_bearer_lowercase_bearer() {
363        assert_eq!(extract_bearer_from_header("bearer abc123"), Some("abc123"));
364    }
365
366    #[test]
367    fn test_extract_bearer_uppercase_scheme() {
368        assert_eq!(extract_bearer_from_header("BEARER abc123"), Some("abc123"));
369    }
370
371    #[test]
372    fn test_extract_bearer_wrong_scheme_basic() {
373        assert_eq!(extract_bearer_from_header("Basic dXNlcjpwYXNz"), None);
374    }
375
376    #[test]
377    fn test_extract_bearer_no_scheme() {
378        assert_eq!(extract_bearer_from_header("abc123"), None);
379    }
380
381    #[test]
382    fn test_extract_bearer_empty_header() {
383        assert_eq!(extract_bearer_from_header(""), None);
384    }
385
386    #[test]
387    fn test_extract_bearer_empty_token_after_space() {
388        assert_eq!(extract_bearer_from_header("Bearer "), None);
389    }
390
391    #[test]
392    fn test_extract_bearer_double_space_rejected() {
393        assert_eq!(extract_bearer_from_header("Bearer  abc"), None);
394    }
395
396    #[test]
397    fn test_extract_bearer_tab_not_space_rejected() {
398        assert_eq!(extract_bearer_from_header("Bearer\tabc"), None);
399    }
400
401    #[test]
402    fn test_extract_bearer_mixed_case_scheme() {
403        assert_eq!(
404            extract_bearer_from_header("BeArEr mytoken"),
405            Some("mytoken")
406        );
407    }
408
409    #[test]
410    fn test_extract_bearer_token_with_dots() {
411        assert_eq!(
412            extract_bearer_from_header("Bearer eyJ0.eyJz.SflK"),
413            Some("eyJ0.eyJz.SflK")
414        );
415    }
416
417    #[test]
418    fn test_verify_bearer_both_empty() {
419        assert!(!verify_bearer_token("", ""));
420    }
421
422    #[test]
423    fn verify_matching_credentials() {
424        assert!(verify_basic_credentials("user", "pass", "user", "pass"));
425    }
426
427    #[test]
428    fn verify_wrong_username() {
429        assert!(!verify_basic_credentials("user", "pass", "xxxx", "pass"));
430    }
431
432    #[test]
433    fn verify_wrong_password() {
434        assert!(!verify_basic_credentials("user", "pass", "user", "xxxx"));
435    }
436
437    #[test]
438    fn verify_empty_provided_username() {
439        assert!(!verify_basic_credentials("", "pass", "user", "pass"));
440    }
441
442    #[test]
443    fn verify_empty_provided_password() {
444        assert!(!verify_basic_credentials("user", "", "user", "pass"));
445    }
446
447    #[test]
448    fn verify_empty_expected_username() {
449        assert!(!verify_basic_credentials("user", "pass", "", "pass"));
450    }
451
452    #[test]
453    fn verify_empty_expected_password() {
454        assert!(!verify_basic_credentials("user", "pass", "user", ""));
455    }
456
457    #[test]
458    fn verify_different_lengths_username_longer() {
459        assert!(!verify_basic_credentials("users", "pass", "user", "pass"));
460    }
461
462    #[test]
463    fn verify_different_lengths_username_shorter() {
464        assert!(!verify_basic_credentials("usr", "pass", "user", "pass"));
465    }
466
467    #[test]
468    fn verify_different_lengths_password_longer() {
469        assert!(!verify_basic_credentials("user", "passw", "user", "pass"));
470    }
471
472    #[test]
473    fn verify_different_lengths_password_shorter() {
474        assert!(!verify_basic_credentials("user", "pas", "user", "pass"));
475    }
476
477    #[test]
478    fn extract_basic_valid() {
479        assert_eq!(
480            extract_basic_from_header("Basic dXNlcjpwYXNz"),
481            Some(("user".to_string(), "pass".to_string()))
482        );
483    }
484
485    #[test]
486    fn extract_basic_mixed_case_scheme() {
487        assert_eq!(
488            extract_basic_from_header("bAsIc dXNlcjpwYXNz"),
489            Some(("user".to_string(), "pass".to_string()))
490        );
491    }
492
493    #[test]
494    fn extract_basic_wrong_scheme() {
495        assert_eq!(extract_basic_from_header("Bearer dXNlcjpwYXNz"), None);
496    }
497
498    #[test]
499    fn extract_basic_empty_header() {
500        assert_eq!(extract_basic_from_header(""), None);
501    }
502
503    #[test]
504    fn extract_basic_double_space_rejected() {
505        assert_eq!(extract_basic_from_header("Basic  dXNlcjpwYXNz"), None);
506    }
507
508    #[test]
509    fn extract_basic_invalid_base64() {
510        assert_eq!(extract_basic_from_header("Basic not-valid-base64!!!"), None);
511    }
512
513    #[test]
514    fn extract_basic_non_utf8_after_decode() {
515        use base64::Engine as _;
516        let encoded = base64::engine::general_purpose::STANDARD.encode([0xff, 0xfe]);
517        assert_eq!(extract_basic_from_header(&format!("Basic {encoded}")), None);
518    }
519
520    #[test]
521    fn extract_basic_no_colon() {
522        use base64::Engine as _;
523        let encoded = base64::engine::general_purpose::STANDARD.encode("userpass");
524        assert_eq!(extract_basic_from_header(&format!("Basic {encoded}")), None);
525    }
526
527    #[test]
528    fn extract_basic_colon_at_start() {
529        use base64::Engine as _;
530        let encoded = base64::engine::general_purpose::STANDARD.encode(":pass");
531        assert_eq!(extract_basic_from_header(&format!("Basic {encoded}")), None);
532    }
533
534    #[test]
535    fn extract_basic_colon_at_end() {
536        use base64::Engine as _;
537        let encoded = base64::engine::general_purpose::STANDARD.encode("user:");
538        assert_eq!(extract_basic_from_header(&format!("Basic {encoded}")), None);
539    }
540
541    #[test]
542    fn extract_basic_password_with_colon() {
543        use base64::Engine as _;
544        let encoded = base64::engine::general_purpose::STANDARD.encode("user:pass:word");
545        assert_eq!(
546            extract_basic_from_header(&format!("Basic {encoded}")),
547            Some(("user".to_string(), "pass:word".to_string()))
548        );
549    }
550}
551
552#[cfg(test)]
553mod authenticator_tests {
554    use super::*;
555    use crate::access::Identity;
556    use crate::testing::StubTokenVerifier;
557
558    fn headers(values: &[&str]) -> HeaderMap {
559        let mut headers = HeaderMap::new();
560        for value in values {
561            headers.append(AUTHORIZATION, HeaderValue::from_str(value).expect("header"));
562        }
563        headers
564    }
565
566    fn with_stub() -> Authenticator {
567        Authenticator::new("service")
568            .with_basic("dav-user", "dav-pass")
569            .with_token_verifier(Arc::new(StubTokenVerifier::default().accepting(
570                "jwt-alice",
571                "alice",
572                ["editors"],
573            )))
574    }
575
576    #[tokio::test]
577    async fn an_absent_header_is_anonymous() {
578        assert_eq!(
579            with_stub()
580                .resolve(&HeaderMap::new(), Schemes::Bearer)
581                .await,
582            Ok(Principal::Anyone)
583        );
584    }
585
586    #[tokio::test]
587    async fn the_service_token_wins_before_the_verifier_is_consulted() {
588        // Given — a verifier that would also accept "service" as a user.
589        let auth = Authenticator::new("service").with_token_verifier(Arc::new(
590            StubTokenVerifier::default().accepting("service", "impostor", []),
591        ));
592
593        // When / Then
594        assert_eq!(
595            auth.resolve(&headers(&["Bearer service"]), Schemes::Bearer)
596                .await,
597            Ok(Principal::service_token())
598        );
599    }
600
601    #[tokio::test]
602    async fn a_verified_identity_token_is_signed_in_as_that_user() {
603        // Given / When
604        let principal = with_stub()
605            .resolve(&headers(&["Bearer jwt-alice"]), Schemes::Bearer)
606            .await
607            .expect("verified");
608
609        // Then
610        let Principal::SignedIn(Identity::User(user)) = principal else {
611            panic!("expected a user identity, got {principal:?}");
612        };
613        assert_eq!(user.subject, "alice");
614        assert!(user.groups.contains("editors"));
615    }
616
617    #[tokio::test]
618    async fn a_bearer_that_is_neither_the_service_token_nor_verifiable_is_refused_not_downgraded() {
619        assert_eq!(
620            with_stub()
621                .resolve(&headers(&["Bearer nobody"]), Schemes::Bearer)
622                .await,
623            Err(CredentialRefused)
624        );
625    }
626
627    #[tokio::test]
628    async fn without_a_verifier_every_foreign_bearer_is_refused() {
629        assert_eq!(
630            Authenticator::new("service")
631                .resolve(&headers(&["Bearer jwt-alice"]), Schemes::Bearer)
632                .await,
633            Err(CredentialRefused)
634        );
635    }
636
637    #[tokio::test]
638    async fn two_authorization_headers_are_refused() {
639        assert_eq!(
640            with_stub()
641                .resolve(
642                    &headers(&["Bearer service", "Bearer service"]),
643                    Schemes::Bearer
644                )
645                .await,
646            Err(CredentialRefused)
647        );
648    }
649
650    #[tokio::test]
651    async fn a_basic_credential_is_refused_where_only_bearer_is_accepted() {
652        // Given — the WebDAV pair, presented to the API.
653        let basic = format!(
654            "Basic {}",
655            base64::engine::general_purpose::STANDARD.encode("dav-user:dav-pass")
656        );
657
658        // When / Then
659        assert_eq!(
660            with_stub()
661                .resolve(&headers(&[&basic]), Schemes::Bearer)
662                .await,
663            Err(CredentialRefused)
664        );
665        assert_eq!(
666            with_stub()
667                .resolve(&headers(&[&basic]), Schemes::BasicOrBearer)
668                .await,
669            Ok(Principal::service_token())
670        );
671    }
672
673    #[tokio::test]
674    async fn a_wrong_basic_credential_is_refused_and_basic_needs_a_configured_pair() {
675        let basic = format!(
676            "Basic {}",
677            base64::engine::general_purpose::STANDARD.encode("dav-user:wrong")
678        );
679        assert_eq!(
680            with_stub()
681                .resolve(&headers(&[&basic]), Schemes::BasicOrBearer)
682                .await,
683            Err(CredentialRefused)
684        );
685        assert_eq!(
686            Authenticator::new("service")
687                .resolve(&headers(&[&basic]), Schemes::BasicOrBearer)
688                .await,
689            Err(CredentialRefused)
690        );
691    }
692
693    #[tokio::test]
694    async fn a_bearer_is_accepted_on_a_basic_surface_too() {
695        assert_eq!(
696            with_stub()
697                .resolve(&headers(&["Bearer jwt-alice"]), Schemes::BasicOrBearer)
698                .await
699                .map(|p| p.is_signed_in()),
700            Ok(true)
701        );
702    }
703
704    #[test]
705    fn debug_output_never_contains_a_secret() {
706        let auth = Authenticator::new("sk-top-secret").with_basic("dav-user", "dav-pass");
707        let rendered = format!("{auth:?}");
708        assert!(!rendered.contains("sk-top-secret"), "{rendered}");
709        assert!(!rendered.contains("dav-pass"), "{rendered}");
710        assert!(rendered.contains("dav-user"), "{rendered}");
711    }
712
713    #[test]
714    fn the_challenge_names_the_metadata_document_only_when_published() {
715        // Given
716        let bare = Authenticator::new("service");
717        let published = Authenticator::new("service").with_protected_resource(ProtectedResource {
718            resource: "https://notes.example.com".into(),
719            authorization_servers: vec!["https://auth.example.com".into()],
720            metadata_url: "https://notes.example.com/.well-known/oauth-protected-resource".into(),
721        });
722
723        // When / Then
724        assert!(bare.bearer_challenge().is_none());
725        assert_eq!(
726            published.bearer_challenge().expect("challenge"),
727            "Bearer resource_metadata=\"https://notes.example.com/.well-known/oauth-protected-resource\""
728        );
729        assert_eq!(
730            published
731                .protected_resource()
732                .expect("published")
733                .document(),
734            serde_json::json!({
735                "resource": "https://notes.example.com",
736                "authorization_servers": ["https://auth.example.com"],
737                "bearer_methods_supported": ["header"],
738            })
739        );
740    }
741}