oauth2_broker/flows/
auth_code_pkce.rs

1//! Authorization Code + PKCE flow orchestration helpers and state carriers.
2//!
3//! These routines keep the most error-prone pieces of the handshake (state generation,
4//! PKCE bookkeeping, token exchange, and persistence) behind the [`Broker`] facade so
5//! application code can focus on redirect plumbing.
6//!
7//! The `AuthorizationSession` returned by [`Broker::start_authorization`] is designed to
8//! be serialized and stored by callers between the authorize redirect and the callback
9//! handler so replayed states or swapped principals can be rejected immediately.
10
11mod session;
12
13pub use session::*;
14
15// self
16use crate::{
17	_prelude::*,
18	auth::{PrincipalId, ScopeSet, TenantId, TokenFamily, TokenRecord},
19	error::ConfigError,
20	flows::Broker,
21	http::TokenHttpClient,
22	oauth::{BasicFacade, OAuth2Facade, TransportErrorMapper},
23	obs::{self, FlowKind, FlowOutcome, FlowSpan},
24	provider::GrantType,
25	store::BrokerStore,
26};
27
28impl<C, M> Broker<C, M>
29where
30	C: ?Sized + TokenHttpClient,
31	M: ?Sized + TransportErrorMapper<C::TransportError>,
32{
33	/// Generates an Authorization Code + PKCE session for the provided tenant context.
34	///
35	/// Calling this method verifies that the backing descriptor advertises
36	/// `authorization_code` support, builds a cryptographically strong `state`, and
37	/// attaches a PKCE verifier/challenge pair. The resulting [`AuthorizationSession`]
38	/// exposes accessor methods that UI layers can use to embed the authorize URL in a
39	/// link or form, while backend handlers can persist the tenant/principal/scope
40	/// context alongside the opaque state for later validation.
41	///
42	/// The broker does **not** automatically persist the session — it is the caller’s
43	/// responsibility to stash it (or the relevant fields) until the redirect round-trip
44	/// completes so [`AuthorizationSession::validate_state`] can run before an exchange.
45	pub fn start_authorization(
46		&self,
47		tenant: TenantId,
48		principal: PrincipalId,
49		scope: ScopeSet,
50		redirect_uri: Url,
51	) -> Result<AuthorizationSession> {
52		const KIND: FlowKind = FlowKind::AuthorizationCode;
53
54		let _span = FlowSpan::new(KIND, "start_authorization").entered();
55
56		obs::record_flow_outcome(KIND, FlowOutcome::Attempt);
57
58		let result = (|| -> Result<AuthorizationSession> {
59			self.ensure_authorization_code_supported()?;
60			Ok(build_session(
61				&self.descriptor,
62				self.client_id.as_str(),
63				tenant,
64				principal,
65				scope,
66				redirect_uri,
67			))
68		})();
69
70		match &result {
71			Ok(_) => obs::record_flow_outcome(KIND, FlowOutcome::Success),
72			Err(_) => obs::record_flow_outcome(KIND, FlowOutcome::Failure),
73		}
74		result
75	}
76
77	/// Exchanges an authorization code + PKCE verifier for broker-managed tokens.
78	///
79	/// The `AuthorizationSession` generated by [`Broker::start_authorization`] carries
80	/// the tenant/principal/scope context, redirect URI, and PKCE verifier needed to
81	/// process the callback. Once the provider redirects back with a code, call this
82	/// method with the original session and the returned `code`. Successful exchanges
83	/// emit a [`TokenRecord`] that has already been written to the configured
84	/// [`BrokerStore`](crate::store::BrokerStore) so subsequent fetches observe the
85	/// latest secrets.
86	pub async fn exchange_code(
87		&self,
88		session: AuthorizationSession,
89		authorization_code: impl AsRef<str>,
90	) -> Result<TokenRecord> {
91		const KIND: FlowKind = FlowKind::AuthorizationCode;
92
93		let span = FlowSpan::new(KIND, "exchange_code");
94
95		obs::record_flow_outcome(KIND, FlowOutcome::Attempt);
96
97		let result = span
98			.instrument(async move {
99				self.ensure_authorization_code_supported()?;
100				let (tenant, principal, requested_scope, redirect_uri, pkce) =
101					session.into_exchange_parts();
102				let mut family = TokenFamily::new(tenant, principal);
103
104				family.provider = Some(self.descriptor.id.clone());
105
106				let facade: BasicFacade<C, M> = BasicFacade::from_descriptor(
107					&self.descriptor,
108					&self.client_id,
109					self.client_secret.as_deref(),
110					Some(&redirect_uri),
111					self.http_client.clone(),
112					self.transport_mapper.clone(),
113				)?;
114				let record = facade
115					.exchange_authorization_code(
116						self.strategy.as_ref(),
117						family,
118						authorization_code.as_ref(),
119						&pkce.verifier,
120						&requested_scope,
121						&redirect_uri,
122					)
123					.await?;
124
125				<dyn BrokerStore>::save(self.store.as_ref(), record.clone())
126					.await
127					.map_err(Error::from)?;
128
129				Ok(record)
130			})
131			.await;
132
133		match &result {
134			Ok(_) => obs::record_flow_outcome(KIND, FlowOutcome::Success),
135			Err(_) => obs::record_flow_outcome(KIND, FlowOutcome::Failure),
136		}
137
138		result
139	}
140
141	fn ensure_authorization_code_supported(&self) -> Result<()> {
142		if self.descriptor.supports(GrantType::AuthorizationCode) {
143			Ok(())
144		} else {
145			Err(ConfigError::UnsupportedGrant {
146				descriptor: self.descriptor.id.to_string(),
147				grant: "authorization_code",
148			}
149			.into())
150		}
151	}
152}