1use 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#[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 #[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 #[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 #[must_use]
86 pub fn with_issuer(mut self, iss: Option<Arc<str>>) -> Self {
87 self.iss = iss;
88 self
89 }
90
91 #[must_use]
93 pub fn code(&self) -> Option<&str> {
94 self.code.as_deref()
95 }
96
97 #[must_use]
99 pub fn state(&self) -> Option<&str> {
100 self.state.as_deref()
101 }
102
103 #[must_use]
105 pub fn issuer(&self) -> Option<&str> {
106 self.iss.as_deref()
107 }
108
109 #[must_use]
111 pub fn error(&self) -> Option<&str> {
112 self.error.as_deref()
113 }
114}
115
116pub 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 #[must_use]
144 pub fn code(&self) -> &str {
145 &self.code
146 }
147
148 #[must_use]
150 pub const fn code_verifier(&self) -> &CodeVerifier {
151 &self.verifier
152 }
153
154 #[must_use]
156 pub const fn resource(&self) -> &ResourceBinding {
157 &self.resource
158 }
159
160 #[must_use]
162 pub const fn client_id(&self) -> &ClientId {
163 &self.client_id
164 }
165
166 #[must_use]
169 pub fn redirect_uri(&self) -> &str {
170 &self.redirect_uri
171 }
172
173 #[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 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
199pub enum CallbackError {
200 #[error("the authorization response's state does not match the one this client issued")]
209 StateMismatch,
210
211 #[error(transparent)]
213 Issuer(#[from] AuthorizationResponseError),
214
215 #[error("the authorization server refused the request: {error}")]
222 Refused {
223 error: Arc<str>,
225 },
226
227 #[error("the authorization response carried neither a code nor an error")]
233 NoOutcome,
234}
235
236pub fn authorize_callback(
288 model: &AuthorizationModel,
289 pending: &PendingAuthorization,
290 callback: &AuthorizationCallback,
291) -> Result<AuthorizedCode, CallbackError> {
292 if !callback
294 .state()
295 .is_some_and(|state| pending.state().matches(state))
296 {
297 return Err(CallbackError::StateMismatch);
298 }
299
300 check_response_issuer_advertised(
304 model,
305 pending.issuer(),
306 callback.issuer(),
307 pending.iss_advertised(),
308 )?;
309
310 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 #[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 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 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 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 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 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 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 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 let _: &AuthorizedCode = &authorized;
676 assert_eq!(authorized.code(), "the-code");
677 assert!(pending.state().matches(STATE));
678 Ok(())
679 }
680}