Skip to main content

protocol_droid/application/oauth/
redeem.rs

1//! Completing the authorization-code exchange: trading the code for a token.
2//!
3//! [`begin_authorization`](super::client::begin_authorization) sends the user
4//! somewhere, and [`authorize_callback`](super::callback::authorize_callback)
5//! validates what comes back. Neither of those produces a token, and this is
6//! the step that does.
7//!
8//! # The request is assembled, never supplied
9//!
10//! Every parameter of the token request is read off the
11//! [`AuthorizedCode`] — the value the callback step produced — rather than
12//! passed in by the caller. That is not tidiness. RFC 6749 §4.1.3 requires the
13//! `redirect_uri` to be **identical** to the one in the authorization request,
14//! and a caller that supplies it separately can supply a different one. The
15//! same goes for the `code_verifier`: presenting anything other than the value
16//! the `code_challenge` was derived from is a failed PKCE check, and the
17//! failure is the server refusing a code the client genuinely owns.
18//!
19//! # The code is consumed, so it cannot be spent twice
20//!
21//! [`redeem`] takes the [`AuthorizedCode`] by value, and `AuthorizedCode` is
22//! not [`Clone`], so one value cannot be redeemed twice. A retry after a
23//! dropped connection — the case where the first attempt may already have been
24//! honoured — therefore cannot silently spend the same code again.
25//!
26//! That is the whole of the guarantee, and it is narrower than "a code is used
27//! once". A caller that runs [`authorize_callback`](crate::application::oauth::callback::authorize_callback) twice against the same
28//! [`PendingAuthorization`](super::client::PendingAuthorization) holds two
29//! values and may try both; RFC 6749 §4.1.2 requires the authorization server
30//! to deny the second. The type closes the accident, the server closes the
31//! choice.
32
33use std::fmt;
34
35use crate::application::oauth::callback::AuthorizedCode;
36use crate::domain::oauth::issued_token::{IssuedToken, TokenResponseError};
37use crate::domain::oauth::pkce::CodeVerifier;
38use crate::domain::oauth::registration::{
39    ClientId, ClientSecret, RegisteredClient, TokenAuthMethod,
40};
41use crate::domain::oauth::resource::ResourceParameters;
42
43/// One token request, assembled from an [`AuthorizedCode`] and a registration.
44///
45/// Borrowed rather than owned because every member already exists in one of
46/// those two values and copying it would create a second copy of a credential
47/// to keep alive for no reason.
48///
49/// # Examples
50///
51/// An implementor reads what it needs and encodes it however its transport
52/// requires; the `oauth` adapter reads all of it:
53///
54/// ```
55/// # use protocol_droid::application::oauth::redeem::{TokenEndpoint, TokenEndpointError, TokenRequest};
56/// # use protocol_droid::domain::oauth::issued_token::{IssuedToken, TokenResponseError};
57/// struct Transport;
58///
59/// impl TokenEndpoint for Transport {
60///     async fn redeem(
61///         &self,
62///         request: TokenRequest<'_>,
63///     ) -> Result<IssuedToken, TokenEndpointError> {
64///         let body = format!(
65///             "grant_type=authorization_code&code={}&redirect_uri={}",
66///             request.code(),
67///             request.redirect_uri(),
68///         );
69///         println!("{body}");
70///         # Ok::<IssuedToken, TokenResponseError>(
71///             #     IssuedToken::parse(br#"{"access_token":"at","token_type":"Bearer"}"#)?
72///             # )
73///         .map_err(TokenEndpointError::Refused)
74///     }
75/// }
76/// ```
77#[derive(Debug)]
78pub struct TokenRequest<'a> {
79    code: &'a str,
80    redirect_uri: &'a str,
81    verifier: &'a CodeVerifier,
82    client_id: &'a ClientId,
83    client_secret: Option<&'a ClientSecret>,
84    resource: &'a ResourceParameters,
85    auth_method: TokenAuthMethod,
86    scope: Option<&'a str>,
87}
88
89impl<'a> TokenRequest<'a> {
90    /// The authorization code to redeem.
91    #[must_use]
92    pub const fn code(&self) -> &'a str {
93        self.code
94    }
95
96    /// The redirect URI, exactly as the authorization request carried it.
97    #[must_use]
98    pub const fn redirect_uri(&self) -> &'a str {
99        self.redirect_uri
100    }
101
102    /// The PKCE verifier the `code_challenge` was derived from.
103    #[must_use]
104    pub const fn code_verifier(&self) -> &'a CodeVerifier {
105        self.verifier
106    }
107
108    /// The client this exchange is being conducted as.
109    #[must_use]
110    pub const fn client_id(&self) -> &'a ClientId {
111        self.client_id
112    }
113
114    /// The secret, for a confidential client only.
115    ///
116    /// [`None`] for a public client — one registered through a client
117    /// identity metadata document, which has nowhere to keep a secret that is
118    /// still secret. An adapter must **omit** the member in that case rather
119    /// than sending it empty: an empty `client_secret` is a value, and RFC 6749
120    /// §2.3.1 treats the presence of the member as the statement that the
121    /// client is confidential.
122    #[must_use]
123    pub const fn client_secret(&self) -> Option<&'a ClientSecret> {
124        self.client_secret
125    }
126
127    /// How the client authenticates at the token endpoint.
128    #[must_use]
129    pub const fn auth_method(&self) -> TokenAuthMethod {
130        self.auth_method
131    }
132
133    /// The `scope` to send with the token request, if any.
134    ///
135    /// The authorization page lets a client that wants a refresh token add
136    /// `offline_access` "to the `scope` parameter of the authorization and
137    /// token requests", so the token request carries the scope the
138    /// authorization request asked for.
139    #[must_use]
140    pub const fn scope(&self) -> Option<&'a str> {
141        self.scope
142    }
143
144    /// The RFC 8707 resource indicators this grant is bound to.
145    ///
146    /// Repeated as one `resource` member per indicator, which is the encoding
147    /// §2 of that RFC defines.
148    #[must_use]
149    pub const fn resource(&self) -> &'a ResourceParameters {
150        self.resource
151    }
152}
153
154/// Asks an authorization server to trade a code for a token.
155///
156/// One method, and one because redemption is a single act. An adapter does the
157/// HTTP; this crate decides what to ask for, and the domain decides what came
158/// back.
159///
160/// Not dyn compatible, and that is deliberate. Redemption happens once per
161/// client rather than once per request, so the call site takes `impl
162/// TokenEndpoint` and the monomorphised future costs nothing that a box would
163/// not.
164pub trait TokenEndpoint {
165    /// Trade the authorization code for a token.
166    ///
167    /// # Errors
168    ///
169    /// [`TokenEndpointError::Refused`] when the server answered RFC 6749 §5.2,
170    /// or with a body that was not a usable token. [`TokenEndpointError::Unavailable`]
171    /// when the server could not be asked or its answer could not be read.
172    fn redeem(
173        &self,
174        request: TokenRequest<'_>,
175    ) -> impl Future<Output = Result<IssuedToken, TokenEndpointError>> + Send;
176
177    /// Trade a refresh token for a new access token (RFC 6749 §6).
178    ///
179    /// The default refuses, so an endpoint written before this method existed
180    /// keeps compiling and a caller finds out at runtime, as an
181    /// [`TokenEndpointError::Unavailable`], that it cannot refresh.
182    ///
183    /// # Errors
184    ///
185    /// As [`redeem`](Self::redeem). `invalid_grant` here means the refresh
186    /// token is spent or revoked, and the caller falls back to a full
187    /// authorization.
188    fn refresh(
189        &self,
190        request: RefreshRequest<'_>,
191    ) -> impl Future<Output = Result<IssuedToken, TokenEndpointError>> + Send {
192        let _ = request;
193        core::future::ready(Err(TokenEndpointError::Unavailable(
194            "this token endpoint does not support refresh tokens".to_owned(),
195        )))
196    }
197}
198
199/// One `grant_type=refresh_token` request.
200///
201/// Borrowed, like [`TokenRequest`], so a refresh token is not copied around.
202#[derive(Debug)]
203pub struct RefreshRequest<'a> {
204    refresh_token: &'a str,
205    client_id: &'a ClientId,
206    client_secret: Option<&'a ClientSecret>,
207    resource: &'a ResourceParameters,
208    auth_method: TokenAuthMethod,
209    scope: Option<&'a str>,
210}
211
212impl<'a> RefreshRequest<'a> {
213    /// A refresh of `refresh_token` as `client`, for the resource the grant
214    /// was bound to.
215    #[must_use]
216    pub const fn new(
217        refresh_token: &'a str,
218        client: &'a RegisteredClient,
219        resource: &'a ResourceParameters,
220        scope: Option<&'a str>,
221    ) -> Self {
222        Self {
223            refresh_token,
224            client_id: client.client_id(),
225            client_secret: client.secret(),
226            resource,
227            auth_method: client.auth_method(),
228            scope,
229        }
230    }
231
232    /// The refresh token being redeemed.
233    #[must_use]
234    pub const fn refresh_token(&self) -> &'a str {
235        self.refresh_token
236    }
237
238    /// The client the refresh is made as.
239    #[must_use]
240    pub const fn client_id(&self) -> &'a ClientId {
241        self.client_id
242    }
243
244    /// The secret, for a confidential client only; omitted otherwise.
245    #[must_use]
246    pub const fn client_secret(&self) -> Option<&'a ClientSecret> {
247        self.client_secret
248    }
249
250    /// How the client authenticates at the token endpoint.
251    #[must_use]
252    pub const fn auth_method(&self) -> TokenAuthMethod {
253        self.auth_method
254    }
255
256    /// The RFC 8707 resource indicators the grant is bound to.
257    #[must_use]
258    pub const fn resource(&self) -> &'a ResourceParameters {
259        self.resource
260    }
261
262    /// The scope to ask for, if narrower than the original grant.
263    #[must_use]
264    pub const fn scope(&self) -> Option<&'a str> {
265        self.scope
266    }
267}
268
269/// Why a redemption did not produce a token.
270///
271/// `#[error]` text never carries the code, the verifier or the secret. This is
272/// the first place any of those three could reach a message somebody pastes
273/// into a bug report.
274#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
275pub enum TokenEndpointError {
276    /// The server refused, or answered with something unusable.
277    #[error(transparent)]
278    Refused(#[from] TokenResponseError),
279
280    /// The server could not be asked, or its answer could not be read.
281    #[error("the token endpoint could not be reached: {0}")]
282    Unavailable(String),
283}
284
285/// Why [`redeem`] did not produce a token.
286#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
287pub enum RedeemError {
288    /// The registration presented does not match the client that started the
289    /// exchange.
290    ///
291    /// [`redeem`] compares rather than trusting, because the client id is a
292    /// parameter the adapter is about to send: a caller that reached for the
293    /// wrong registration would otherwise produce a token request naming one
294    /// client with a `code_verifier` generated for another, and the server's
295    /// answer would be an `invalid_grant` with nothing to explain it.
296    #[error("the registration is for {presented}, but this code was issued to {expected}")]
297    ClientMismatch {
298        /// The client the code was issued to.
299        expected: ClientId,
300        /// The client the caller presented.
301        presented: ClientId,
302    },
303
304    /// The endpoint refused, or could not be reached.
305    #[error(transparent)]
306    Endpoint(#[from] TokenEndpointError),
307}
308
309/// Trade a validated authorization code for a token.
310///
311/// The code is consumed. See the module docs for why that is the single-use
312/// rule rather than a convention.
313///
314/// # Errors
315///
316/// [`RedeemError::ClientMismatch`] when the registration is not the client the
317/// code was issued to. [`RedeemError::Endpoint`] when the endpoint refused or
318/// could not be reached; that carries RFC 6749 §5.2's own error code when the
319/// server named one.
320///
321/// # Examples
322///
323/// ```
324/// # use protocol_droid::application::oauth::redeem::{RedeemError, redeem};
325/// # use protocol_droid::domain::oauth::registration::{ClientId, RegisteredClient};
326/// # async fn run(
327/// #     code: protocol_droid::application::oauth::callback::AuthorizedCode,
328/// #     client: RegisteredClient,
329/// #     endpoint: &(impl protocol_droid::application::oauth::redeem::TokenEndpoint + Sync),
330/// # ) -> Result<(), RedeemError> {
331/// let issued = redeem(code, &client, endpoint).await?;
332/// println!("scope granted: {:?}", issued.scope());
333/// # Ok(())
334/// # }
335/// ```
336pub async fn redeem(
337    code: AuthorizedCode,
338    client: &RegisteredClient,
339    endpoint: &(impl TokenEndpoint + Sync),
340) -> Result<IssuedToken, RedeemError> {
341    redeem_with_scope(code, client, endpoint, None).await
342}
343
344/// [`redeem`], sending `scope` with the token request.
345///
346/// # Errors
347///
348/// As [`redeem`].
349pub async fn redeem_with_scope(
350    code: AuthorizedCode,
351    client: &RegisteredClient,
352    endpoint: &(impl TokenEndpoint + Sync),
353    scope: Option<&str>,
354) -> Result<IssuedToken, RedeemError> {
355    if code.client_id() != client.client_id() {
356        return Err(RedeemError::ClientMismatch {
357            expected: code.client_id().clone(),
358            presented: client.client_id().clone(),
359        });
360    }
361
362    let request = TokenRequest {
363        code: code.code(),
364        // Read back off the code rather than taken from the registration: the
365        // registration records where the client is willing to be redirected
366        // *now*, and §4.1.3 asks for where this request was actually sent.
367        redirect_uri: code.redirect_uri(),
368        verifier: code.code_verifier(),
369        client_id: code.client_id(),
370        client_secret: client.secret(),
371        resource: code.resource().token_request(),
372        auth_method: client.auth_method(),
373        scope,
374    };
375
376    endpoint
377        .redeem(request)
378        .await
379        .map_err(RedeemError::Endpoint)
380}
381
382/// Rendered only so a `TokenRequest` in a log line names its fields without
383/// their values.
384impl fmt::Display for TokenRequest<'_> {
385    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
386        write!(
387            formatter,
388            "TokenRequest {{ grant_type: authorization_code, code: …, redirect_uri: {}, \
389             code_verifier: …, client_id: {}, client_secret: {}, resource: [{}] }}",
390            self.redirect_uri,
391            self.client_id,
392            self.client_secret.is_some(),
393            self.resource.iter().collect::<Vec<_>>().join(", "),
394        )
395    }
396}
397#[cfg(test)]
398mod tests {
399    use std::assert_matches;
400    use std::future::Future;
401    use std::sync::Mutex;
402    use std::task::{Context, Poll, Waker};
403
404    use super::{RedeemError, TokenEndpoint, TokenEndpointError, TokenRequest, redeem};
405    use crate::application::oauth::callback::{
406        AuthorizationCallback, AuthorizedCode, authorize_callback,
407    };
408    use crate::application::oauth::client::{
409        AuthorizationSecrets, PendingAuthorization, SecretError, State, begin_authorization,
410    };
411    use crate::domain::ProtocolVersion;
412    use crate::domain::mcp::authorization::{AuthorizationModel, authorization_model};
413    use crate::domain::mcp::resource_metadata::ResourceUri;
414    use crate::domain::oauth::issued_token::{IssuedToken, TokenResponseError};
415    use crate::domain::oauth::pkce::CodeVerifier;
416    use crate::domain::oauth::registration::{ClientId, ClientSecret, RegisteredClient};
417    use crate::domain::oauth::resource::ResourceParameters;
418
419    const AS: &str = "https://auth.example.com";
420    const CLIENT_ID: &str = "https://client.example.com/metadata.json";
421    const REDIRECT: &str = "https://client.example.com/callback";
422    const RESOURCE: &str = "https://api.example.com/app";
423    const VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
424    const STATE: &str = "a-state-value";
425    const GRANTED: &[u8] = br#"{"access_token":"at-1","token_type":"Bearer","expires_in":3600}"#;
426
427    /// Every error in play implements `std::error::Error`, so a test can `?`
428    /// through the lot and print whatever the engine refused with, rather than
429    /// unwrapping the first call that happens to be fallible.
430    type Outcome = Result<(), Box<dyn std::error::Error>>;
431
432    /// Drives a future that is expected never to pend.
433    ///
434    /// `#[tokio::test]` is not available under `oauth`: the tokio macros come in
435    /// with `http`, and pulling a runtime in to test fakes that return
436    /// immediately would be a heavier dependency than the thing under test.
437    /// `Waker::noop` is correct for a future that resolves on its first poll,
438    /// and returning `None` on `Pending` is what makes that assumption
439    /// checkable rather than merely stated.
440    fn now<T>(future: impl Future<Output = T>) -> Option<T> {
441        let mut future = std::pin::pin!(future);
442        match future
443            .as_mut()
444            .poll(&mut Context::from_waker(Waker::noop()))
445        {
446            Poll::Ready(output) => Some(output),
447            Poll::Pending => None,
448        }
449    }
450
451    struct Fixed;
452
453    impl AuthorizationSecrets for Fixed {
454        fn new_state(&self) -> Result<State, SecretError> {
455            Ok(State::new(STATE))
456        }
457        fn new_code_verifier(&self) -> Result<CodeVerifier, SecretError> {
458            CodeVerifier::new(VERIFIER).map_err(|_| SecretError::Rejected)
459        }
460    }
461
462    const fn model() -> AuthorizationModel {
463        authorization_model(ProtocolVersion::V2026_07_28)
464    }
465
466    /// A client registered through a metadata document, so public: no secret.
467    fn public_client() -> Result<RegisteredClient, Box<dyn std::error::Error>> {
468        Ok(RegisteredClient::from_metadata_document(
469            AS,
470            ClientId::metadata_url(CLIENT_ID)?,
471        ))
472    }
473
474    fn confidential_client() -> Result<RegisteredClient, Box<dyn std::error::Error>> {
475        Ok(RegisteredClient::dynamic(
476            AS,
477            ClientId::opaque("client-1"),
478            Some(ClientSecret::issued("s3cret")),
479        )?)
480    }
481
482    /// A validated code for `client`, made through the real path so the values
483    /// in it are the ones a live flow would carry.
484    fn authorized(client: &RegisteredClient) -> Result<AuthorizedCode, Box<dyn std::error::Error>> {
485        let resource = ResourceUri::new(RESOURCE)?;
486        let pending: PendingAuthorization = begin_authorization(
487            &model(),
488            client,
489            REDIRECT,
490            ResourceParameters::single(&resource),
491            &Fixed,
492        )?;
493        let callback = AuthorizationCallback::granted("the-code", STATE);
494        Ok(authorize_callback(&model(), &pending, &callback)?)
495    }
496
497    /// What the fake saw, copied out so it can outlive the borrow the
498    /// `TokenRequest` held.
499    #[derive(Debug)]
500    struct Recorded {
501        code: String,
502        redirect_uri: String,
503        verifier: String,
504        client_id: String,
505        client_secret: Option<String>,
506        resources: Vec<String>,
507    }
508
509    enum Outcome_ {
510        Granted,
511        Refused(TokenResponseError),
512        Unreachable,
513    }
514
515    struct Recording {
516        recorded: Mutex<Option<Recorded>>,
517        outcome: Outcome_,
518    }
519
520    impl Recording {
521        fn granting() -> Self {
522            Self {
523                recorded: Mutex::new(None),
524                outcome: Outcome_::Granted,
525            }
526        }
527
528        fn refusing(error: TokenResponseError) -> Self {
529            Self {
530                recorded: Mutex::new(None),
531                outcome: Outcome_::Refused(error),
532            }
533        }
534
535        fn unreachable() -> Self {
536            Self {
537                recorded: Mutex::new(None),
538                outcome: Outcome_::Unreachable,
539            }
540        }
541
542        fn seen(&self) -> Option<Recorded> {
543            self.recorded.lock().ok().and_then(|mut guard| guard.take())
544        }
545    }
546
547    impl TokenEndpoint for Recording {
548        // Not `async`: this fake resolves on its first poll, so an `async fn`
549        // here would be an async block with no await point in it — which is a
550        // `ready` wearing a hat.
551        fn redeem(
552            &self,
553            request: TokenRequest<'_>,
554        ) -> impl Future<Output = Result<IssuedToken, TokenEndpointError>> + Send {
555            let recorded = Recorded {
556                code: request.code().to_owned(),
557                redirect_uri: request.redirect_uri().to_owned(),
558                verifier: request.code_verifier().as_str().to_owned(),
559                client_id: request.client_id().as_str().to_owned(),
560                client_secret: request
561                    .client_secret()
562                    .map(|secret| secret.expose().to_owned()),
563                resources: request.resource().iter().map(str::to_owned).collect(),
564            };
565            if let Ok(mut guard) = self.recorded.lock() {
566                *guard = Some(recorded);
567            }
568
569            std::future::ready(match &self.outcome {
570                Outcome_::Granted => {
571                    IssuedToken::parse(GRANTED).map_err(TokenEndpointError::Refused)
572                }
573                Outcome_::Refused(error) => Err(TokenEndpointError::Refused(error.clone())),
574                Outcome_::Unreachable => Err(TokenEndpointError::Unavailable(
575                    "no route to host".to_owned(),
576                )),
577            })
578        }
579    }
580
581    /// A redeemed grant is the thing every downstream caller wants, so the
582    /// happy path gets a test that says so rather than one that only checks it
583    /// did not error.
584    #[test]
585    fn a_valid_code_becomes_a_token() -> Outcome {
586        let client = public_client()?;
587        let endpoint = Recording::granting();
588
589        let issued =
590            now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;
591
592        assert_eq!(issued.access_token().as_str(), "at-1");
593        Ok(())
594    }
595
596    /// §4.1.3 asks for a `redirect_uri` *identical* to the authorization
597    /// request's, and the verifier the challenge was derived from. Both come
598    /// off the code rather than off the caller for exactly that reason.
599    #[test]
600    fn the_request_is_assembled_from_the_code_not_the_caller() -> Outcome {
601        let client = public_client()?;
602        let endpoint = Recording::granting();
603
604        now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;
605
606        let seen = endpoint.seen().ok_or("the endpoint was never asked")?;
607        assert_eq!(seen.code, "the-code");
608        assert_eq!(seen.redirect_uri, REDIRECT);
609        assert_eq!(seen.verifier, VERIFIER);
610        assert_eq!(seen.client_id, CLIENT_ID);
611        Ok(())
612    }
613
614    /// RFC 8707 §2. Dropping the indicator here would let the server mint a
615    /// token for an audience the client never asked for.
616    #[test]
617    fn the_resource_binding_is_carried_to_the_token_request() -> Outcome {
618        let client = public_client()?;
619        let endpoint = Recording::granting();
620
621        now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;
622
623        let seen = endpoint.seen().ok_or("the endpoint was never asked")?;
624        assert_eq!(seen.resources, vec![RESOURCE.to_owned()]);
625        Ok(())
626    }
627
628    /// A public client has no secret, and §2.3.1 reads the member's *presence*
629    /// as a claim of confidentiality — so the adapter must be able to omit it.
630    #[test]
631    fn a_public_client_sends_no_secret() -> Outcome {
632        let client = public_client()?;
633        let endpoint = Recording::granting();
634
635        now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;
636
637        let seen = endpoint.seen().ok_or("the endpoint was never asked")?;
638        assert_eq!(seen.client_secret, None);
639        Ok(())
640    }
641
642    #[test]
643    fn a_confidential_client_sends_its_secret() -> Outcome {
644        let client = confidential_client()?;
645        let endpoint = Recording::granting();
646
647        now(redeem(authorized(&client)?, &client, &endpoint)).ok_or("the future pended")??;
648
649        let seen = endpoint.seen().ok_or("the endpoint was never asked")?;
650        assert_eq!(seen.client_secret.as_deref(), Some("s3cret"));
651        Ok(())
652    }
653
654    /// The check that stops a caller reaching for the wrong registration. The
655    /// alternative is a token request naming one client with a verifier
656    /// generated for another, and the server's `invalid_grant` explains
657    /// nothing about which of the two was wrong.
658    #[test]
659    fn a_registration_for_another_client_is_refused_before_the_request() -> Outcome {
660        let issued_for = confidential_client()?;
661        let endpoint = Recording::granting();
662
663        let outcome = now(redeem(
664            authorized(&issued_for)?,
665            &public_client()?,
666            &endpoint,
667        ));
668
669        assert_matches!(outcome, Some(Err(RedeemError::ClientMismatch { .. })));
670        assert!(
671            endpoint.seen().is_none(),
672            "a mismatched code was sent anyway"
673        );
674        Ok(())
675    }
676
677    /// §5.2's code reaches the caller, because `invalid_grant` and
678    /// `invalid_client` call for different responses and a bare message does not
679    /// distinguish them.
680    #[test]
681    fn the_servers_own_refusal_reaches_the_caller() -> Outcome {
682        let client = public_client()?;
683        let endpoint = Recording::refusing(TokenResponseError::AuthorizationServer {
684            error: "invalid_grant".to_owned(),
685            description: None,
686        });
687
688        let outcome = now(redeem(authorized(&client)?, &client, &endpoint));
689
690        assert_matches!(
691            outcome,
692            Some(Err(RedeemError::Endpoint(TokenEndpointError::Refused(
693                TokenResponseError::AuthorizationServer { error, ..
694            })))) if error == "invalid_grant"
695        );
696        Ok(())
697    }
698
699    /// An endpoint that could not be reached is not the same as an endpoint
700    /// that refused: the first is worth retrying later, the second is not.
701    #[test]
702    fn an_unreachable_endpoint_is_not_reported_as_a_refusal() -> Outcome {
703        let client = public_client()?;
704        let endpoint = Recording::unreachable();
705
706        let outcome = now(redeem(authorized(&client)?, &client, &endpoint));
707
708        assert_matches!(
709            outcome,
710            Some(Err(RedeemError::Endpoint(TokenEndpointError::Unavailable(
711                _
712            ))))
713        );
714        Ok(())
715    }
716
717    /// No error on any path carries the code, the verifier or the secret.
718    /// These are the three values a pasted bug report would leak.
719    #[test]
720    fn no_error_text_carries_a_credential() -> Outcome {
721        let client = confidential_client()?;
722        let endpoint = Recording::refusing(TokenResponseError::AuthorizationServer {
723            error: "invalid_grant".to_owned(),
724            description: None,
725        });
726
727        let outcome = now(redeem(authorized(&client)?, &client, &endpoint));
728
729        let rendered = match outcome {
730            Some(Err(error)) => error.to_string(),
731            _ => String::from("no error was produced"),
732        };
733        // Indexed rather than named: this assertion exists because the values
734        // must *not* appear in the error text, so echoing one in the failure
735        // message would put it in the CI log in exactly the circumstance the
736        // test was written to prevent. The index says which of the three went
737        // wrong, which is all a reader needs to look at the code.
738        for (index, secret) in ["the-code", VERIFIER, "s3cret"].iter().enumerate() {
739            assert!(
740                !rendered.contains(*secret),
741                "credential {index} leaked into the error text: {rendered}"
742            );
743        }
744        Ok(())
745    }
746}