dcr.rs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. //! Dynamic client registration (RFC 7591 §2/§3.1/§3.2).
  2. //!
  3. //! `POST /register` mints a client for a caller holding an unexpired initial
  4. //! access token with remaining uses, or — only when
  5. //! `allow_open_registration` is set — for an anonymous caller inside a
  6. //! fixed-window Redis budget (the router's rate-limit layer in task 40 is the
  7. //! primary throttle; the in-handler budget here is defense in depth).
  8. //! Confidential clients must bind a token sender-constraint (mTLS or DPoP);
  9. //! `private_key_jwt` alone never satisfies it. Secrets leave this module
  10. //! exactly once (the 201 body) and persist only as Argon2id hashes; the
  11. //! registration access token persists only as a keyed hash on the client row
  12. //! so management (task 39) survives a Redis loss.
  13. //!
  14. //! `jwks_uri` values are fetched only through the SSRF guard
  15. //! (`protocol::ssrf`); inline `jwks` is preferred. RFC 7592 management
  16. //! (`GET`/`PUT`/`DELETE /register/{id}`) lives in [`manage`].
  17. #[cfg(test)]
  18. mod tests;
  19. pub mod manage;
  20. use std::collections::HashSet;
  21. use axum::Json;
  22. use axum::extract::State;
  23. use axum::http::{HeaderMap, StatusCode, header};
  24. use secrecy::ExposeSecret as _;
  25. use serde::{Deserialize, Serialize};
  26. use sqlx::PgPool;
  27. use time::OffsetDateTime;
  28. use uuid::Uuid;
  29. use crate::config::{Config, DcrRedirectPolicy};
  30. use crate::crypto::secret as secret_crypto;
  31. use crate::crypto::tokens as tokens_crypto;
  32. use crate::db::client_repo::{self, NewClient, TokenEndpointAuthMethod};
  33. use crate::db::{redirect_repo, scope_repo};
  34. use crate::error::AppError;
  35. use crate::protocol::ssrf;
  36. use crate::state::{AppState, RedisConn};
  37. use crate::store::{Store, ratelimit};
  38. /// Anonymous-registration fixed-window bucket (hashed before keying).
  39. const ANON_REGISTER_BUCKET: &str = "dcr-anon-register";
  40. /// Upper bound on `client_name` length.
  41. const MAX_CLIENT_NAME_LEN: usize = 256;
  42. /// Grant types this server can register (RFC 7591; no `client_credentials`).
  43. const SUPPORTED_GRANTS: [&str; 3] = [
  44. "authorization_code",
  45. "refresh_token",
  46. "urn:ietf:params:oauth:grant-type:device_code",
  47. ];
  48. /// Description stored on catalog rows created for dynamic URIs.
  49. const DYNAMIC_REDIRECT_DESCRIPTION: &str = "dynamic client registration";
  50. /// Datastore handles needed by [`register_client`].
  51. pub struct RegisterStores<'a> {
  52. /// Postgres pool for the client row, catalog reads, and attachments.
  53. pub pool: &'a PgPool,
  54. /// Redis connection for the IAT consume and the anonymous budget.
  55. pub conn: &'a mut RedisConn,
  56. /// Key layout plus retry/durability knobs.
  57. pub store: &'a Store,
  58. }
  59. /// Request-scoped registration inputs.
  60. pub struct RegisterContext<'a> {
  61. /// Validated runtime configuration (issuer, policies, peppers, budgets).
  62. pub config: &'a Config,
  63. /// Current time for issued-at stamps.
  64. pub now: OffsetDateTime,
  65. }
  66. /// RFC 7591 §2 registration request body.
  67. ///
  68. /// Unknown fields are ignored; client-asserted secrets, ids, or timestamps
  69. /// are never honored (the server mints every credential it returns).
  70. #[derive(Debug, Clone, Deserialize)]
  71. pub struct RegisterRequest {
  72. /// Requested redirect URIs (required, non-empty).
  73. pub redirect_uris: Option<Vec<String>>,
  74. /// Human-readable client name.
  75. pub client_name: Option<String>,
  76. /// Token-endpoint auth method (default `client_secret_basic`).
  77. pub token_endpoint_auth_method: Option<String>,
  78. /// Requested grant types (default `["authorization_code"]`).
  79. pub grant_types: Option<Vec<String>>,
  80. /// Requested response types (default `["code"]`; only `code` registers).
  81. pub response_types: Option<Vec<String>>,
  82. /// Space-delimited requested scope (subset of the enabled catalog).
  83. pub scope: Option<String>,
  84. /// Inline JWKS document (XOR `jwks_uri`).
  85. pub jwks: Option<serde_json::Value>,
  86. /// Remote JWKS URI (XOR `jwks`; fetched only through the SSRF guard).
  87. pub jwks_uri: Option<String>,
  88. /// RFC 8705 mTLS subject DN (requires `tls_client_auth`).
  89. pub tls_client_auth_subject_dn: Option<String>,
  90. /// DPoP binding signal (RFC 9449): the client opts into DPoP-bound tokens.
  91. pub dpop_bound_access_tokens: Option<bool>,
  92. /// Contact addresses, stored verbatim.
  93. pub contacts: Option<serde_json::Value>,
  94. /// Pre-registered JAR `request_uri` values (RFC 9101), stored verbatim.
  95. pub request_uris: Option<serde_json::Value>,
  96. /// Software identifier, stored verbatim.
  97. pub software_id: Option<String>,
  98. }
  99. /// RFC 7591 §3.2.1 registration response (HTTP 201).
  100. ///
  101. /// `client_secret` appears only for secret-issued confidential clients and is
  102. /// never persisted or returned again; `registration_access_token` is shown
  103. /// exactly once (only its keyed hash is stored).
  104. #[derive(Debug, Clone, Serialize)]
  105. pub struct RegisterResponse {
  106. /// The minted public client identifier.
  107. pub client_id: String,
  108. /// The minted client secret (secret-issued confidential clients only).
  109. #[serde(skip_serializing_if = "Option::is_none")]
  110. pub client_secret: Option<String>,
  111. /// When the minted secret expires, if it does.
  112. #[serde(skip_serializing_if = "Option::is_none")]
  113. pub client_secret_expires_at: Option<i64>,
  114. /// The RFC 7592 management token, shown exactly once.
  115. pub registration_access_token: String,
  116. /// The RFC 7592 management URI for this client.
  117. pub registration_client_uri: String,
  118. /// Effective attached redirect URIs.
  119. pub redirect_uris: Vec<String>,
  120. /// Registered client name.
  121. pub client_name: String,
  122. /// Registered token-endpoint authentication method.
  123. pub token_endpoint_auth_method: String,
  124. /// Registered grant types.
  125. pub grant_types: Vec<String>,
  126. /// Registered response types.
  127. pub response_types: Vec<String>,
  128. /// Registered scope string.
  129. pub scope: String,
  130. /// Registered inline JWKS, if any.
  131. #[serde(skip_serializing_if = "Option::is_none")]
  132. pub jwks: Option<serde_json::Value>,
  133. /// Registered JWKS URI, if any.
  134. #[serde(skip_serializing_if = "Option::is_none")]
  135. pub jwks_uri: Option<String>,
  136. /// Registered mTLS subject DN, if any.
  137. #[serde(skip_serializing_if = "Option::is_none")]
  138. pub tls_client_auth_subject_dn: Option<String>,
  139. /// Whether the client registered DPoP-bound tokens.
  140. pub dpop_bound_access_tokens: bool,
  141. /// When the client identifier was issued (unix seconds).
  142. pub client_id_issued_at: i64,
  143. }
  144. /// Validated registration ready to persist.
  145. struct ValidatedRegistration {
  146. method: TokenEndpointAuthMethod,
  147. grant_types: Vec<String>,
  148. scope: String,
  149. client_name: String,
  150. jwks: Option<serde_json::Value>,
  151. jwks_uri: Option<String>,
  152. subject_dn: Option<String>,
  153. dpop_bound: bool,
  154. contacts: Option<serde_json::Value>,
  155. request_uris: Option<serde_json::Value>,
  156. software_id: Option<String>,
  157. redirect_uris: Vec<String>,
  158. }
  159. /// Axum entry point for `POST /register` (mounted by the router in task 40).
  160. ///
  161. /// # Errors
  162. ///
  163. /// Returns `invalid_client` (401 + `WWW-Authenticate: Bearer`) for a missing
  164. /// or invalid initial access token, `invalid_redirect_uri` /
  165. /// `invalid_client_metadata` (400) for bad metadata, `temporarily_unavailable`
  166. /// (503) when an anonymous budget is exhausted or a dependency fails closed,
  167. /// and `server_error` (500, masked) for unexpected internal failures.
  168. pub async fn register_handler(
  169. State(state): State<AppState>,
  170. headers: HeaderMap,
  171. Json(request): Json<RegisterRequest>,
  172. ) -> Result<(StatusCode, Json<RegisterResponse>), AppError> {
  173. let token = bearer_token(&headers);
  174. let mut conn = state.redis.clone();
  175. let store = Store::from_config(&state.config.redis);
  176. let mut stores = RegisterStores {
  177. pool: &state.pool,
  178. conn: &mut conn,
  179. store: &store,
  180. };
  181. let ctx = RegisterContext {
  182. config: &state.config,
  183. now: state.clock.now(),
  184. };
  185. let response = register_client(&mut stores, &ctx, token, &request).await?;
  186. Ok((StatusCode::CREATED, Json(response)))
  187. }
  188. /// Register a client: authorize the caller, validate metadata, persist the
  189. /// row (secret and registration token as hashes only), attach redirects.
  190. ///
  191. /// A spent initial-access use is burned even when later validation fails: a
  192. /// use authorizes one registration call, not one successful call.
  193. ///
  194. /// # Errors
  195. ///
  196. /// Returns the same errors as [`register_handler`].
  197. pub async fn register_client(
  198. stores: &mut RegisterStores<'_>,
  199. ctx: &RegisterContext<'_>,
  200. initial_access_token: Option<&str>,
  201. request: &RegisterRequest,
  202. ) -> Result<RegisterResponse, AppError> {
  203. authorize_caller(stores, ctx.config, initial_access_token).await?;
  204. let validated = validate_metadata(stores.pool, request).await?;
  205. let redirect_ids = resolve_redirects(stores.pool, ctx.config, &validated.redirect_uris).await?;
  206. let client_id = tokens_crypto::mint_token().map_err(AppError::internal)?;
  207. let registration_token = tokens_crypto::mint_token().map_err(AppError::internal)?;
  208. let registration_hash = keyed_hash(ctx.config, &registration_token)?;
  209. let secret_hash = match validated.method {
  210. TokenEndpointAuthMethod::ClientSecretBasic | TokenEndpointAuthMethod::ClientSecretPost => {
  211. let secret = tokens_crypto::mint_token().map_err(AppError::internal)?;
  212. let hash = hash_secret(ctx.config, &secret).await?;
  213. Some((secret, hash))
  214. }
  215. TokenEndpointAuthMethod::None
  216. | TokenEndpointAuthMethod::PrivateKeyJwt
  217. | TokenEndpointAuthMethod::TlsClientAuth
  218. | TokenEndpointAuthMethod::SelfSignedTlsClientAuth => None,
  219. };
  220. let new = NewClient {
  221. id: Uuid::new_v4(),
  222. client_id: client_id.clone(),
  223. client_secret_hash: secret_hash.as_ref().map(|(_, hash)| hash.clone()),
  224. client_secret_expires_at: None,
  225. client_name: validated.client_name.clone(),
  226. grant_types: validated.grant_types.clone(),
  227. response_types: vec!["code".to_owned()],
  228. scope: validated.scope.clone(),
  229. token_endpoint_auth_method: validated.method.as_str().to_owned(),
  230. jwks: validated.jwks.clone(),
  231. jwks_uri: validated.jwks_uri.clone(),
  232. tls_client_auth_subject_dn: validated.subject_dn.clone(),
  233. tls_client_auth_cert_thumbprint: None,
  234. request_uris: validated.request_uris.clone(),
  235. contacts: validated.contacts.clone(),
  236. software_id: validated.software_id.clone(),
  237. dpop_bound_access_tokens: validated.dpop_bound,
  238. registration_access_token_hash: Some(registration_hash),
  239. };
  240. let row = client_repo::insert(
  241. stores.pool,
  242. &ctx.config.keys,
  243. ctx.config.pii_dual_write,
  244. &new,
  245. )
  246. .await?;
  247. for id in &redirect_ids {
  248. redirect_repo::attach(stores.pool, &client_id, *id, None).await?;
  249. }
  250. Ok(RegisterResponse {
  251. client_id: row.client_id,
  252. client_secret: secret_hash.map(|(secret, _)| secret),
  253. client_secret_expires_at: None,
  254. registration_access_token: registration_token,
  255. registration_client_uri: format!("{}/register/{client_id}", ctx.config.issuer),
  256. redirect_uris: validated.redirect_uris.clone(),
  257. client_name: row.client_name,
  258. token_endpoint_auth_method: validated.method.as_str().to_owned(),
  259. grant_types: validated.grant_types,
  260. response_types: vec!["code".to_owned()],
  261. scope: validated.scope,
  262. jwks: validated.jwks,
  263. jwks_uri: validated.jwks_uri,
  264. tls_client_auth_subject_dn: validated.subject_dn,
  265. dpop_bound_access_tokens: validated.dpop_bound,
  266. client_id_issued_at: ctx.now.unix_timestamp(),
  267. })
  268. }
  269. /// Authorize a registration call: spend one use of a valid initial access
  270. /// token, or — only under `allow_open_registration` — spend from the
  271. /// anonymous fixed-window budget.
  272. async fn authorize_caller(
  273. stores: &mut RegisterStores<'_>,
  274. config: &Config,
  275. initial_access_token: Option<&str>,
  276. ) -> Result<(), AppError> {
  277. if let Some(token) = initial_access_token {
  278. let hash = keyed_hash(config, token)?;
  279. match crate::store::dcr::consume_one_use(stores.conn, stores.store, &hash).await? {
  280. Some(_) => Ok(()),
  281. None => Err(AppError::invalid_client_bearer()),
  282. }
  283. } else if config.allow_open_registration {
  284. match ratelimit::check_and_incr(
  285. stores.conn,
  286. stores.store,
  287. ANON_REGISTER_BUCKET,
  288. config.rate_limit.window,
  289. u64::from(config.rate_limit.per_ip),
  290. )
  291. .await
  292. {
  293. Ok(decision) if decision.allowed => Ok(()),
  294. Ok(_) => Err(AppError::temporarily_unavailable()),
  295. Err(error) => Err(AppError::from(error)),
  296. }
  297. } else {
  298. Err(AppError::invalid_client_bearer())
  299. }
  300. }
  301. /// Validate every metadata field except redirects: auth method, grants and
  302. /// their compatibility with the method, response types, scope subset, the
  303. /// `jwks`/`jwks_uri` exclusive-or, mTLS fields, and — for confidential
  304. /// clients — the mandatory sender-constraint binding.
  305. async fn validate_metadata(
  306. pool: &PgPool,
  307. request: &RegisterRequest,
  308. ) -> Result<ValidatedRegistration, AppError> {
  309. let method = match request.token_endpoint_auth_method.as_deref() {
  310. None => TokenEndpointAuthMethod::ClientSecretBasic,
  311. Some(value) => TokenEndpointAuthMethod::try_from(value)
  312. .map_err(|_| AppError::invalid_client_metadata("unsupported token_endpoint_auth_method"))?,
  313. };
  314. let grant_types = dedupe_strings(
  315. request
  316. .grant_types
  317. .clone()
  318. .unwrap_or_else(|| vec!["authorization_code".to_owned()]),
  319. );
  320. if grant_types.is_empty() {
  321. return Err(AppError::invalid_client_metadata(
  322. "grant_types must not be empty",
  323. ));
  324. }
  325. for grant in &grant_types {
  326. if !SUPPORTED_GRANTS.contains(&grant.as_str()) {
  327. return Err(AppError::invalid_client_metadata("unsupported grant type"));
  328. }
  329. }
  330. let response_types = request
  331. .response_types
  332. .clone()
  333. .unwrap_or_else(|| vec!["code".to_owned()]);
  334. if response_types.is_empty() || response_types.iter().any(|kind| kind != "code") {
  335. return Err(AppError::invalid_client_metadata(
  336. "only response_types [\"code\"] is supported",
  337. ));
  338. }
  339. let scope = validate_scope(pool, request.scope.as_deref()).await?;
  340. if request.jwks.is_some() && request.jwks_uri.is_some() {
  341. return Err(AppError::invalid_client_metadata(
  342. "jwks and jwks_uri are mutually exclusive",
  343. ));
  344. }
  345. let jwks = match request.jwks.as_ref() {
  346. Some(value) => {
  347. validate_jwks_shape(value)?;
  348. Some(value.clone())
  349. }
  350. None => None,
  351. };
  352. let jwks_uri = match request.jwks_uri.as_deref() {
  353. Some(uri) => {
  354. let fetched = fetch_jwks_uri(uri).await?;
  355. Some((uri.to_owned(), fetched))
  356. }
  357. None => None,
  358. };
  359. if matches!(method, TokenEndpointAuthMethod::PrivateKeyJwt)
  360. && jwks.is_none()
  361. && jwks_uri.is_none()
  362. {
  363. return Err(AppError::invalid_client_metadata(
  364. "private_key_jwt requires jwks or jwks_uri",
  365. ));
  366. }
  367. if matches!(method, TokenEndpointAuthMethod::SelfSignedTlsClientAuth)
  368. && jwks.is_none()
  369. && jwks_uri.is_none()
  370. {
  371. return Err(AppError::invalid_client_metadata(
  372. "self_signed_tls_client_auth requires jwks or jwks_uri",
  373. ));
  374. }
  375. let subject_dn = match request.tls_client_auth_subject_dn.as_deref() {
  376. Some(dn) if dn.trim().is_empty() => {
  377. return Err(AppError::invalid_client_metadata(
  378. "tls_client_auth_subject_dn must not be empty",
  379. ));
  380. }
  381. Some(dn) => {
  382. if !matches!(method, TokenEndpointAuthMethod::TlsClientAuth) {
  383. return Err(AppError::invalid_client_metadata(
  384. "tls_client_auth_subject_dn requires tls_client_auth",
  385. ));
  386. }
  387. Some(dn.trim().to_owned())
  388. }
  389. None => None,
  390. };
  391. if matches!(method, TokenEndpointAuthMethod::TlsClientAuth) && subject_dn.is_none() {
  392. return Err(AppError::invalid_client_metadata(
  393. "tls_client_auth requires tls_client_auth_subject_dn",
  394. ));
  395. }
  396. let dpop_bound = request.dpop_bound_access_tokens.unwrap_or(false);
  397. let confidential = !matches!(method, TokenEndpointAuthMethod::None);
  398. let mtls_bound = matches!(
  399. method,
  400. TokenEndpointAuthMethod::TlsClientAuth | TokenEndpointAuthMethod::SelfSignedTlsClientAuth
  401. );
  402. if confidential && !(mtls_bound || dpop_bound) {
  403. return Err(AppError::invalid_client_metadata(
  404. "confidential clients must register a sender-constraint binding \
  405. (tls_client_auth, self_signed_tls_client_auth, or dpop_bound_access_tokens)",
  406. ));
  407. }
  408. let client_name = request.client_name.clone().unwrap_or_default();
  409. if client_name.len() > MAX_CLIENT_NAME_LEN {
  410. return Err(AppError::invalid_client_metadata("client_name is too long"));
  411. }
  412. let redirect_uris = request.redirect_uris.clone().unwrap_or_default();
  413. if redirect_uris.is_empty() {
  414. return Err(AppError::invalid_redirect_uri(
  415. "redirect_uris must not be empty",
  416. ));
  417. }
  418. Ok(ValidatedRegistration {
  419. method,
  420. grant_types,
  421. scope,
  422. client_name,
  423. jwks: jwks.or_else(|| jwks_uri.as_ref().map(|(_, fetched)| fetched.clone())),
  424. jwks_uri: jwks_uri.map(|(uri, _)| uri),
  425. subject_dn,
  426. dpop_bound,
  427. contacts: request.contacts.clone(),
  428. request_uris: request.request_uris.clone(),
  429. software_id: request.software_id.clone(),
  430. redirect_uris,
  431. })
  432. }
  433. /// Check the requested scope string against the enabled catalog: every token
  434. /// must be an enabled scope.
  435. async fn validate_scope(pool: &PgPool, scope: Option<&str>) -> Result<String, AppError> {
  436. let requested: Vec<&str> = scope.unwrap_or_default().split_whitespace().collect();
  437. if requested.is_empty() {
  438. return Ok(String::new());
  439. }
  440. let enabled: HashSet<String> = scope_repo::list_enabled(pool)
  441. .await?
  442. .into_iter()
  443. .map(|record| record.name)
  444. .collect();
  445. for token in &requested {
  446. if !enabled.contains(*token) {
  447. return Err(AppError::invalid_client_metadata("unknown scope"));
  448. }
  449. }
  450. Ok(requested.join(" "))
  451. }
  452. /// Resolve requested redirects to catalog ids: catalog hits attach directly;
  453. /// anything else follows `dcr_redirect_policy` (`open` and dynamic loopback
  454. /// URIs are upserted into the catalog, then attached). An upsert that lands
  455. /// on an admin-disabled row is rejected rather than resurrected.
  456. async fn resolve_redirects(
  457. pool: &PgPool,
  458. config: &Config,
  459. redirect_uris: &[String],
  460. ) -> Result<Vec<Uuid>, AppError> {
  461. let catalog = redirect_repo::list(pool, &config.keys, false).await?;
  462. let mut ids = Vec::with_capacity(redirect_uris.len());
  463. for uri in redirect_uris {
  464. if let Some(hit) = catalog.iter().find(|row| row.uri == *uri) {
  465. ids.push(hit.id);
  466. continue;
  467. }
  468. match config.dcr_redirect_policy {
  469. DcrRedirectPolicy::Catalog => {
  470. return Err(AppError::invalid_redirect_uri(
  471. "redirect URI is not in the catalog",
  472. ));
  473. }
  474. DcrRedirectPolicy::CatalogLoopback => {
  475. if !is_loopback_uri(uri) {
  476. return Err(AppError::invalid_redirect_uri(
  477. "redirect URI is not in the catalog or a loopback URI",
  478. ));
  479. }
  480. }
  481. DcrRedirectPolicy::Open => {
  482. if !is_open_redirect_uri(uri) {
  483. return Err(AppError::invalid_redirect_uri(
  484. "redirect URI must be an absolute https URI or a loopback URI",
  485. ));
  486. }
  487. }
  488. }
  489. let (row, _) = redirect_repo::upsert(
  490. pool,
  491. &config.keys,
  492. config.pii_dual_write,
  493. &redirect_repo::NewRedirectUri {
  494. id: Uuid::new_v4(),
  495. uri,
  496. kind: redirect_repo::RedirectKind::Authorize,
  497. description: DYNAMIC_REDIRECT_DESCRIPTION,
  498. created_by: None,
  499. },
  500. )
  501. .await?;
  502. if !row.enabled {
  503. return Err(AppError::invalid_redirect_uri(
  504. "redirect URI is disabled by the administrator",
  505. ));
  506. }
  507. ids.push(row.id);
  508. }
  509. Ok(ids)
  510. }
  511. /// Whether `uri` is an RFC 8252 §7.3 loopback URI: plaintext `http` to a
  512. /// loopback literal (`127.0.0.1` or `[::1]`, any port) with no userinfo,
  513. /// query, or fragment. `localhost` is never loopback here.
  514. fn is_loopback_uri(uri: &str) -> bool {
  515. let (before_fragment, fragment) = match uri.split_once('#') {
  516. Some((before, fragment)) => (before, Some(fragment)),
  517. None => (uri, None),
  518. };
  519. if fragment.is_some() {
  520. return false;
  521. }
  522. let (before_query, query) = match before_fragment.split_once('?') {
  523. Some((before, query)) => (before, Some(query)),
  524. None => (before_fragment, None),
  525. };
  526. if query.is_some() {
  527. return false;
  528. }
  529. let rest = match strip_scheme_prefix(before_query, "http") {
  530. Some(rest) => rest,
  531. None => return false,
  532. };
  533. let after_slashes = match rest.strip_prefix("//") {
  534. Some(after) => after,
  535. None => return false,
  536. };
  537. let authority = match after_slashes.find('/') {
  538. Some(index) => &after_slashes[..index],
  539. None => after_slashes,
  540. };
  541. if authority.contains('@') {
  542. return false;
  543. }
  544. if let Some(inner) = authority.strip_prefix('[') {
  545. match inner.find(']') {
  546. Some(close) => {
  547. &inner[..close] == "::1"
  548. && (inner.len() == close + 1 || inner.as_bytes().get(close + 1) == Some(&b':'))
  549. }
  550. None => false,
  551. }
  552. } else {
  553. match authority.split_once(':') {
  554. Some((host, _)) => host == "127.0.0.1",
  555. None => authority == "127.0.0.1",
  556. }
  557. }
  558. }
  559. /// Whether `uri` may be registered under the `open` DCR redirect policy: an
  560. /// RFC 8252 loopback URI, or an absolute `https` URI with a non-empty host
  561. /// and no userinfo, fragment, or whitespace. Accepting an arbitrary absolute
  562. /// URI here would turn the opt-in `open` policy into an open-redirect
  563. /// factory.
  564. fn is_open_redirect_uri(uri: &str) -> bool {
  565. if is_loopback_uri(uri) {
  566. return true;
  567. }
  568. if uri.contains('#') || uri.chars().any(char::is_whitespace) {
  569. return false;
  570. }
  571. let Some(rest) = strip_scheme_prefix(uri, "https") else {
  572. return false;
  573. };
  574. let Some(after_slashes) = rest.strip_prefix("//") else {
  575. return false;
  576. };
  577. let authority = match after_slashes.find(['/', '?']) {
  578. Some(index) => &after_slashes[..index],
  579. None => after_slashes,
  580. };
  581. if authority.contains('@') {
  582. return false;
  583. }
  584. if let Some(inner) = authority.strip_prefix('[') {
  585. match inner.find(']') {
  586. Some(close) => {
  587. close > 0 && (inner.len() == close + 1 || inner.as_bytes().get(close + 1) == Some(&b':'))
  588. }
  589. None => false,
  590. }
  591. } else {
  592. match authority.split_once(':') {
  593. Some((host, _)) => !host.is_empty(),
  594. None => !authority.is_empty(),
  595. }
  596. }
  597. }
  598. /// Strip a case-insensitive `scheme:` prefix without ever slicing at a
  599. /// non-character boundary.
  600. fn strip_scheme_prefix<'a>(uri: &'a str, scheme: &str) -> Option<&'a str> {
  601. let head = uri.get(..scheme.len())?;
  602. if !head.eq_ignore_ascii_case(scheme) {
  603. return None;
  604. }
  605. uri.get(scheme.len()..)?.strip_prefix(':')
  606. }
  607. /// Require an inline JWKS document to carry a non-empty `keys` array.
  608. /// Signature and key-use validation belong to the assertion/mTLS verifiers.
  609. fn validate_jwks_shape(value: &serde_json::Value) -> Result<(), AppError> {
  610. let has_keys = value
  611. .get("keys")
  612. .and_then(|keys| keys.as_array())
  613. .is_some_and(|keys| !keys.is_empty());
  614. if has_keys {
  615. Ok(())
  616. } else {
  617. Err(AppError::invalid_client_metadata(
  618. "jwks must contain a non-empty keys array",
  619. ))
  620. }
  621. }
  622. /// Fetch a `jwks_uri` document through the SSRF guard and require the same
  623. /// JWKS shape as an inline document. Guard/fetch failures are fail-closed
  624. /// and report no fetch internals to the client.
  625. async fn fetch_jwks_uri(uri: &str) -> Result<serde_json::Value, AppError> {
  626. let bytes = ssrf::fetch_guarded_bytes(uri)
  627. .await
  628. .map_err(|_| AppError::invalid_client_metadata("jwks_uri fetch failed"))?;
  629. let document: serde_json::Value = serde_json::from_slice(&bytes)
  630. .map_err(|_| AppError::invalid_client_metadata("jwks_uri did not return JSON"))?;
  631. validate_jwks_shape(&document)?;
  632. Ok(document)
  633. }
  634. /// Random 256-bit client secrets are hashed with Argon2id off the async
  635. /// worker; only the PHC string is stored.
  636. async fn hash_secret(config: &Config, secret: &str) -> Result<String, AppError> {
  637. let params = secret_crypto::Argon2Params::new(
  638. config.argon2.memory_kib,
  639. config.argon2.iterations,
  640. config.argon2.parallelism,
  641. 32,
  642. )
  643. .map_err(AppError::internal)?;
  644. let owned = secret.to_owned();
  645. tokio::task::spawn_blocking(move || secret_crypto::hash_secret(&owned, &params))
  646. .await
  647. .map_err(AppError::internal)?
  648. .map_err(AppError::internal)
  649. }
  650. /// Keyed token hash for at-rest storage under the active pepper — the same
  651. /// scheme the CLI uses when issuing initial access tokens, so registration
  652. /// validates exactly what issuance stored.
  653. fn keyed_hash(config: &Config, token: &str) -> Result<String, AppError> {
  654. let (key_id, pepper) = config
  655. .keys
  656. .active_pepper()
  657. .ok_or_else(AppError::server_error)?;
  658. let generation = key_id
  659. .as_str()
  660. .trim_start_matches('v')
  661. .parse::<u32>()
  662. .unwrap_or(1);
  663. let hash = tokens_crypto::token_hash(
  664. &tokens_crypto::KeyId::new(generation),
  665. pepper.expose_secret().as_bytes(),
  666. token,
  667. );
  668. Ok(hash.to_storage_string())
  669. }
  670. /// Extract a Bearer token from an `Authorization` header, if well-formed.
  671. fn bearer_token(headers: &HeaderMap) -> Option<&str> {
  672. let value = headers.get(header::AUTHORIZATION)?.to_str().ok()?;
  673. let token = value.strip_prefix("Bearer ")?.trim();
  674. if token.is_empty() { None } else { Some(token) }
  675. }
  676. /// Deduplicate strings preserving first-seen order.
  677. fn dedupe_strings(values: Vec<String>) -> Vec<String> {
  678. let mut seen = HashSet::with_capacity(values.len());
  679. let mut out = Vec::with_capacity(values.len());
  680. for value in values {
  681. if seen.insert(value.clone()) {
  682. out.push(value);
  683. }
  684. }
  685. out
  686. }