dispatch.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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};
  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::{
  26. dpop::{DpopRequest, verify_dpop},
  27. jwt_access::JwtAccessSigner,
  28. },
  29. protocol::{
  30. client_auth::{self, AuthContext, AuthStores, ClientAuthRequest},
  31. mtls,
  32. scope::ScopeSet,
  33. },
  34. state::{AppState, RedisConn},
  35. store::Store,
  36. tls::PeerCertificates,
  37. };
  38. /// `POST /token` request form (RFC 6749 §4 plus every extension this server
  39. /// supports). All fields are optional here: each grant validates its own
  40. /// required members and answers `invalid_request` when one is missing.
  41. #[derive(Debug, Default, Deserialize)]
  42. pub struct TokenForm {
  43. /// The grant being exercised.
  44. pub grant_type: Option<String>,
  45. /// Authorization code (authorization-code grant).
  46. pub code: Option<String>,
  47. /// Redirect URI bound to the code (authorization-code grant).
  48. pub redirect_uri: Option<String>,
  49. /// PKCE verifier (authorization-code grant).
  50. pub code_verifier: Option<String>,
  51. /// Refresh token (refresh-token grant).
  52. pub refresh_token: Option<String>,
  53. /// Client identifier, from the form body.
  54. pub client_id: Option<String>,
  55. /// Client secret, from the form body only.
  56. pub client_secret: Option<String>,
  57. /// Requested scope, or `None` to keep the grant's scope.
  58. pub scope: Option<String>,
  59. /// Device code (device-code grant).
  60. pub device_code: Option<String>,
  61. /// Subject token (token-exchange grant).
  62. pub subject_token: Option<String>,
  63. /// Subject token type URN (token-exchange grant).
  64. pub subject_token_type: Option<String>,
  65. /// Actor token (token-exchange grant).
  66. pub actor_token: Option<String>,
  67. /// Actor token type URN (token-exchange grant).
  68. pub actor_token_type: Option<String>,
  69. /// Requested token type URN (token-exchange grant).
  70. pub requested_token_type: Option<String>,
  71. /// Token type hint (accepted for RFC 7009/7662 parity; never trusted).
  72. pub token_type_hint: Option<String>,
  73. /// `private_key_jwt` assertion type.
  74. pub client_assertion_type: Option<String>,
  75. /// `private_key_jwt` assertion.
  76. pub client_assertion: Option<String>,
  77. }
  78. impl TokenForm {
  79. /// Fields the extractor recognizes: a repeat of any of these is the
  80. /// duplicate-field rejection (mapped to `invalid_request`, per RFC 6749
  81. /// §5.2 "includes a parameter more than once"). Unknown fields (including
  82. /// a repeated `resource`) are silently absorbed, as `serde_urlencoded`
  83. /// absorbed them.
  84. const KNOWN_FIELDS: [&str; 17] = [
  85. "grant_type",
  86. "code",
  87. "redirect_uri",
  88. "code_verifier",
  89. "refresh_token",
  90. "client_id",
  91. "client_secret",
  92. "scope",
  93. "device_code",
  94. "subject_token",
  95. "subject_token_type",
  96. "actor_token",
  97. "actor_token_type",
  98. "requested_token_type",
  99. "token_type_hint",
  100. "client_assertion_type",
  101. "client_assertion",
  102. ];
  103. /// Build the form from a strict single-valued map (the `Bytes` extractor
  104. /// replacement for `Form<TokenForm>`). Every member is optional: each
  105. /// grant validates its own required members.
  106. fn from_map(map: &std::collections::HashMap<String, String>) -> Self {
  107. Self {
  108. grant_type: map.get("grant_type").cloned(),
  109. code: map.get("code").cloned(),
  110. redirect_uri: map.get("redirect_uri").cloned(),
  111. code_verifier: map.get("code_verifier").cloned(),
  112. refresh_token: map.get("refresh_token").cloned(),
  113. client_id: map.get("client_id").cloned(),
  114. client_secret: map.get("client_secret").cloned(),
  115. scope: map.get("scope").cloned(),
  116. device_code: map.get("device_code").cloned(),
  117. subject_token: map.get("subject_token").cloned(),
  118. subject_token_type: map.get("subject_token_type").cloned(),
  119. actor_token: map.get("actor_token").cloned(),
  120. actor_token_type: map.get("actor_token_type").cloned(),
  121. requested_token_type: map.get("requested_token_type").cloned(),
  122. token_type_hint: map.get("token_type_hint").cloned(),
  123. client_assertion_type: map.get("client_assertion_type").cloned(),
  124. client_assertion: map.get("client_assertion").cloned(),
  125. }
  126. }
  127. }
  128. /// RFC 6749 §5.1 token response plus the RFC 8693 §2.2.1
  129. /// `issued_token_type` member. Optional members are omitted, never null.
  130. #[derive(Debug, Serialize)]
  131. struct TokenResponseBody {
  132. /// Opaque access token.
  133. access_token: String,
  134. /// `DPoP` or `Bearer`.
  135. token_type: String,
  136. /// Access-token lifetime, seconds.
  137. expires_in: u64,
  138. /// Opaque refresh token, absent for grants that issue none.
  139. #[serde(skip_serializing_if = "Option::is_none")]
  140. refresh_token: Option<String>,
  141. /// Space-joined effective scope.
  142. scope: String,
  143. /// Token-exchange result type, absent for every other grant.
  144. #[serde(skip_serializing_if = "Option::is_none")]
  145. issued_token_type: Option<String>,
  146. }
  147. /// Token-endpoint failure: the RFC 6749 error model plus the RFC 8628 §3.5
  148. /// polling codes.
  149. enum TokenError {
  150. /// A protocol or dependency failure with its own status.
  151. OAuth(AppError),
  152. /// A device-polling failure (`authorization_pending`, `slow_down`, ...).
  153. Device(DevicePollError),
  154. }
  155. impl From<AppError> for TokenError {
  156. fn from(error: AppError) -> Self {
  157. Self::OAuth(error)
  158. }
  159. }
  160. impl From<DevicePollError> for TokenError {
  161. fn from(error: DevicePollError) -> Self {
  162. Self::Device(error)
  163. }
  164. }
  165. impl IntoResponse for TokenError {
  166. fn into_response(self) -> Response {
  167. match self {
  168. Self::OAuth(error) => error.into_response(),
  169. Self::Device(error) => error.into_response(),
  170. }
  171. }
  172. }
  173. /// Request-scoped handles shared by every grant branch.
  174. struct Dispatch<'a> {
  175. /// Shared application state.
  176. state: &'a AppState,
  177. /// Redis connection for codes, tokens, proofs, and replay markers.
  178. conn: &'a mut RedisConn,
  179. /// Key layout plus retry/durability knobs.
  180. store: &'a Store,
  181. /// Request headers (Basic credentials, `DPoP` proof).
  182. headers: &'a HeaderMap,
  183. /// Parsed form body.
  184. form: &'a TokenForm,
  185. /// Verified peer chain injected by the TLS accept loop, when present.
  186. peer_certs: Option<&'a PeerCertificates>,
  187. /// Validated resource audiences (RFC 8707) from this token request, sorted
  188. /// and deduped; empty when the request carried no `resource`.
  189. resources: &'a [String],
  190. /// CIMD resolution settings for a registration-free fallback; `None` when
  191. /// `config.cimd_enabled` is off.
  192. cimd: Option<crate::protocol::cimd::CimdSettings<'a>>,
  193. /// Current time as seconds since the Unix epoch.
  194. now_unix: i64,
  195. }
  196. impl Dispatch<'_> {
  197. /// Authenticate the token-request client through the shared dispatch.
  198. async fn authenticate(&mut self) -> Result<client_auth::AuthenticatedClient, AppError> {
  199. let endpoint = client_auth::token_endpoint(&self.state.config.issuer);
  200. let request = ClientAuthRequest {
  201. basic_header: basic_header(self.headers),
  202. client_id: self.form.client_id.as_deref(),
  203. body_secret: self.form.client_secret.clone().map(SecretString::from),
  204. assertion_type: self.form.client_assertion_type.as_deref(),
  205. assertion: self.form.client_assertion.as_deref(),
  206. peer_certs: self.peer_certs,
  207. };
  208. let mut stores = AuthStores {
  209. pool: &self.state.pool,
  210. keys: &self.state.keys,
  211. conn: &mut *self.conn,
  212. store: self.store,
  213. };
  214. let ctx = AuthContext {
  215. token_endpoint: &endpoint,
  216. now: self.state.clock.now(),
  217. cimd: self.cimd,
  218. };
  219. client_auth::authenticate_client(&mut stores, &ctx, &request).await
  220. }
  221. /// Enforce this client's access-token format on a minting path.
  222. ///
  223. /// `opaque` returns without constructing the JWT signer or reading any key
  224. /// material; a `jwt` client whose global switch is off (or any unrecognized
  225. /// stored value) fails closed with `temporarily_unavailable` (503).
  226. fn require_token_format(&self, client: &client_repo::Client) -> Result<TokenFormat, AppError> {
  227. resolve_token_format(
  228. &client.access_token_format,
  229. self.state.config.jwt_access_tokens_enabled,
  230. )
  231. }
  232. /// Apply the format gate to a grant that authenticates inside its own
  233. /// module (`authorization_code`, `device_code`), and report the resolved
  234. /// format for the mint call.
  235. ///
  236. /// Before delegation the dispatcher only knows a candidate presenter id. An
  237. /// unknown or inactive client, or a structurally inconsistent Basic/body
  238. /// pair, is left to the grant's existing `invalid_client` path so error
  239. /// precedence does not change (`Ok(None)`: the grant never mints). A
  240. /// client the dispatcher can name is gated first, so a `jwt` client with
  241. /// the global switch off never reaches minting (and never burns a code or
  242. /// a device poll). A registration-free CIMD id resolves to `Ok(None)` here
  243. /// and to `Opaque` at the call site: the synthetic principal is always
  244. /// `opaque` by construction (a document can never opt into JWT), so no
  245. /// format gate is skipped.
  246. async fn gate_candidate_client_format(
  247. &self,
  248. client_id: Option<String>,
  249. ) -> Result<Option<TokenFormat>, AppError> {
  250. let Some(client_id) = client_id else {
  251. return Ok(None);
  252. };
  253. let Some(client) =
  254. client_repo::find_active_by_client_id(&self.state.pool, &self.state.keys, &client_id).await?
  255. else {
  256. return Ok(None);
  257. };
  258. self.require_token_format(&client).map(Some)
  259. }
  260. /// Verify this request's fresh sender proofs (DPoP `jkt`, mTLS `x5t#S256`).
  261. async fn sender_proof(&mut self, client: &client_repo::Client) -> Result<SenderProof, AppError> {
  262. let htu = client_auth::token_endpoint(&self.state.config.issuer);
  263. let dpop_jkt = match dpop_proof(self.headers) {
  264. Some(proof) => Some(
  265. verify_dpop(
  266. &mut *self.conn,
  267. self.store,
  268. &DpopRequest {
  269. proof: Some(proof),
  270. htm: "POST",
  271. htu: &htu,
  272. access_token: None,
  273. now_unix: self.now_unix,
  274. },
  275. )
  276. .await?
  277. .jkt,
  278. ),
  279. None => None,
  280. };
  281. let mtls_x5t_s256 = match client.auth_method() {
  282. Ok(
  283. TokenEndpointAuthMethod::TlsClientAuth | TokenEndpointAuthMethod::SelfSignedTlsClientAuth,
  284. ) => {
  285. let leaf = self.peer_certs.and_then(|certs| certs.chain().first());
  286. Some(
  287. mtls::authenticate(&self.state.pool, &self.state.keys, client, leaf)
  288. .await
  289. .map_err(mtls::MtlsError::into_app_error)?
  290. .x5t_s256,
  291. )
  292. }
  293. _ => None,
  294. };
  295. Ok(SenderProof {
  296. dpop_jkt,
  297. mtls_x5t_s256,
  298. })
  299. }
  300. }
  301. /// The raw `Authorization` header, when present and UTF-8.
  302. fn basic_header(headers: &HeaderMap) -> Option<&str> {
  303. headers
  304. .get(header::AUTHORIZATION)
  305. .and_then(|value| value.to_str().ok())
  306. }
  307. /// The raw `DPoP` proof header, when present and UTF-8.
  308. fn dpop_proof(headers: &HeaderMap) -> Option<&str> {
  309. headers.get("dpop").and_then(|value| value.to_str().ok())
  310. }
  311. /// Re-exported from [`super::types`], where the decision type lives so both
  312. /// this dispatcher and [`super::service`] share it without a module cycle.
  313. pub use super::types::TokenFormat;
  314. /// Resolve a client's stored `access_token_format` to a [`TokenFormat`].
  315. ///
  316. /// `opaque` is the default and never consults the JWT stack. RFC 9068 §2 makes
  317. /// JWT access tokens an opt-in profile the authorization server decides per
  318. /// client; §4 ties issuance to signing-key material, so a `jwt` client is
  319. /// accepted only while the operator's global `jwt_access_tokens_enabled`
  320. /// switch is on. When that switch is off — or the stored value is unknown
  321. /// (corruption or a future value) — the request fails closed with
  322. /// `temporarily_unavailable` (HTTP 503): the client asked for a token shape
  323. /// this deployment cannot mint, so there is no silent opaque fallback and no
  324. /// partial token. It is deliberately not `server_error` (HTTP 500), which
  325. /// would retract the fail-closed dependency contract, and not a 4xx, which
  326. /// would blame the client for a deployment state.
  327. fn resolve_token_format(
  328. stored: &str,
  329. jwt_access_tokens_enabled: bool,
  330. ) -> Result<TokenFormat, AppError> {
  331. match stored {
  332. "opaque" => Ok(TokenFormat::Opaque),
  333. "jwt" if jwt_access_tokens_enabled => Ok(TokenFormat::Jwt),
  334. "jwt" => Err(AppError::temporarily_unavailable()),
  335. _ => Err(AppError::temporarily_unavailable()),
  336. }
  337. }
  338. /// Public vs confidential: the sender-constraint matrix selector.
  339. fn client_kind(client: &client_repo::Client) -> Result<ClientKind, AppError> {
  340. match client.auth_method() {
  341. Ok(TokenEndpointAuthMethod::None) => Ok(ClientKind::Public),
  342. Ok(_) => Ok(ClientKind::Confidential),
  343. Err(_) => Err(AppError::invalid_client_non_basic()),
  344. }
  345. }
  346. /// The client must be registered for the grant it is exercising.
  347. fn require_grant(client: &client_repo::Client, grant: &str) -> Result<(), AppError> {
  348. if client
  349. .grant_types
  350. .iter()
  351. .any(|registered| registered == grant)
  352. {
  353. Ok(())
  354. } else {
  355. Err(AppError::unauthorized_client(
  356. "client may not use this grant",
  357. ))
  358. }
  359. }
  360. /// A client registered for DPoP-bound tokens must present a fresh proof.
  361. ///
  362. /// Shared with the grants that authenticate their client internally
  363. /// (`authorization_code`, `device_code`) so the registration flag is enforced
  364. /// on every token-time path, not only the dispatcher-authenticated ones.
  365. pub(super) fn require_dpop_binding(
  366. client: &client_repo::Client,
  367. proof: &SenderProof,
  368. ) -> Result<(), AppError> {
  369. let proven = proof.dpop_jkt.as_deref().is_some_and(|jkt| !jkt.is_empty());
  370. if client.dpop_bound_access_tokens && !proven {
  371. Err(AppError::invalid_grant(
  372. "DPoP proof is required for this client",
  373. ))
  374. } else {
  375. Ok(())
  376. }
  377. }
  378. /// The enabled scope catalog as a set.
  379. async fn enabled_scopes(pool: &PgPool) -> Result<ScopeSet, AppError> {
  380. let names: Vec<String> = scope_repo::list_enabled(pool)
  381. .await?
  382. .into_iter()
  383. .map(|row| row.name)
  384. .collect();
  385. ScopeSet::from_names(&names)
  386. }
  387. /// Build the token service from the validated TTLs and key ring.
  388. fn token_service(state: &AppState) -> Result<TokenService, AppError> {
  389. TokenService::new(&state.config.ttl, &state.keys).map_err(AppError::from)
  390. }
  391. /// Build the JWT signer for a `Jwt` mint from the validated config: the
  392. /// active `jwt_signing_key` signs, previous keys verify only. Fail-closed
  393. /// `temporarily_unavailable` when no key is configured (the format gate
  394. /// guarantees this is unreachable in practice): never a 500, and the typed
  395. /// [`crate::jose::jwt_access::JwtError`] carries no key material.
  396. fn jwt_signer(state: &AppState) -> Result<JwtAccessSigner, AppError> {
  397. JwtAccessSigner::from_config(&state.config).map_err(|_| AppError::temporarily_unavailable())
  398. }
  399. /// `POST /token`: the five-grant dispatcher.
  400. ///
  401. /// # Errors
  402. ///
  403. /// Returns the RFC 6749 §5.2 error for the failing grant (or the RFC 8628
  404. /// §3.5 polling code), always with the correct HTTP status.
  405. pub async fn handler(
  406. State(state): State<AppState>,
  407. peer_certs: Option<Extension<PeerCertificates>>,
  408. headers: HeaderMap,
  409. body: axum::body::Bytes,
  410. ) -> Response {
  411. // `Bytes` + a manual parse replaces `Result<Form<TokenForm>, FormRejection>`:
  412. // the extractor consumed the raw body, hiding repeats. The explicit
  413. // content-type gate preserves the 415 `Form` rendered (`Bytes` never fails
  414. // on content-type); any other transport failure keeps the documented
  415. // `invalid_request` mapping ("includes a parameter more than once"). The
  416. // repeated `resource` indicator (RFC 8707) is captured in order by
  417. // `params::repeated_values` and validated fail-closed here; the
  418. // authorization-code branch binds the validated set to the redeemed code,
  419. // and the remaining grants wire it in later todos.
  420. if let Err(reject) = crate::protocol::params::require_form_content_type(&headers) {
  421. return reject.into_response();
  422. }
  423. let malformed = || {
  424. AppError::invalid_request(
  425. "the request body is malformed or includes a parameter more than once",
  426. )
  427. };
  428. let text = match std::str::from_utf8(&body) {
  429. Ok(text) => text,
  430. Err(_) => return TokenError::from(malformed()).into_response(),
  431. };
  432. let pairs = match crate::protocol::params::parse_pairs(text) {
  433. Ok(pairs) => pairs,
  434. Err(_) => return TokenError::from(malformed()).into_response(),
  435. };
  436. if crate::protocol::params::has_duplicate_field(&pairs, &TokenForm::KNOWN_FIELDS) {
  437. return TokenError::from(malformed()).into_response();
  438. }
  439. let map = crate::protocol::params::single_valued_map(&pairs);
  440. let form = TokenForm::from_map(&map);
  441. let resources =
  442. crate::protocol::params::repeated_values(text, crate::protocol::params::RESOURCE_PARAM);
  443. // RFC 8707 resource indicators do not apply to RFC 8693 token exchange;
  444. // reject the parameter BEFORE the catalog lookup so a bogus value cannot
  445. // surface `invalid_target` from a grant that must answer `invalid_request`.
  446. if form.grant_type.as_deref()
  447. == Some(crate::protocol::token::grants::token_exchange::GRANT_TYPE_TOKEN_EXCHANGE)
  448. && !resources.is_empty()
  449. {
  450. return TokenError::from(AppError::invalid_request(
  451. "resource is not supported by token-exchange",
  452. ))
  453. .into_response();
  454. }
  455. let aud = match crate::protocol::params::validate_resources(
  456. &state.pool,
  457. state.config.resource_indicators_enabled,
  458. &resources,
  459. )
  460. .await
  461. {
  462. Ok(aud) => aud,
  463. Err(error) => return TokenError::from(error).into_response(),
  464. };
  465. let mut conn = state.redis.clone();
  466. let store = Store::from_config(&state.config.redis);
  467. let now_unix = state.clock.now().unix_timestamp();
  468. let mut dispatch = Dispatch {
  469. state: &state,
  470. conn: &mut conn,
  471. store: &store,
  472. headers: &headers,
  473. form: &form,
  474. peer_certs: peer_certs.as_deref(),
  475. resources: &aud,
  476. cimd: state
  477. .config
  478. .cimd_enabled
  479. .then(|| crate::protocol::cimd::CimdSettings {
  480. token_prefix: &state.config.redis.token_key_prefix,
  481. max_bytes: state.config.cimd_max_bytes,
  482. timeout: state.config.cimd_fetch_timeout,
  483. allow_localhost: state.config.allow_localhost_redirect,
  484. }),
  485. now_unix,
  486. };
  487. match grants::dispatch_grant(&mut dispatch).await {
  488. Ok(body) => token_response(body),
  489. Err(error) => error.into_response(),
  490. }
  491. }
  492. /// Shape a success response: JSON plus the RFC 6749 §5.1 no-store headers.
  493. fn token_response(body: TokenResponseBody) -> Response {
  494. let mut response = Json(body).into_response();
  495. response
  496. .headers_mut()
  497. .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
  498. response
  499. .headers_mut()
  500. .insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
  501. response
  502. }
  503. #[cfg(test)]
  504. mod tests {
  505. use axum::http::StatusCode;
  506. use super::{TokenFormat, resolve_token_format};
  507. #[test]
  508. fn unknown_format_fails_closed_503() {
  509. let unknown = resolve_token_format("bogus", true)
  510. .err()
  511. .map(|error| error.status());
  512. assert_eq!(unknown, Some(StatusCode::SERVICE_UNAVAILABLE));
  513. let jwt_off = resolve_token_format("jwt", false)
  514. .err()
  515. .map(|error| error.status());
  516. assert_eq!(jwt_off, Some(StatusCode::SERVICE_UNAVAILABLE));
  517. assert!(matches!(
  518. resolve_token_format("opaque", false),
  519. Ok(TokenFormat::Opaque)
  520. ));
  521. assert!(matches!(
  522. resolve_token_format("opaque", true),
  523. Ok(TokenFormat::Opaque)
  524. ));
  525. assert!(matches!(
  526. resolve_token_format("jwt", true),
  527. Ok(TokenFormat::Jwt)
  528. ));
  529. }
  530. }