dispatch.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. //! `POST /token` grant dispatcher (RFC 6749 §5, RFC 8693 §2, RFC 9449 §6).
  2. //!
  3. //! Parses the form once, authenticates the client through the shared
  4. //! [`client_auth`] dispatch (or delegates to the grants that authenticate
  5. //! themselves), verifies the fresh sender proof, dispatches on `grant_type`,
  6. //! and shapes the RFC 6749 §5.1 JSON response. A missing `grant_type` is
  7. //! `invalid_request`; an unknown one fails closed with `unsupported_grant_type`.
  8. //!
  9. //! The per-grant branches live in [`grants`]; this module owns the HTTP
  10. //! contract, the request-scoped handles, and the shared authentication and
  11. //! proof helpers.
  12. mod grants;
  13. use axum::Json;
  14. use axum::extract::{Extension, State, rejection::FormRejection};
  15. use axum::http::{HeaderMap, HeaderValue, header};
  16. use axum::response::{IntoResponse, Response};
  17. use secrecy::SecretString;
  18. use serde::{Deserialize, Serialize};
  19. use sqlx::PgPool;
  20. use super::grants::device_code::DevicePollError;
  21. use super::{ClientKind, SenderProof, TokenService};
  22. use crate::{
  23. db::{TokenEndpointAuthMethod, client_repo, scope_repo},
  24. error::AppError,
  25. jose::dpop::{DpopRequest, verify_dpop},
  26. protocol::{
  27. client_auth::{self, AuthContext, AuthStores, ClientAuthRequest},
  28. mtls,
  29. scope::ScopeSet,
  30. },
  31. state::{AppState, RedisConn},
  32. store::Store,
  33. tls::PeerCertificates,
  34. };
  35. /// `POST /token` request form (RFC 6749 §4 plus every extension this server
  36. /// supports). All fields are optional here: each grant validates its own
  37. /// required members and answers `invalid_request` when one is missing.
  38. #[derive(Debug, Default, Deserialize)]
  39. pub struct TokenForm {
  40. /// The grant being exercised.
  41. pub grant_type: Option<String>,
  42. /// Authorization code (authorization-code grant).
  43. pub code: Option<String>,
  44. /// Redirect URI bound to the code (authorization-code grant).
  45. pub redirect_uri: Option<String>,
  46. /// PKCE verifier (authorization-code grant).
  47. pub code_verifier: Option<String>,
  48. /// Refresh token (refresh-token grant).
  49. pub refresh_token: Option<String>,
  50. /// Client identifier, from the form body.
  51. pub client_id: Option<String>,
  52. /// Client secret, from the form body only.
  53. pub client_secret: Option<String>,
  54. /// Requested scope, or `None` to keep the grant's scope.
  55. pub scope: Option<String>,
  56. /// Device code (device-code grant).
  57. pub device_code: Option<String>,
  58. /// Subject token (token-exchange grant).
  59. pub subject_token: Option<String>,
  60. /// Subject token type URN (token-exchange grant).
  61. pub subject_token_type: Option<String>,
  62. /// Actor token (token-exchange grant).
  63. pub actor_token: Option<String>,
  64. /// Actor token type URN (token-exchange grant).
  65. pub actor_token_type: Option<String>,
  66. /// Requested token type URN (token-exchange grant).
  67. pub requested_token_type: Option<String>,
  68. /// Token type hint (accepted for RFC 7009/7662 parity; never trusted).
  69. pub token_type_hint: Option<String>,
  70. /// `private_key_jwt` assertion type.
  71. pub client_assertion_type: Option<String>,
  72. /// `private_key_jwt` assertion.
  73. pub client_assertion: Option<String>,
  74. }
  75. /// RFC 6749 §5.1 token response plus the RFC 8693 §2.2.1
  76. /// `issued_token_type` member. Optional members are omitted, never null.
  77. #[derive(Debug, Serialize)]
  78. struct TokenResponseBody {
  79. /// Opaque access token.
  80. access_token: String,
  81. /// `DPoP` or `Bearer`.
  82. token_type: String,
  83. /// Access-token lifetime, seconds.
  84. expires_in: u64,
  85. /// Opaque refresh token, absent for grants that issue none.
  86. #[serde(skip_serializing_if = "Option::is_none")]
  87. refresh_token: Option<String>,
  88. /// Space-joined effective scope.
  89. scope: String,
  90. /// Token-exchange result type, absent for every other grant.
  91. #[serde(skip_serializing_if = "Option::is_none")]
  92. issued_token_type: Option<String>,
  93. }
  94. /// Token-endpoint failure: the RFC 6749 error model plus the RFC 8628 §3.5
  95. /// polling codes.
  96. enum TokenError {
  97. /// A protocol or dependency failure with its own status.
  98. OAuth(AppError),
  99. /// A device-polling failure (`authorization_pending`, `slow_down`, ...).
  100. Device(DevicePollError),
  101. }
  102. impl From<AppError> for TokenError {
  103. fn from(error: AppError) -> Self {
  104. Self::OAuth(error)
  105. }
  106. }
  107. impl From<DevicePollError> for TokenError {
  108. fn from(error: DevicePollError) -> Self {
  109. Self::Device(error)
  110. }
  111. }
  112. impl IntoResponse for TokenError {
  113. fn into_response(self) -> Response {
  114. match self {
  115. Self::OAuth(error) => error.into_response(),
  116. Self::Device(error) => error.into_response(),
  117. }
  118. }
  119. }
  120. /// Request-scoped handles shared by every grant branch.
  121. struct Dispatch<'a> {
  122. /// Shared application state.
  123. state: &'a AppState,
  124. /// Redis connection for codes, tokens, proofs, and replay markers.
  125. conn: &'a mut RedisConn,
  126. /// Key layout plus retry/durability knobs.
  127. store: &'a Store,
  128. /// Request headers (Basic credentials, `DPoP` proof).
  129. headers: &'a HeaderMap,
  130. /// Parsed form body.
  131. form: &'a TokenForm,
  132. /// Verified peer chain injected by the TLS accept loop, when present.
  133. peer_certs: Option<&'a PeerCertificates>,
  134. /// Current time as seconds since the Unix epoch.
  135. now_unix: i64,
  136. }
  137. impl Dispatch<'_> {
  138. /// Authenticate the token-request client through the shared dispatch.
  139. async fn authenticate(&mut self) -> Result<client_auth::AuthenticatedClient, AppError> {
  140. let endpoint = client_auth::token_endpoint(&self.state.config.issuer);
  141. let request = ClientAuthRequest {
  142. basic_header: basic_header(self.headers),
  143. client_id: self.form.client_id.as_deref(),
  144. body_secret: self.form.client_secret.clone().map(SecretString::from),
  145. assertion_type: self.form.client_assertion_type.as_deref(),
  146. assertion: self.form.client_assertion.as_deref(),
  147. peer_certs: self.peer_certs,
  148. };
  149. let mut stores = AuthStores {
  150. pool: &self.state.pool,
  151. keys: &self.state.keys,
  152. conn: &mut *self.conn,
  153. store: self.store,
  154. };
  155. let ctx = AuthContext {
  156. token_endpoint: &endpoint,
  157. now: self.state.clock.now(),
  158. };
  159. client_auth::authenticate_client(&mut stores, &ctx, &request).await
  160. }
  161. /// Verify this request's fresh sender proofs (DPoP `jkt`, mTLS `x5t#S256`).
  162. async fn sender_proof(&mut self, client: &client_repo::Client) -> Result<SenderProof, AppError> {
  163. let htu = client_auth::token_endpoint(&self.state.config.issuer);
  164. let dpop_jkt = match dpop_proof(self.headers) {
  165. Some(proof) => Some(
  166. verify_dpop(
  167. &mut *self.conn,
  168. self.store,
  169. &DpopRequest {
  170. proof: Some(proof),
  171. htm: "POST",
  172. htu: &htu,
  173. access_token: None,
  174. now_unix: self.now_unix,
  175. },
  176. )
  177. .await?
  178. .jkt,
  179. ),
  180. None => None,
  181. };
  182. let mtls_x5t_s256 = match client.auth_method() {
  183. Ok(
  184. TokenEndpointAuthMethod::TlsClientAuth | TokenEndpointAuthMethod::SelfSignedTlsClientAuth,
  185. ) => {
  186. let leaf = self.peer_certs.and_then(|certs| certs.chain().first());
  187. Some(
  188. mtls::authenticate(&self.state.pool, &self.state.keys, client, leaf)
  189. .await
  190. .map_err(mtls::MtlsError::into_app_error)?
  191. .x5t_s256,
  192. )
  193. }
  194. _ => None,
  195. };
  196. Ok(SenderProof {
  197. dpop_jkt,
  198. mtls_x5t_s256,
  199. })
  200. }
  201. }
  202. /// The raw `Authorization` header, when present and UTF-8.
  203. fn basic_header(headers: &HeaderMap) -> Option<&str> {
  204. headers
  205. .get(header::AUTHORIZATION)
  206. .and_then(|value| value.to_str().ok())
  207. }
  208. /// The raw `DPoP` proof header, when present and UTF-8.
  209. fn dpop_proof(headers: &HeaderMap) -> Option<&str> {
  210. headers.get("dpop").and_then(|value| value.to_str().ok())
  211. }
  212. /// Public vs confidential: the sender-constraint matrix selector.
  213. fn client_kind(client: &client_repo::Client) -> Result<ClientKind, AppError> {
  214. match client.auth_method() {
  215. Ok(TokenEndpointAuthMethod::None) => Ok(ClientKind::Public),
  216. Ok(_) => Ok(ClientKind::Confidential),
  217. Err(_) => Err(AppError::invalid_client_non_basic()),
  218. }
  219. }
  220. /// The client must be registered for the grant it is exercising.
  221. fn require_grant(client: &client_repo::Client, grant: &str) -> Result<(), AppError> {
  222. if client
  223. .grant_types
  224. .iter()
  225. .any(|registered| registered == grant)
  226. {
  227. Ok(())
  228. } else {
  229. Err(AppError::unauthorized_client(
  230. "client may not use this grant",
  231. ))
  232. }
  233. }
  234. /// A client registered for DPoP-bound tokens must present a fresh proof.
  235. ///
  236. /// Shared with the grants that authenticate their client internally
  237. /// (`authorization_code`, `device_code`) so the registration flag is enforced
  238. /// on every token-time path, not only the dispatcher-authenticated ones.
  239. pub(super) fn require_dpop_binding(
  240. client: &client_repo::Client,
  241. proof: &SenderProof,
  242. ) -> Result<(), AppError> {
  243. let proven = proof.dpop_jkt.as_deref().is_some_and(|jkt| !jkt.is_empty());
  244. if client.dpop_bound_access_tokens && !proven {
  245. Err(AppError::invalid_grant(
  246. "DPoP proof is required for this client",
  247. ))
  248. } else {
  249. Ok(())
  250. }
  251. }
  252. /// The enabled scope catalog as a set.
  253. async fn enabled_scopes(pool: &PgPool) -> Result<ScopeSet, AppError> {
  254. let names: Vec<String> = scope_repo::list_enabled(pool)
  255. .await?
  256. .into_iter()
  257. .map(|row| row.name)
  258. .collect();
  259. ScopeSet::from_names(&names)
  260. }
  261. /// Build the token service from the validated TTLs and key ring.
  262. fn token_service(state: &AppState) -> Result<TokenService, AppError> {
  263. TokenService::new(&state.config.ttl, &state.keys).map_err(AppError::from)
  264. }
  265. /// `POST /token`: the five-grant dispatcher.
  266. ///
  267. /// # Errors
  268. ///
  269. /// Returns the RFC 6749 §5.2 error for the failing grant (or the RFC 8628
  270. /// §3.5 polling code), always with the correct HTTP status.
  271. pub async fn handler(
  272. State(state): State<AppState>,
  273. peer_certs: Option<Extension<PeerCertificates>>,
  274. headers: HeaderMap,
  275. form: Result<axum::Form<TokenForm>, FormRejection>,
  276. ) -> Response {
  277. let form = match form {
  278. Ok(axum::Form(form)) => form,
  279. Err(rejection) => return form_rejection_response(rejection),
  280. };
  281. let mut conn = state.redis.clone();
  282. let store = Store::from_config(&state.config.redis);
  283. let now_unix = state.clock.now().unix_timestamp();
  284. let mut dispatch = Dispatch {
  285. state: &state,
  286. conn: &mut conn,
  287. store: &store,
  288. headers: &headers,
  289. form: &form,
  290. peer_certs: peer_certs.as_deref(),
  291. now_unix,
  292. };
  293. match grants::dispatch_grant(&mut dispatch).await {
  294. Ok(body) => token_response(body),
  295. Err(error) => error.into_response(),
  296. }
  297. }
  298. /// Map a form-extractor rejection onto the documented error model.
  299. ///
  300. /// A deserialization failure is the transport shape of RFC 6749 §5.2
  301. /// "includes a parameter more than once", so it renders the documented
  302. /// `invalid_request` JSON error; non-form content types and body-read
  303. /// failures keep the extractor's own status and body.
  304. fn form_rejection_response(rejection: FormRejection) -> Response {
  305. match rejection {
  306. FormRejection::FailedToDeserializeForm(_) | FormRejection::FailedToDeserializeFormBody(_) => {
  307. AppError::invalid_request(
  308. "the request body is malformed or includes a parameter more than once",
  309. )
  310. .into_response()
  311. }
  312. other => other.into_response(),
  313. }
  314. }
  315. /// Shape a success response: JSON plus the RFC 6749 §5.1 no-store headers.
  316. fn token_response(body: TokenResponseBody) -> Response {
  317. let mut response = Json(body).into_response();
  318. response
  319. .headers_mut()
  320. .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
  321. response
  322. .headers_mut()
  323. .insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
  324. response
  325. }