···33 io,44 net::{TcpListener, ToSocketAddrs as _},55 ops,66- sync::{Arc, Mutex, MutexGuard, RwLock},77- time::Duration,66+ sync::{Arc, Mutex, RwLock},87};98109use atproto::{did::Did, tid::Tid};1111-use auth::jwt;1210use bytes::Bytes;1111+use futures_util::{FutureExt, future::BoxFuture};1312use identity::{HttpClient, Resolver};1413use jetstream::{JetstreamClient, client_config::JetstreamConfig};1514use lexicon::sh::tangled::{git::RefUpdate, repo::Repo};···2122use crate::{2223 services::{2324 atrepo,2424- authorization::AuthorizationClaimsStore,2525+ authorization::{AuthorizationClaimsStore, AuthorizationClaimsStoreError},2526 database::{DataStore, DataStoreError},2627 },2728 types::{repository_key::RepositoryKey, repository_path::RepositoryPath},···6869 pool: ThreadPool,69707071 events: tokio::sync::broadcast::Sender<(i64, OffsetDateTime, Event)>,7171-7272- /// Stores JWT claims to prevent re-use.7373- jwt_claims: Mutex<HashMap<Box<str>, jwt::Claims>>,74727573 /// Resolved repository path lookup cache.7674 ///···124128 store: database,125129 pool,126130 events,127127- jwt_claims: Default::default(),128131 repo_cache: Default::default(),129132 push_seed: Default::default(),130133 private_addrs,···146151 let internal = crate::private::router().with_state(Arc::clone(&inner).into());147152 tasks.spawn(crate::serve_all(internal, private_listeners))148153 };149149-150150- let state = Arc::clone(&inner);151151- tasks.spawn(async move {152152- use tokio_stream::wrappers::IntervalStream;153153-154154- const EXPIRY_SLOP: i64 = 60;155155-156156- let mut interval = tokio::time::interval(Duration::from_secs(120));157157- interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);158158-159159- let mut interval = IntervalStream::new(interval);160160- while interval.next().await.is_some() {161161- let now = OffsetDateTime::now_utc().unix_timestamp() + EXPIRY_SLOP;162162-163163- let mut store = state.jwt_claims.lock().unwrap();164164- let before = store.len();165165- store.retain(|_, claims| claims.exp > now);166166- match before - store.len() {167167- 0 => {}168168- n => tracing::debug!("evicted {n} expired jti claims"),169169- }170170- }171171-172172- Ok(())173173- });174154175155 Ok((inner, tasks))176156 }···178208 #[inline]179209 pub fn store(&self) -> &DataStore {180210 &self.store181181- }182182-183183- /// Acquire a lock on the jwt claims store.184184- pub fn jwt_claims(&self) -> MutexGuard<'_, HashMap<Box<str>, jwt::Claims>> {185185- self.jwt_claims.lock().unwrap()186211 }187212188213 pub async fn fetch_public_keys(&self, did: &Did) -> anyhow::Result<()> {···503538}504539505540impl AuthorizationClaimsStore<auth::jwt::Claims> for KnotState {506506- #[inline]507507- fn get_unexpired_claims(&self, jti: &str, now: i64) -> Option<auth::jwt::Claims> {508508- let mut store = self.jwt_claims();509509- let claims = store.get(jti).cloned();541541+ fn get_unexpired_claims<'a: 'b, 'b>(542542+ &'a self,543543+ jti: &'b str,544544+ now: i64,545545+ ) -> BoxFuture<'b, Result<Option<auth::jwt::Claims>, AuthorizationClaimsStoreError>> {546546+ async move {547547+ let claims = self.store().get_claims(jti, now).await.ok().flatten();510548511511- // If the claims have expired, remove them.512512- if matches!(&claims, Some(claims) if claims.exp < now) {513513- store.remove(jti);514514- return None;549549+ // If the claims have expired, remove them.550550+ if matches!(&claims, Some(claims) if claims.exp < now) {551551+ self.store()552552+ .delete_claims(jti)553553+ .await554554+ .map_err(|error| AuthorizationClaimsStoreError(error.into()))?;555555+556556+ return Ok(None);557557+ }558558+559559+ Ok(claims)515560 }516516-517517- claims561561+ .boxed()518562 }519563520520- #[inline]521521- fn store_claims(&self, claims: auth::jwt::Claims) {522522- self.jwt_claims().insert(claims.jti.clone(), claims);564564+ fn store_claims(565565+ &self,566566+ claims: auth::jwt::Claims,567567+ now: i64,568568+ ) -> BoxFuture<'_, Result<(), AuthorizationClaimsStoreError>> {569569+ async move {570570+ self.store()571571+ .store_claims(claims, now)572572+ .await573573+ .map_err(|error| AuthorizationClaimsStoreError(error.into()))?;574574+575575+ Ok(())576576+ }577577+ .boxed()523578 }524579}525580
+6-5
crates/knot/src/public/git/authorization.rs
···5050 // Before performing a relatively expensive DID look-up, ensure the token5151 // claims are valid.5252 let unverified_claims = unverified_token.claims;5353- GitVerification::verify(&knot, now, knot.instance_audience(), &unverified_claims).map_err(5454- |error| match error {5353+ GitVerification::verify(&knot, now, knot.instance_audience(), &unverified_claims)5454+ .await5555+ .map_err(|error| match error {5556 // Git re-uses the token from the credential helper for each request in a single push.5657 //5758 // Returning 'Forbidden' here will make git abort. Instead, we return an Unauthorized5859 // which will force git to get a new token from the credential helper.5960 VerificationError::Reused => Error::unauthorized(&knot, "authorization re-used"),6061 error => Error::forbidden(&knot, error.to_string()),6161- },6262- )?;6262+ })?;63636464 // Resolve the DID document for the claimed issuer, extract and parse6565 // the verification methods into public keys.···97979898 // Re-verify the claims for the sake of paranoia.9999 GitVerification::verify(&knot, now, knot.instance_audience(), &claims)100100+ .await100101 .map_err(|error| Error::forbidden(&knot, error.to_string()))?;101102102103 // Store the JWT so it cannot be re-used within the claim period.103103- knot.store_claims(claims.clone());104104+ knot.store_claims(claims.clone(), now).await?;104105 return Ok(Self(claims));105106 }106107 }