Skip to main content

protocol_droid/application/oauth/
callback.rs

1//! Handling the authorization callback: the redirect back from the browser.
2//!
3//! This is the half of the flow where the attack surface arrives. Everything
4//! the client knows about its own request — the `state`, the issuer, the PKCE
5//! verifier, the resource — was recorded before the user left; everything on
6//! this side of the redirect came from a channel the client does not control.
7//! The job here is to check the latter against the former and produce a
8//! redemption-ready value, or refuse.
9//!
10//! # The order of the checks is the point
11//!
12//! `state` is checked **first**, before the response is interpreted as success
13//! or failure. RFC 6749 §4.1.2 requires the client validate `state` on the
14//! response whatever it contains. Two consequences follow, and both are why the
15//! sequence is not incidental:
16//!
17//! - An attacker who can induce the client to follow a crafted redirect must
18//!   not be able to make it report a failure either. A forged
19//!   `error=access_denied` is a denial of service against a flow the user
20//!   believes is in progress, and reading the error before checking `state`
21//!   would let anyone with a link do it.
22//! - An attacker must not be able to make the client redeem *someone else's*
23//!   code by pairing a valid `code` with a forged `state`. The two are checked
24//!   together precisely so neither is meaningful alone.
25//!
26//! `iss` is checked next, then the response is interpreted. RFC 9207's mix-up
27//! defence only means anything if it runs before the code is accepted, and it
28//! runs against [`PendingAuthorization::issuer`] — what the client recorded on
29//! the way out — rather than against a value read from the response being
30//! validated.
31//!
32//! # What this does not do
33//!
34//! It does not redeem the code and it does not talk to the authorization
35//! server. [`AuthorizedCode`] is what a later use case takes to the token
36//! endpoint.
37
38use core::fmt;
39use std::sync::Arc;
40
41use super::client::PendingAuthorization;
42use crate::domain::mcp::authorization::AuthorizationModel;
43use crate::domain::oauth::issuer::{AuthorizationResponseError, check_response_issuer_advertised};
44use crate::domain::oauth::pkce::CodeVerifier;
45use crate::domain::oauth::registration::ClientId;
46use crate::domain::oauth::resource::ResourceBinding;
47
48/// The query parameters an authorization server redirected with.
49///
50/// Raw strings from a URL this crate did not construct. Parsed once, here, into
51/// the two shapes it can mean — a grant or a refusal — before anything acts on
52/// them.
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub struct AuthorizationCallback {
55    code: Option<Arc<str>>,
56    state: Option<Arc<str>>,
57    iss: Option<Arc<str>>,
58    error: Option<Arc<str>>,
59}
60
61impl AuthorizationCallback {
62    /// A successful response carrying `code` and `state`.
63    #[must_use]
64    pub fn granted(code: impl AsRef<str>, state: impl AsRef<str>) -> Self {
65        Self {
66            code: Some(Arc::from(code.as_ref())),
67            state: Some(Arc::from(state.as_ref())),
68            iss: None,
69            error: None,
70        }
71    }
72
73    /// A refusal carrying `error` and `state`.
74    #[must_use]
75    pub fn refused(error: impl AsRef<str>, state: impl AsRef<str>) -> Self {
76        Self {
77            code: None,
78            state: Some(Arc::from(state.as_ref())),
79            iss: None,
80            error: Some(Arc::from(error.as_ref())),
81        }
82    }
83
84    /// Add the `iss` the response carried, if any.
85    #[must_use]
86    pub fn with_issuer(mut self, iss: Option<Arc<str>>) -> Self {
87        self.iss = iss;
88        self
89    }
90
91    /// The authorization code, if the response carried one.
92    #[must_use]
93    pub fn code(&self) -> Option<&str> {
94        self.code.as_deref()
95    }
96
97    /// The `state` the response carried.
98    #[must_use]
99    pub fn state(&self) -> Option<&str> {
100        self.state.as_deref()
101    }
102
103    /// The `iss` the response carried.
104    #[must_use]
105    pub fn issuer(&self) -> Option<&str> {
106        self.iss.as_deref()
107    }
108
109    /// The error code the response carried.
110    #[must_use]
111    pub fn error(&self) -> Option<&str> {
112        self.error.as_deref()
113    }
114}
115
116/// A validated callback, ready to redeem.
117///
118/// Holds the code and everything needed to complete the token request, all of
119/// it recorded before the user left the client.
120///
121/// Deliberately **not** [`Clone`]. RFC 6749 §4.1.2 makes an authorization code
122/// single-use, and the way this crate holds that line is that redeeming
123/// consumes the value: [`redeem`](super::redeem::redeem) takes it by value, so
124/// one value cannot be spent twice. That is the scope of the guarantee, and it
125/// is worth being exact about what it does *not* cover: a caller that invokes
126/// [`authorize_callback`] twice against the same [`PendingAuthorization`]
127/// still holds two values, and may try both. What stops the second is the
128/// authorization server, which §4.1.2 requires to deny a reused code. The type
129/// closes the accidental double-spend — a retry after a dropped connection,
130/// where the first attempt may already have been honoured — and the server
131/// closes the deliberate one.
132pub struct AuthorizedCode {
133    code: Arc<str>,
134    verifier: CodeVerifier,
135    resource: ResourceBinding,
136    client_id: ClientId,
137    redirect_uri: Arc<str>,
138    issuer: Arc<str>,
139}
140
141impl AuthorizedCode {
142    /// The authorization code to present at the token endpoint.
143    #[must_use]
144    pub fn code(&self) -> &str {
145        &self.code
146    }
147
148    /// The PKCE verifier to send alongside it.
149    #[must_use]
150    pub const fn code_verifier(&self) -> &CodeVerifier {
151        &self.verifier
152    }
153
154    /// What the grant is bound to — the resource the token request must repeat.
155    #[must_use]
156    pub const fn resource(&self) -> &ResourceBinding {
157        &self.resource
158    }
159
160    /// The client identifier.
161    #[must_use]
162    pub const fn client_id(&self) -> &ClientId {
163        &self.client_id
164    }
165
166    /// The redirect URI the authorization request named, which RFC 6749 §4.1.3
167    /// requires a client to repeat if it included one.
168    #[must_use]
169    pub fn redirect_uri(&self) -> &str {
170        &self.redirect_uri
171    }
172
173    /// The issuer the code came from.
174    #[must_use]
175    pub fn issuer(&self) -> &str {
176        &self.issuer
177    }
178}
179
180impl fmt::Debug for AuthorizedCode {
181    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
182        // The code is a single-use credential redeemable exactly once, and the
183        // verifier is the secret that redeems it. Both redacted; the rest is
184        // what an operator needs to see.
185        formatter
186            .debug_struct("AuthorizedCode")
187            .field("code", &"…")
188            .field("code_verifier", &self.verifier)
189            .field("resource", &self.resource)
190            .field("client_id", &self.client_id)
191            .field("redirect_uri", &self.redirect_uri)
192            .field("issuer", &self.issuer)
193            .finish()
194    }
195}
196
197/// Why an authorization callback was refused.
198#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
199pub enum CallbackError {
200    /// The `state` did not match, or was absent.
201    ///
202    /// RFC 6749 §4.1.2 requires the client validate `state`. A mismatch means
203    /// this response is not the answer to the request this client made, which
204    /// covers a forged redirect, a stale tab, and a callback for a different
205    /// flow. The two are not distinguished: an attacker learns nothing useful
206    /// from which, and a client that reported them separately would leak the
207    /// presence of an outstanding flow.
208    #[error("the authorization response's state does not match the one this client issued")]
209    StateMismatch,
210
211    /// The response named a different authorization server.
212    #[error(transparent)]
213    Issuer(#[from] AuthorizationResponseError),
214
215    /// The authorization server refused.
216    ///
217    /// Carries the RFC 6749 §4.1.2.1 `error` code. This is the server saying no
218    /// — `access_denied`, `invalid_scope`, a user's "no" at the consent screen —
219    /// and is deliberately not folded into a generic failure, because a client
220    /// showing the user "access denied" is doing the right thing.
221    #[error("the authorization server refused the request: {error}")]
222    Refused {
223        /// The `error` parameter from the response.
224        error: Arc<str>,
225    },
226
227    /// Neither a code nor an error.
228    ///
229    /// A well-formed response carries exactly one. This is a server sending
230    /// something this crate cannot interpret, which is not the same as a
231    /// refusal and not the same as success.
232    #[error("the authorization response carried neither a code nor an error")]
233    NoOutcome,
234}
235
236/// Validate an authorization callback against the request that started it.
237///
238/// Checks, in this order and for the reasons in the module documentation:
239/// `state`, then `iss`, then whether the response carries a grant or a refusal.
240///
241/// # Errors
242///
243/// [`CallbackError::StateMismatch`] if the `state` is absent or different —
244/// checked first so that neither a forged success nor a forged refusal can be
245/// acted on. [`CallbackError::Issuer`] if a present `iss` names another server.
246/// [`CallbackError::Refused`] if the server refused, carrying its error code.
247/// [`CallbackError::NoOutcome`] if the response carries neither a code nor an
248/// error.
249///
250/// # Examples
251///
252/// ```
253/// use protocol_droid::domain::ProtocolVersion;
254/// use protocol_droid::domain::mcp::authorization::authorization_model;
255/// use protocol_droid::domain::mcp::resource_metadata::ResourceUri;
256/// use protocol_droid::domain::oauth::registration::{ClientId, RegisteredClient};
257/// use protocol_droid::domain::oauth::resource::ResourceParameters;
258/// use protocol_droid::application::oauth::client::{AuthorizationSecrets, SecretError, State, begin_authorization};
259/// use protocol_droid::application::oauth::callback::{AuthorizationCallback, authorize_callback};
260/// use protocol_droid::domain::oauth::pkce::{ChallengeMethod, CodeVerifier};
261///
262/// struct Fixed;
263/// impl AuthorizationSecrets for Fixed {
264///     fn new_state(&self) -> Result<State, SecretError> { Ok(State::new("state-value")) }
265///     fn new_code_verifier(&self) -> Result<CodeVerifier, SecretError> {
266///         CodeVerifier::new("a".repeat(43)).map_err(|_| SecretError::Rejected)
267///     }
268/// }
269///
270/// let model = authorization_model(ProtocolVersion::V2026_07_28);
271/// let client = RegisteredClient::from_metadata_document(
272///     "https://auth.example.com",
273///     ClientId::metadata_url("https://client.example.com/metadata.json")?,
274/// );
275/// let resource = ResourceUri::new("https://api.example.com/app")?;
276/// let pending = begin_authorization(
277///     &model, &client, "https://client.example.com/callback",
278///     ResourceParameters::single(&resource), &Fixed,
279/// )?;
280///
281/// let callback = AuthorizationCallback::granted("the-code", "state-value");
282/// let authorized = authorize_callback(&model, &pending, &callback)?;
283/// assert_eq!(authorized.code(), "the-code");
284/// assert!(authorize_callback(&model, &pending, &AuthorizationCallback::granted("c", "wrong")).is_err());
285/// # Ok::<(), Box<dyn std::error::Error>>(())
286/// ```
287pub fn authorize_callback(
288    model: &AuthorizationModel,
289    pending: &PendingAuthorization,
290    callback: &AuthorizationCallback,
291) -> Result<AuthorizedCode, CallbackError> {
292    // First, unconditionally, before the response is read as good or bad.
293    if !callback
294        .state()
295        .is_some_and(|state| pending.state().matches(state))
296    {
297        return Err(CallbackError::StateMismatch);
298    }
299
300    // Second, and against what the client recorded on the way out. SEP-2468
301    // makes a present `iss` mandatory to check and an absent one tolerable, and
302    // that asymmetry lives in the predicate rather than being re-decided here.
303    check_response_issuer_advertised(
304        model,
305        pending.issuer(),
306        callback.issuer(),
307        pending.iss_advertised(),
308    )?;
309
310    // Third: a response is a grant or a refusal, never both and never neither.
311    if let Some(code) = callback.code() {
312        return Ok(AuthorizedCode {
313            code: Arc::from(code),
314            verifier: pending.code_verifier().clone(),
315            resource: pending.resource().clone(),
316            client_id: pending.client_id().clone(),
317            redirect_uri: Arc::from(pending.redirect_uri()),
318            issuer: Arc::from(pending.issuer()),
319        });
320    }
321    callback
322        .error()
323        .map_or(Err(CallbackError::NoOutcome), |error| {
324            Err(CallbackError::Refused {
325                error: Arc::from(error),
326            })
327        })
328}
329
330#[cfg(test)]
331mod tests {
332    use std::assert_matches;
333
334    use super::{AuthorizationCallback, AuthorizedCode, CallbackError, authorize_callback};
335    use crate::application::oauth::client::{
336        AuthorizationSecrets, PendingAuthorization, SecretError, State, begin_authorization,
337    };
338    use crate::domain::ProtocolVersion;
339    use crate::domain::mcp::authorization::{AuthorizationModel, authorization_model};
340    use crate::domain::mcp::resource_metadata::{ResourceUri, ResourceUriError};
341    use crate::domain::oauth::pkce::{CodeVerifier, PkceError};
342    use crate::domain::oauth::registration::{ClientId, ClientIdError, RegisteredClient};
343    use crate::domain::oauth::resource::{ResourceIndicatorError, ResourceParameters};
344
345    const AS: &str = "https://auth.example.com";
346    const CLIENT_ID: &str = "https://client.example.com/metadata.json";
347    const REDIRECT: &str = "https://client.example.com/callback";
348    const RESOURCE: &str = "https://api.example.com/app";
349    const VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
350    const STATE: &str = "a-state-value";
351
352    /// Every fallible call in this module, in one type, so a test can `?`
353    /// through the lot instead of stopping at whichever error comes first.
354    #[derive(Debug)]
355    enum Failure {
356        Uri(ResourceUriError),
357        ClientId(ClientIdError),
358        Pkce(PkceError),
359        Resource(ResourceIndicatorError),
360        Begin(super::super::client::BeginError),
361        Callback(CallbackError),
362    }
363
364    /// Delegates so a failing test prints the engine's own account of what it
365    /// refused. It also reads the fields, which is why they are not dead: a
366    /// derived `Debug` does not count as a read, so without this every variant
367    /// would be reported unused.
368    impl core::fmt::Display for Failure {
369        fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
370            match self {
371                Self::Uri(error) => write!(formatter, "{error}"),
372                Self::ClientId(error) => write!(formatter, "{error}"),
373                Self::Pkce(error) => write!(formatter, "{error}"),
374                Self::Resource(error) => write!(formatter, "{error}"),
375                Self::Begin(error) => write!(formatter, "{error}"),
376                Self::Callback(error) => write!(formatter, "{error}"),
377            }
378        }
379    }
380
381    impl From<ResourceUriError> for Failure {
382        fn from(error: ResourceUriError) -> Self {
383            Self::Uri(error)
384        }
385    }
386    impl From<ClientIdError> for Failure {
387        fn from(error: ClientIdError) -> Self {
388            Self::ClientId(error)
389        }
390    }
391    impl From<PkceError> for Failure {
392        fn from(error: PkceError) -> Self {
393            Self::Pkce(error)
394        }
395    }
396    impl From<ResourceIndicatorError> for Failure {
397        fn from(error: ResourceIndicatorError) -> Self {
398            Self::Resource(error)
399        }
400    }
401    impl From<super::super::client::BeginError> for Failure {
402        fn from(error: super::super::client::BeginError) -> Self {
403            Self::Begin(error)
404        }
405    }
406    impl From<CallbackError> for Failure {
407        fn from(error: CallbackError) -> Self {
408            Self::Callback(error)
409        }
410    }
411
412    struct Fixed;
413
414    impl AuthorizationSecrets for Fixed {
415        fn new_state(&self) -> Result<State, SecretError> {
416            Ok(State::new(STATE))
417        }
418        fn new_code_verifier(&self) -> Result<CodeVerifier, SecretError> {
419            CodeVerifier::new(VERIFIER).map_err(|_| SecretError::Rejected)
420        }
421    }
422
423    const fn model() -> AuthorizationModel {
424        authorization_model(ProtocolVersion::V2026_07_28)
425    }
426
427    fn pending() -> Result<PendingAuthorization, Failure> {
428        let resource = ResourceUri::new(RESOURCE)?;
429        let client =
430            RegisteredClient::from_metadata_document(AS, ClientId::metadata_url(CLIENT_ID)?);
431        begin_authorization(
432            &model(),
433            &client,
434            REDIRECT,
435            ResourceParameters::single(&resource),
436            &Fixed,
437        )
438        .map_err(Failure::from)
439    }
440
441    #[test]
442    fn a_matching_callback_carries_the_request_forward() -> Result<(), Failure> {
443        let pending = pending()?;
444        let callback = AuthorizationCallback::granted("the-code", STATE);
445
446        let authorized = authorize_callback(&model(), &pending, &callback)?;
447
448        assert_eq!(authorized.code(), "the-code");
449        assert_eq!(authorized.code_verifier().as_str(), VERIFIER);
450        assert_eq!(authorized.redirect_uri(), REDIRECT);
451        assert_eq!(authorized.issuer(), AS);
452        assert_eq!(authorized.client_id().as_str(), CLIENT_ID);
453        Ok(())
454    }
455
456    #[test]
457    fn the_grant_stays_bound_to_the_resource_it_was_requested_for() -> Result<(), Failure> {
458        let pending = pending()?;
459        let callback = AuthorizationCallback::granted("the-code", STATE);
460
461        let authorized = authorize_callback(&model(), &pending, &callback)?;
462
463        // The binding must survive the round trip to the browser; dropping it
464        // here would mean the token request arrives without a resource and the
465        // server is free to mint one for the wrong audience.
466        let other = ResourceUri::new(RESOURCE)?;
467        assert!(
468            authorized
469                .resource()
470                .matches_token_request(&ResourceParameters::single(&other))
471        );
472        assert!(
473            !authorized
474                .resource()
475                .matches_token_request(&ResourceParameters::many([]))
476        );
477        Ok(())
478    }
479
480    #[test]
481    fn the_verifier_is_the_one_drawn_before_the_redirect_not_a_fresh_one() -> Result<(), Failure> {
482        let pending = pending()?;
483        let callback = AuthorizationCallback::granted("the-code", STATE);
484
485        let authorized = authorize_callback(&model(), &pending, &callback)?;
486
487        // Redeeming with a freshly drawn verifier would always fail S256 at the
488        // token endpoint, so the value has to be the one the challenge was
489        // derived from.
490        assert_eq!(authorized.code_verifier(), pending.code_verifier());
491        Ok(())
492    }
493
494    #[test]
495    fn a_state_from_another_flow_is_refused() -> Result<(), Failure> {
496        let pending = pending()?;
497        let callback = AuthorizationCallback::granted("the-code", "some-other-state");
498
499        assert_matches!(
500            authorize_callback(&model(), &pending, &callback),
501            Err(CallbackError::StateMismatch)
502        );
503        Ok(())
504    }
505
506    #[test]
507    fn a_missing_state_is_refused() -> Result<(), Failure> {
508        let pending = pending()?;
509        let callback = AuthorizationCallback {
510            code: Some("the-code".into()),
511            state: None,
512            iss: None,
513            error: None,
514        };
515
516        assert_matches!(
517            authorize_callback(&model(), &pending, &callback),
518            Err(CallbackError::StateMismatch)
519        );
520        Ok(())
521    }
522
523    #[test]
524    fn state_is_checked_before_a_refusal_is_reported() -> Result<(), Failure> {
525        let pending = pending()?;
526        // A forged error with a forged state. Reading the error first would
527        // let anyone with a link abort an authorization the user is in the
528        // middle of.
529        let callback = AuthorizationCallback::refused("access_denied", "not-our-state");
530
531        assert_matches!(
532            authorize_callback(&model(), &pending, &callback),
533            Err(CallbackError::StateMismatch)
534        );
535        Ok(())
536    }
537
538    #[test]
539    fn state_is_checked_before_a_grant_is_redeemed() -> Result<(), Failure> {
540        let pending = pending()?;
541        // Someone else's code paired with a guess at the state. The code is
542        // meaningless without the verifier, which never left this process.
543        let callback = AuthorizationCallback::granted("attacker-code", "guess");
544
545        assert_matches!(
546            authorize_callback(&model(), &pending, &callback),
547            Err(CallbackError::StateMismatch)
548        );
549        Ok(())
550    }
551
552    #[test]
553    fn a_response_from_another_issuer_is_refused() -> Result<(), Failure> {
554        let pending = pending()?;
555        let callback = AuthorizationCallback::granted("the-code", STATE)
556            .with_issuer(Some("https://evil.example.com".into()));
557
558        assert_matches!(
559            authorize_callback(&model(), &pending, &callback),
560            Err(CallbackError::Issuer(
561                crate::domain::oauth::issuer::AuthorizationResponseError::IssuerMismatch { .. }
562            ))
563        );
564        Ok(())
565    }
566
567    #[test]
568    fn a_response_from_the_right_issuer_is_accepted() -> Result<(), Failure> {
569        let pending = pending()?;
570        let callback =
571            AuthorizationCallback::granted("the-code", STATE).with_issuer(Some(AS.into()));
572
573        let authorized = authorize_callback(&model(), &pending, &callback)?;
574
575        assert_eq!(authorized.code(), "the-code");
576        Ok(())
577    }
578
579    #[test]
580    fn an_absent_issuer_is_tolerated() -> Result<(), Failure> {
581        let pending = pending()?;
582        // SEP-2468 makes a present `iss` mandatory to check but an absent one
583        // not an error, because a server that omits it is not mixing anything up.
584        let callback = AuthorizationCallback::granted("the-code", STATE);
585
586        assert_eq!(
587            authorize_callback(&model(), &pending, &callback)?.code(),
588            "the-code"
589        );
590        Ok(())
591    }
592
593    #[test]
594    fn a_refusal_carries_the_servers_own_error_code() -> Result<(), Failure> {
595        let pending = pending()?;
596        let callback = AuthorizationCallback::refused("access_denied", STATE);
597
598        assert_matches!(
599            authorize_callback(&model(), &pending, &callback),
600            Err(CallbackError::Refused { ref error }) if &**error == "access_denied"
601        );
602        Ok(())
603    }
604
605    #[test]
606    fn a_response_that_is_neither_grant_nor_refusal_is_refused() -> Result<(), Failure> {
607        let pending = pending()?;
608        let callback = AuthorizationCallback {
609            code: None,
610            state: Some(STATE.into()),
611            iss: None,
612            error: None,
613        };
614
615        assert_matches!(
616            authorize_callback(&model(), &pending, &callback),
617            Err(CallbackError::NoOutcome)
618        );
619        Ok(())
620    }
621
622    #[test]
623    fn a_code_alongside_an_error_is_still_a_code() -> Result<(), Failure> {
624        let pending = pending()?;
625        // A server sending both is malformed, but the conservative reading is
626        // the code path — an error alongside a grant is not a signal to fail.
627        let callback = AuthorizationCallback {
628            code: Some("the-code".into()),
629            state: Some(STATE.into()),
630            iss: None,
631            error: Some("access_denied".into()),
632        };
633
634        assert_eq!(
635            authorize_callback(&model(), &pending, &callback)?.code(),
636            "the-code"
637        );
638        Ok(())
639    }
640
641    #[test]
642    fn neither_the_code_nor_the_verifier_reaches_the_log() -> Result<(), Failure> {
643        let pending = pending()?;
644        let callback = AuthorizationCallback::granted("the-code", STATE);
645
646        let rendered = format!("{:?}", authorize_callback(&model(), &pending, &callback)?);
647
648        assert!(
649            !rendered.contains("the-code"),
650            "the code leaked: {rendered}"
651        );
652        assert!(
653            !rendered.contains(VERIFIER),
654            "the verifier leaked: {rendered}"
655        );
656        assert!(
657            rendered.contains(AS),
658            "the issuer should be readable: {rendered}"
659        );
660        assert!(
661            rendered.contains(REDIRECT),
662            "the redirect should be readable: {rendered}"
663        );
664        Ok(())
665    }
666
667    #[test]
668    fn an_authorized_code_reads_back_what_it_came_from() -> Result<(), Failure> {
669        let pending = pending()?;
670        let callback = AuthorizationCallback::granted("the-code", STATE);
671        let authorized = authorize_callback(&model(), &pending, &callback)?;
672
673        // Every field the token request needs, reachable without reaching back
674        // into the pending value or re-deriving anything.
675        let _: &AuthorizedCode = &authorized;
676        assert_eq!(authorized.code(), "the-code");
677        assert!(pending.state().matches(STATE));
678        Ok(())
679    }
680}