Skip to main content

termivar_scanner/web_runtime/supplied_session_runtime/
oauth_code.rs

1//! Private imported-code acquisition, before the existing Bearer health loop.
2//!
3//! The external authorization, its S256 selection and user-agent binding are
4//! operator assertions. A successful redemption is not OIDC identity validation
5//! or proof that this client initiated/secured the front-channel transaction.
6
7use std::time::Duration;
8
9use serde::Serialize;
10use termivar_core::{Evidence, EvidenceId, EvidenceKind};
11
12use super::{
13    commit_value_safe_evidence, DispatchBudget, RuntimeExecution, SharedWebRuntimeAuthority,
14    SuppliedSessionAuditOutcome, SuppliedSessionBodyState, SuppliedSessionCredential,
15    SuppliedSessionPolicy, SuppliedSessionPolicyVersion, RESOURCE_COMMIT_EVIDENCE_NAMESPACE,
16};
17use crate::{
18    http_evidence::{OAuthCodeRequestBroker, OAuthCodeRequestDescriptor, OAuthCodeRequestError},
19    supplied_session_review::{oauth_code::parse_token_response, SuppliedSessionOAuthGrant},
20    DecisionExecutionLimits, KnowledgeBase,
21};
22
23#[cfg(test)]
24#[path = "oauth_code_tests.rs"]
25pub(super) mod tests;
26
27const ACQUISITION_PREDICATE: &str = "oauth-code-credential-acquisition-committed";
28
29/// Closed result of the single selected IdP operation, separate from health.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "snake_case")]
32#[non_exhaustive]
33pub enum SuppliedSessionOAuthCodeOutcome {
34    NotDispatched,
35    Cancelled,
36    RuntimeLimit,
37    GrantUnavailable,
38    TransportFailed,
39    Throttled,
40    ResponseRejected,
41    CommitFailed,
42    CredentialCommitted,
43}
44
45impl SuppliedSessionOAuthCodeOutcome {
46    pub const fn as_str(self) -> &'static str {
47        match self {
48            Self::NotDispatched => "not_dispatched",
49            Self::Cancelled => "cancelled",
50            Self::RuntimeLimit => "runtime_limit",
51            Self::GrantUnavailable => "grant_unavailable",
52            Self::TransportFailed => "transport_failed",
53            Self::Throttled => "throttled",
54            Self::ResponseRejected => "response_rejected",
55            Self::CommitFailed => "commit_failed",
56            Self::CredentialCommitted => "credential_committed",
57        }
58    }
59}
60
61/// Value-free V5 acquisition facts. It is not an executable IdP permit.
62#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
63pub struct SuppliedSessionOAuthCodeAudit {
64    profile: &'static str,
65    transport_profile: String,
66    idp_reference: String,
67    transaction_context_assurance: &'static str,
68    pkce_assurance: &'static str,
69    token_interpretation: &'static str,
70    authorization_initiated: bool,
71    browser_callback_received: bool,
72    state_nonce_verified: bool,
73    oidc_identity_validation_performed: bool,
74    token_signature_validation_performed: bool,
75    request_attempt_count: u8,
76    request_admitted_count: u8,
77    response_completed_count: u8,
78    credential_commit_count: u8,
79    verifier_in_admitted_request: bool,
80    status: Option<u16>,
81    body_state: SuppliedSessionBodyState,
82    response_bytes: u64,
83    configured_token_response_byte_limit: u64,
84    token_response_validated: bool,
85    response_scope_matched: Option<bool>,
86    declared_token_lifetime_seconds: Option<u64>,
87    refresh_token_returned: Option<bool>,
88    evidence_reference: Option<String>,
89    outcome: SuppliedSessionOAuthCodeOutcome,
90}
91
92impl SuppliedSessionOAuthCodeAudit {
93    pub(super) fn selected(policy: &SuppliedSessionPolicy) -> Option<Self> {
94        let idp = policy.oauth_code_policy()?;
95        Some(Self {
96            profile: "external-public-code-s256-bearer/v1",
97            transport_profile: idp.transport_profile().to_owned(),
98            idp_reference: idp.binding_reference().to_owned(),
99            transaction_context_assurance: "operator_declared_external_transaction",
100            pkce_assurance: "imported_s256_context_not_remote_enforcement",
101            token_interpretation: "opaque_bearer_not_oidc_identity",
102            authorization_initiated: false,
103            browser_callback_received: false,
104            state_nonce_verified: false,
105            oidc_identity_validation_performed: false,
106            token_signature_validation_performed: false,
107            request_attempt_count: 0,
108            request_admitted_count: 0,
109            response_completed_count: 0,
110            credential_commit_count: 0,
111            verifier_in_admitted_request: false,
112            status: None,
113            body_state: SuppliedSessionBodyState::Unavailable,
114            response_bytes: 0,
115            configured_token_response_byte_limit: u64::try_from(idp.max_token_response_bytes())
116                .unwrap_or(u64::MAX),
117            token_response_validated: false,
118            response_scope_matched: None,
119            declared_token_lifetime_seconds: None,
120            refresh_token_returned: None,
121            evidence_reference: None,
122            outcome: SuppliedSessionOAuthCodeOutcome::NotDispatched,
123        })
124    }
125
126    pub fn idp_reference(&self) -> &str {
127        &self.idp_reference
128    }
129    pub const fn outcome(&self) -> SuppliedSessionOAuthCodeOutcome {
130        self.outcome
131    }
132    pub const fn request_attempt_count(&self) -> u8 {
133        self.request_attempt_count
134    }
135    pub const fn request_admitted_count(&self) -> u8 {
136        self.request_admitted_count
137    }
138    pub const fn response_completed_count(&self) -> u8 {
139        self.response_completed_count
140    }
141    pub const fn credential_commit_count(&self) -> u8 {
142        self.credential_commit_count
143    }
144    pub const fn response_bytes(&self) -> u64 {
145        self.response_bytes
146    }
147    pub const fn configured_token_response_byte_limit(&self) -> u64 {
148        self.configured_token_response_byte_limit
149    }
150    pub const fn declared_token_lifetime_seconds(&self) -> Option<u64> {
151        self.declared_token_lifetime_seconds
152    }
153    pub fn evidence_reference(&self) -> Option<&str> {
154        self.evidence_reference.as_deref()
155    }
156
157    pub(crate) fn is_consistent(&self) -> bool {
158        let digest = |value: &str, prefix: &str| {
159            value.strip_prefix(prefix).is_some_and(|rest| {
160                rest.len() == 64
161                    && rest
162                        .bytes()
163                        .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
164            })
165        };
166        let complete = self.body_state == SuppliedSessionBodyState::Complete;
167        let has_response = self.body_state != SuppliedSessionBodyState::Unavailable;
168        let valid_token_facts = self
169            .declared_token_lifetime_seconds
170            .is_some_and(|seconds| (1..=3_600).contains(&seconds))
171            && self.response_scope_matched == Some(true)
172            && self.refresh_token_returned.is_some();
173        if self.profile != "external-public-code-s256-bearer/v1"
174            || !matches!(
175                self.transport_profile.as_str(),
176                "verified-https/v1" | "owned-numeric-loopback-http/v1"
177            )
178            || !digest(&self.idp_reference, "oauth-code-policy-sha256:")
179            || self.transaction_context_assurance != "operator_declared_external_transaction"
180            || self.pkce_assurance != "imported_s256_context_not_remote_enforcement"
181            || self.token_interpretation != "opaque_bearer_not_oidc_identity"
182            || self.authorization_initiated
183            || self.browser_callback_received
184            || self.state_nonce_verified
185            || self.oidc_identity_validation_performed
186            || self.token_signature_validation_performed
187            || self.request_attempt_count > 1
188            || self.request_admitted_count > self.request_attempt_count
189            || self.response_completed_count > self.request_admitted_count
190            || self.credential_commit_count > u8::from(self.token_response_validated)
191            || self.verifier_in_admitted_request != (self.request_admitted_count == 1)
192            || self.response_completed_count != u8::from(complete)
193            || self.status.is_some() != has_response
194            || self
195                .status
196                .is_some_and(|status| !(100..=599).contains(&status))
197            || (has_response || self.response_bytes > 0) && self.request_admitted_count != 1
198            || !(1..=32_768).contains(&self.configured_token_response_byte_limit)
199            || self.token_response_validated != valid_token_facts
200            || (!self.token_response_validated
201                && (self.declared_token_lifetime_seconds.is_some()
202                    || self.response_scope_matched.is_some()
203                    || self.refresh_token_returned.is_some()))
204            || self.token_response_validated
205                && (!complete
206                    || self.status != Some(200)
207                    || self.response_bytes == 0
208                    || self.response_bytes > self.configured_token_response_byte_limit)
209            || (self.credential_commit_count == 1) != self.evidence_reference.is_some()
210            || self.evidence_reference.as_deref().is_some_and(|reference| {
211                !digest(reference, "supplied-session-oauth-code-evidence-sha256:")
212            })
213        {
214            return false;
215        }
216        match self.outcome {
217            SuppliedSessionOAuthCodeOutcome::NotDispatched => {
218                self.request_attempt_count == 0
219                    && !has_response
220                    && self.response_bytes == 0
221                    && !self.token_response_validated
222            },
223            SuppliedSessionOAuthCodeOutcome::Cancelled
224            | SuppliedSessionOAuthCodeOutcome::RuntimeLimit => {
225                !self.token_response_validated && self.credential_commit_count == 0
226            },
227            SuppliedSessionOAuthCodeOutcome::GrantUnavailable => {
228                self.request_attempt_count == 1
229                    && self.request_admitted_count == 0
230                    && !has_response
231                    && self.response_bytes == 0
232            },
233            SuppliedSessionOAuthCodeOutcome::TransportFailed => {
234                self.request_attempt_count == 1 && !has_response && !self.token_response_validated
235            },
236            SuppliedSessionOAuthCodeOutcome::Throttled => {
237                self.request_admitted_count == 1
238                    && self.status == Some(429)
239                    && !self.token_response_validated
240            },
241            SuppliedSessionOAuthCodeOutcome::ResponseRejected => {
242                self.request_admitted_count == 1 && has_response && !self.token_response_validated
243            },
244            SuppliedSessionOAuthCodeOutcome::CommitFailed => {
245                self.request_admitted_count == 1
246                    && self.token_response_validated
247                    && self.credential_commit_count == 0
248            },
249            SuppliedSessionOAuthCodeOutcome::CredentialCommitted => {
250                self.request_admitted_count == 1
251                    && self.token_response_validated
252                    && self.credential_commit_count == 1
253            },
254        }
255    }
256}
257
258/// V7 exposes only token-exchange facts, without relabelling the historical
259/// external-transaction audit or asserting OIDC nonce/signature validation.
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct SuppliedSessionOAuthInitiatedTokenAudit {
262    inner: SuppliedSessionOAuthCodeAudit,
263}
264
265impl SuppliedSessionOAuthInitiatedTokenAudit {
266    pub(super) fn from_exchange(inner: SuppliedSessionOAuthCodeAudit) -> Self {
267        Self { inner }
268    }
269    pub fn idp_reference(&self) -> &str {
270        self.inner.idp_reference()
271    }
272    pub const fn request_attempt_count(&self) -> u8 {
273        self.inner.request_attempt_count()
274    }
275    pub const fn request_admitted_count(&self) -> u8 {
276        self.inner.request_admitted_count()
277    }
278    pub const fn credential_commit_count(&self) -> u8 {
279        self.inner.credential_commit_count()
280    }
281    pub const fn response_bytes(&self) -> u64 {
282        self.inner.response_bytes()
283    }
284    pub const fn outcome(&self) -> SuppliedSessionOAuthCodeOutcome {
285        self.inner.outcome()
286    }
287    pub(crate) fn is_consistent(&self) -> bool {
288        self.inner.is_consistent()
289    }
290
291    pub(crate) fn is_consistent_for(
292        &self,
293        audit: &super::WebAssessmentSuppliedSessionAudit,
294        authorization: &super::SuppliedSessionOAuthAuthorizationAudit,
295    ) -> bool {
296        use super::SuppliedSessionOAuthAuthorizationOutcome as A;
297        let a = &self.inner;
298        if !a.is_consistent()
299            || !authorization.is_consistent()
300            || a.configured_token_response_byte_limit() > audit.response_byte_limit()
301            || (a.request_attempt_count() > 0 && authorization.callback_commit_count() != 1)
302        {
303            return false;
304        }
305        if a.credential_commit_count() == 1 {
306            authorization.callback_commit_count() == 1
307                && matches!(
308                    audit.outcome(),
309                    SuppliedSessionAuditOutcome::Complete
310                        | SuppliedSessionAuditOutcome::StartupUnhealthy
311                        | SuppliedSessionAuditOutcome::SessionLost
312                        | SuppliedSessionAuditOutcome::ResourceUnavailable
313                        | SuppliedSessionAuditOutcome::RuntimeLimit
314                        | SuppliedSessionAuditOutcome::Cancelled
315                )
316        } else {
317            audit.checkpoints().is_empty()
318                && audit
319                    .resources()
320                    .iter()
321                    .all(|r| r.outcome() == super::SuppliedSessionResourceOutcome::NotDispatched)
322                && audit.coverage() == super::SuppliedSessionCoverage::None
323                && if authorization.callback_commit_count() == 0 {
324                    a.request_attempt_count() == 0
325                        && a.outcome() == SuppliedSessionOAuthCodeOutcome::NotDispatched
326                        && match authorization.outcome() {
327                            A::Cancelled => {
328                                audit.outcome() == SuppliedSessionAuditOutcome::Cancelled
329                            },
330                            A::RuntimeLimit => {
331                                audit.outcome() == SuppliedSessionAuditOutcome::RuntimeLimit
332                            },
333                            _ => {
334                                audit.outcome()
335                                    == SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable
336                            },
337                        }
338                } else {
339                    match a.outcome() {
340                        SuppliedSessionOAuthCodeOutcome::Cancelled => {
341                            audit.outcome() == SuppliedSessionAuditOutcome::Cancelled
342                        },
343                        SuppliedSessionOAuthCodeOutcome::RuntimeLimit => {
344                            audit.outcome() == SuppliedSessionAuditOutcome::RuntimeLimit
345                        },
346                        _ => {
347                            audit.outcome()
348                                == SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable
349                        },
350                    }
351                }
352        }
353    }
354}
355
356impl Serialize for SuppliedSessionOAuthInitiatedTokenAudit {
357    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
358        use serde::ser::SerializeStruct;
359        let a = &self.inner;
360        let mut s = serializer.serialize_struct("SuppliedSessionOAuthInitiatedTokenAudit", 20)?;
361        s.serialize_field("profile", "client-initiated-public-code-s256-bearer/v1")?;
362        s.serialize_field(
363            "pkce_assurance",
364            "client_generated_s256_not_remote_enforcement",
365        )?;
366        s.serialize_field("transport_profile", &a.transport_profile)?;
367        s.serialize_field("idp_reference", &a.idp_reference)?;
368        s.serialize_field("token_interpretation", &a.token_interpretation)?;
369        s.serialize_field("request_attempt_count", &a.request_attempt_count)?;
370        s.serialize_field("request_admitted_count", &a.request_admitted_count)?;
371        s.serialize_field("response_completed_count", &a.response_completed_count)?;
372        s.serialize_field("credential_commit_count", &a.credential_commit_count)?;
373        s.serialize_field(
374            "verifier_in_admitted_request",
375            &a.verifier_in_admitted_request,
376        )?;
377        s.serialize_field("status", &a.status)?;
378        s.serialize_field("body_state", &a.body_state)?;
379        s.serialize_field("response_bytes", &a.response_bytes)?;
380        s.serialize_field(
381            "configured_token_response_byte_limit",
382            &a.configured_token_response_byte_limit,
383        )?;
384        s.serialize_field("token_response_validated", &a.token_response_validated)?;
385        s.serialize_field("response_scope_matched", &a.response_scope_matched)?;
386        s.serialize_field(
387            "declared_token_lifetime_seconds",
388            &a.declared_token_lifetime_seconds,
389        )?;
390        s.serialize_field("refresh_token_returned", &a.refresh_token_returned)?;
391        s.serialize_field("evidence_reference", &a.evidence_reference)?;
392        s.serialize_field("outcome", &a.outcome)?;
393        s.end()
394    }
395}
396
397/// A saved string cannot construct this health-loop qualification.
398pub(super) struct CommittedOAuthCodeReceipt {
399    knowledge_authority: crate::knowledge::KnowledgeAuthority,
400    policy_reference: String,
401    application_reference: String,
402    idp_reference: String,
403    evidence_reference: String,
404    principal_alias: String,
405    expected_evidence: Evidence,
406    authorization: Option<Box<super::oauth_authorization::CommittedOAuthAuthorizationReceipt>>,
407}
408
409impl CommittedOAuthCodeReceipt {
410    pub(super) fn evidence_reference(&self) -> &str {
411        &self.evidence_reference
412    }
413
414    pub(super) fn is_valid_for(
415        &self,
416        policy: &SuppliedSessionPolicy,
417        knowledge: &KnowledgeBase,
418    ) -> bool {
419        self.knowledge_authority.is_same_as(knowledge.authority())
420            && matches!(
421                policy.version(),
422                SuppliedSessionPolicyVersion::V5
423                    | SuppliedSessionPolicyVersion::V6
424                    | SuppliedSessionPolicyVersion::V7
425            )
426            && match policy.version() {
427                SuppliedSessionPolicyVersion::V7 => self
428                    .authorization
429                    .as_ref()
430                    .is_some_and(|receipt| receipt.is_valid_for(policy, knowledge)),
431                _ => self.authorization.is_none(),
432            }
433            && policy.policy_reference() == self.policy_reference
434            && policy.application_reference() == self.application_reference
435            && policy.principal_alias() == self.principal_alias
436            && policy
437                .oauth_code_policy()
438                .is_some_and(|idp| idp.binding_reference() == self.idp_reference)
439            && self.expected_evidence.kind() == &EvidenceKind::Authentication
440            && self.expected_evidence.predicate().namespace() == RESOURCE_COMMIT_EVIDENCE_NAMESPACE
441            && self.expected_evidence.predicate().name() == ACQUISITION_PREDICATE
442            && EvidenceId::parse(&self.evidence_reference)
443                .ok()
444                .and_then(|id| {
445                    knowledge.inspect_evidence(&id, |stored| stored == &self.expected_evidence)
446                })
447                == Some(true)
448    }
449}
450
451pub(super) struct AcquiredOAuthCodeCredential {
452    pub(super) credential: SuppliedSessionCredential,
453    pub(super) receipt: CommittedOAuthCodeReceipt,
454    pub(super) declared_lifetime: Duration,
455    pub(super) refresh_token:
456        Option<crate::supplied_session_review::oauth_refresh::OAuthRefreshToken>,
457}
458
459pub(super) struct OAuthCodeAcquisitionContext<'a> {
460    pub(super) authority: &'a SharedWebRuntimeAuthority,
461    pub(super) requests: &'a OAuthCodeRequestBroker,
462    pub(super) descriptor: &'a OAuthCodeRequestDescriptor,
463    pub(super) policy: &'a SuppliedSessionPolicy,
464    pub(super) deadline: Option<tokio::time::Instant>,
465    pub(super) budget: DispatchBudget,
466}
467
468pub(super) async fn acquire(
469    context: OAuthCodeAcquisitionContext<'_>,
470    grant: &SuppliedSessionOAuthGrant,
471) -> (
472    SuppliedSessionOAuthCodeAudit,
473    Result<AcquiredOAuthCodeCredential, SuppliedSessionAuditOutcome>,
474) {
475    acquire_from(context, AcquisitionSource::Imported(grant)).await
476}
477
478pub(super) async fn acquire_authorized(
479    context: OAuthCodeAcquisitionContext<'_>,
480    permit: super::oauth_authorization::OAuthAuthorizationDispatchPermit,
481) -> (
482    SuppliedSessionOAuthCodeAudit,
483    Result<AcquiredOAuthCodeCredential, SuppliedSessionAuditOutcome>,
484) {
485    acquire_from(context, AcquisitionSource::Authorized(Box::new(permit))).await
486}
487
488enum AcquisitionSource<'a> {
489    Imported(&'a SuppliedSessionOAuthGrant),
490    Authorized(Box<super::oauth_authorization::OAuthAuthorizationDispatchPermit>),
491}
492
493async fn acquire_from(
494    context: OAuthCodeAcquisitionContext<'_>,
495    source: AcquisitionSource<'_>,
496) -> (
497    SuppliedSessionOAuthCodeAudit,
498    Result<AcquiredOAuthCodeCredential, SuppliedSessionAuditOutcome>,
499) {
500    let initiated = matches!(&source, AcquisitionSource::Authorized(_));
501    let mut audit = SuppliedSessionOAuthCodeAudit::selected(context.policy)
502        .expect("only a validated V5 policy reaches code acquisition");
503    if context.authority.cancellation().is_cancelled() {
504        audit.outcome = SuppliedSessionOAuthCodeOutcome::Cancelled;
505        return (audit, Err(SuppliedSessionAuditOutcome::Cancelled));
506    }
507    if context
508        .deadline
509        .is_some_and(|end| tokio::time::Instant::now() >= end)
510        || context.budget.dispatched_requests >= context.policy.max_session_requests()
511        || context.budget.accounted_response_bytes >= context.policy.max_total_response_bytes()
512    {
513        audit.outcome = SuppliedSessionOAuthCodeOutcome::RuntimeLimit;
514        return (audit, Err(SuppliedSessionAuditOutcome::RuntimeLimit));
515    }
516    let remaining = context
517        .policy
518        .max_total_response_bytes()
519        .saturating_sub(context.budget.accounted_response_bytes);
520    let body_limit = remaining.min(audit.configured_token_response_byte_limit);
521    let before = context.authority.request_accounting().snapshot();
522    audit.request_attempt_count = 1;
523    let execution = super::boxed_dispatch_execution(|| async {
524        match &source {
525            AcquisitionSource::Imported(grant) => {
526                context
527                    .requests
528                    .collect(
529                        context.descriptor,
530                        context.policy,
531                        grant,
532                        DecisionExecutionLimits::new().with_max_response_body_bytes(body_limit),
533                    )
534                    .await
535            },
536            AcquisitionSource::Authorized(permit) => {
537                context
538                    .requests
539                    .collect_authorized(
540                        context.descriptor,
541                        context.policy,
542                        permit,
543                        DecisionExecutionLimits::new().with_max_response_body_bytes(body_limit),
544                    )
545                    .await
546            },
547        }
548    });
549    let result = super::super::await_execution(
550        context.authority.cancellation(),
551        context.deadline,
552        execution,
553    )
554    .await;
555    let after = context.authority.request_accounting().snapshot();
556    let admitted = after
557        .total_requests()
558        .saturating_sub(before.total_requests());
559    audit.request_admitted_count = u8::try_from(admitted).unwrap_or(u8::MAX);
560    audit.verifier_in_admitted_request = admitted == 1;
561    audit.response_bytes = after
562        .response_bytes()
563        .saturating_sub(before.response_bytes());
564    // Never quietly truncate an unexpected concurrent ledger change to one.
565    if admitted > 1 {
566        audit.outcome = SuppliedSessionOAuthCodeOutcome::CommitFailed;
567        return (
568            audit,
569            Err(SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable),
570        );
571    }
572    let response = match result {
573        RuntimeExecution::Completed(Ok(response)) => response,
574        RuntimeExecution::Cancelled => {
575            audit.outcome = SuppliedSessionOAuthCodeOutcome::Cancelled;
576            return (audit, Err(SuppliedSessionAuditOutcome::Cancelled));
577        },
578        RuntimeExecution::WallTimeExceeded
579        | RuntimeExecution::Completed(Err(OAuthCodeRequestError::RuntimeLimit)) => {
580            audit.outcome = SuppliedSessionOAuthCodeOutcome::RuntimeLimit;
581            return (audit, Err(SuppliedSessionAuditOutcome::RuntimeLimit));
582        },
583        RuntimeExecution::Completed(Err(OAuthCodeRequestError::Http)) => {
584            audit.outcome = SuppliedSessionOAuthCodeOutcome::TransportFailed;
585            return (
586                audit,
587                Err(SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable),
588            );
589        },
590        RuntimeExecution::Completed(Err(
591            OAuthCodeRequestError::InvalidDescriptor
592            | OAuthCodeRequestError::GrantExpired
593            | OAuthCodeRequestError::AlreadyAttempted,
594        )) => {
595            audit.outcome = SuppliedSessionOAuthCodeOutcome::GrantUnavailable;
596            return (
597                audit,
598                Err(SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable),
599            );
600        },
601    };
602    audit.status = Some(response.status);
603    audit.body_state = if response.complete {
604        SuppliedSessionBodyState::Complete
605    } else {
606        SuppliedSessionBodyState::Incomplete
607    };
608    audit.response_completed_count = u8::from(response.complete);
609    if audit.response_bytes > body_limit {
610        audit.outcome = SuppliedSessionOAuthCodeOutcome::RuntimeLimit;
611        return (audit, Err(SuppliedSessionAuditOutcome::RuntimeLimit));
612    }
613    if response.status == 429 {
614        audit.outcome = SuppliedSessionOAuthCodeOutcome::Throttled;
615        return (
616            audit,
617            Err(SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable),
618        );
619    }
620    if admitted != 1
621        || response.status != 200
622        || !response.complete
623        || !response.representation_supported
624    {
625        audit.outcome = SuppliedSessionOAuthCodeOutcome::ResponseRejected;
626        return (
627            audit,
628            Err(SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable),
629        );
630    }
631    let idp = context
632        .policy
633        .oauth_code_policy()
634        .expect("validated V5 IdP binding");
635    let parsed = if context.policy.version() == SuppliedSessionPolicyVersion::V6 {
636        crate::supplied_session_review::oauth_refresh::parse_renewable_token_response(
637            context.policy,
638            response.body,
639            None,
640        )
641        .and_then(|parsed| {
642            let lifetime = parsed.expires_in_seconds();
643            let (authorization, refresh_token) = parsed.into_parts()?;
644            Ok((authorization, lifetime, true, Some(refresh_token)))
645        })
646    } else {
647        parse_token_response(idp, response.body).and_then(|parsed| {
648            let lifetime = parsed.expires_in_seconds();
649            let refresh_returned = parsed.refresh_token_returned();
650            let authorization = parsed.into_authorization()?;
651            Ok((authorization, lifetime, refresh_returned, None))
652        })
653    };
654    let (authorization, lifetime, refresh_returned, refresh_token) = match parsed {
655        Ok(value) => value,
656        Err(_) => {
657            audit.outcome = SuppliedSessionOAuthCodeOutcome::ResponseRejected;
658            return (
659                audit,
660                Err(SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable),
661            );
662        },
663    };
664    audit.token_response_validated = true;
665    audit.response_scope_matched = Some(true);
666    audit.declared_token_lifetime_seconds = Some(lifetime);
667    audit.refresh_token_returned = Some(refresh_returned);
668    let transaction_context = if initiated {
669        "client_initiated_committed_callback"
670    } else {
671        "operator_declared_external"
672    };
673    let binding = format!(
674        "policy={};application={};idp={};purpose=oauth_code_exchange;status=200;body_state=complete;response_bytes={};scope=exact_supported_echo;declared_lifetime={lifetime};refresh_returned={refresh_returned};context={transaction_context};token=opaque_bearer;outcome=credential_committed",
675        context.policy.policy_reference(), context.policy.application_reference(),
676        idp.binding_reference(), audit.response_bytes,
677    );
678    let authorization_receipt = match source {
679        AcquisitionSource::Imported(_) => None,
680        AcquisitionSource::Authorized(permit) => Some(Box::new((*permit).into_receipt())),
681    };
682    let receipt = commit_value_safe_evidence(
683        context.policy,
684        idp.binding_reference(),
685        b"security.supplied-session-oauth-code-evidence.v1\0",
686        "supplied-session-oauth-code-evidence-sha256",
687        ACQUISITION_PREDICATE,
688        if initiated {
689            "descriptor-bound-client-initiated-code-exchange"
690        } else {
691            "descriptor-bound-imported-code-exchange"
692        },
693        binding,
694        context.authority.knowledge(),
695    )
696    .and_then(|reference| {
697        let id = EvidenceId::parse(&reference).ok()?;
698        let expected_evidence = context
699            .authority
700            .knowledge()
701            .inspect_evidence(&id, Clone::clone)?;
702        let receipt = CommittedOAuthCodeReceipt {
703            knowledge_authority: context.authority.knowledge().authority().clone(),
704            policy_reference: context.policy.policy_reference().to_owned(),
705            application_reference: context.policy.application_reference().to_owned(),
706            idp_reference: idp.binding_reference().to_owned(),
707            principal_alias: context.policy.principal_alias().to_owned(),
708            evidence_reference: reference,
709            expected_evidence,
710            authorization: authorization_receipt,
711        };
712        receipt
713            .is_valid_for(context.policy, context.authority.knowledge())
714            .then_some(receipt)
715    });
716    let Some(receipt) = receipt else {
717        audit.outcome = SuppliedSessionOAuthCodeOutcome::CommitFailed;
718        return (
719            audit,
720            Err(SuppliedSessionAuditOutcome::OAuthCodeExchangeUnavailable),
721        );
722    };
723    audit.evidence_reference = Some(receipt.evidence_reference.clone());
724    audit.credential_commit_count = 1;
725    audit.outcome = SuppliedSessionOAuthCodeOutcome::CredentialCommitted;
726    (
727        audit,
728        Ok(AcquiredOAuthCodeCredential {
729            credential: SuppliedSessionCredential::Authorization(authorization),
730            receipt,
731            declared_lifetime: Duration::from_secs(lifetime),
732            refresh_token,
733        }),
734    )
735}