don't
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: move service-auth specific structures out of generic jwt module

Signed-off-by: tjh <x@tjh.dev>

tjh 57322576 d2db43d2

+262 -187
+1 -1
crates/gordian-auth/src/client.rs
··· 315 315 316 316 jwt::encode_and_sign( 317 317 serde_json::json!({ 318 - "typ": jwt::Type::Jwt, 318 + "typ": jwt::Type::JWT, 319 319 "alg": jwt::Algorithm::ES256, 320 320 "kid": key_id 321 321 }),
+61 -75
crates/gordian-auth/src/jwt.rs
··· 2 2 3 3 use aws_lc_rs::rand::SystemRandom; 4 4 use aws_lc_rs::signature::EcdsaKeyPair; 5 - use data_encoding::BASE64URL_NOPAD as Encoding; 6 - use gordian_types::DidBuf; 7 - use gordian_types::Nsid; 8 5 use serde::Deserialize; 9 6 use serde::Serialize; 10 7 use serde::de::DeserializeOwned; 11 8 12 9 use crate::error::Unspecified; 13 10 use crate::jwk::JsonWebKey; 11 + use crate::serde::base64::ENCODING; 14 12 use crate::verification_key::VerificationKey; 13 + 14 + pub trait TokenHeader: DeserializeOwned + Serialize { 15 + fn algorithm(&self) -> &Algorithm; 16 + } 17 + 18 + pub trait TokenClaims: DeserializeOwned + Serialize {} 15 19 16 20 #[derive(Debug, Default, PartialEq, Eq, Deserialize, Serialize)] 17 21 pub enum Type { 18 22 #[default] 19 - #[serde(rename = "JWT")] 20 - Jwt, 21 - #[serde(rename = "dpop+jwt")] 22 - DpopJwt, 23 + JWT, 23 24 } 24 25 25 26 /// Signature algorithm. 26 - /// 27 - /// See: <https://atproto.com/specs/xrpc#inter-service-authentication-jwt> 28 27 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] 29 28 #[non_exhaustive] 30 29 pub enum Algorithm { ··· 36 37 } 37 38 38 39 impl fmt::Display for Algorithm { 39 - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 41 fmt::Debug::fmt(self, f) 41 42 } 42 43 } 43 44 44 - /// See: <https://docs.rs/jose-jwk/latest/jose_jwk/enum.OkpCurves.html> 45 + /// Ref: <https://docs.rs/jose-jwk/latest/jose_jwk/enum.OkpCurves.html> 45 46 #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] 46 47 #[non_exhaustive] 47 48 pub enum Curve { ··· 65 66 #[must_use] 66 67 pub const fn new(alg: Algorithm, crv: Option<Curve>) -> Self { 67 68 Self { 68 - typ: Type::Jwt, 69 + typ: Type::JWT, 69 70 alg, 70 71 crv, 71 72 jwk: None, ··· 73 74 } 74 75 } 75 76 76 - /// Standard claims for inter-service authentication (JWT). 77 - /// 78 - /// See: <https://atproto.com/specs/xrpc#inter-service-authentication-jwt> 79 - #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] 80 - pub struct Claims { 81 - /// Account DID associated with the service that the request is being 82 - /// sent to. 83 - pub iss: DidBuf, 84 - 85 - /// Service DID associated with the service that the request is being 86 - /// sent to. 87 - pub aud: DidBuf, 88 - 89 - /// Token creation time as a UNIX timestamp. 90 - pub iat: i64, 91 - 92 - /// Token expiration time as a UNIX timestamp. 93 - pub exp: i64, 94 - 95 - /// Lexicon method in NSID syntax. 96 - #[serde(skip_serializing_if = "Option::is_none")] 97 - pub lxm: Option<Box<Nsid>>, 98 - 99 - /// Nonce. 100 - pub jti: Box<str>, 101 - } 102 - 103 77 #[derive(Debug, Deserialize, Serialize)] 104 - pub struct Token<C = Claims> { 105 - pub header: Header, 78 + pub struct Token<H, C> { 79 + pub header: H, 106 80 pub claims: C, 107 81 } 108 82 109 - impl Token<Claims> { 83 + impl<H: TokenHeader, C: TokenClaims> Token<H, C> { 110 84 /// Deserialize [`Self`] from `token` after verifying the `token`'s 111 85 /// signature with `public_key`. 112 86 /// ··· 87 115 /// 88 116 /// Returns an error if the token cannot be deserialized or if the signature 89 117 /// verification fails. 90 - pub fn decode( 91 - token: impl AsRef<[u8]>, 92 - public_key: &dyn VerificationKey, 93 - ) -> Result<Self, Error> { 118 + pub fn decode<T>(token: T, public_key: &dyn VerificationKey) -> Result<Self, Error> 119 + where 120 + T: AsRef<[u8]>, 121 + { 94 122 decode(token, public_key) 95 123 } 96 124 ··· 101 129 /// 102 130 /// Returns an error if the token is in the wrong format, cannot be decoded, 103 131 /// or if deserialization fails. 104 - pub fn decode_unverified(token: impl AsRef<[u8]>) -> Result<Self, Error> { 132 + pub fn decode_unverified<T>(token: T) -> Result<Self, Error> 133 + where 134 + T: AsRef<[u8]>, 135 + { 105 136 decode_unverified(token) 106 137 } 107 138 } ··· 136 161 } 137 162 138 163 fn parse<T: DeserializeOwned + Serialize>(encoded_bytes: &[u8]) -> Result<T, Error> { 139 - let bytes = Encoding.decode(encoded_bytes)?; 164 + let bytes = ENCODING.decode(encoded_bytes)?; 140 165 141 166 // Verify the deserialized input matches the decoded bytes when re-serialized. 167 + // 168 + // This requires the "preserve_order" feature for serde_json. 142 169 let value: serde_json::Value = serde_json::from_slice(&bytes)?; 143 170 let serialized = serde_json::to_vec(&value)?; 144 171 if bytes != serialized { ··· 156 179 /// 157 180 /// Returns an error if the token is in the wrong format, cannot be decoded, or 158 181 /// if deserialization fails. 159 - pub fn decode_unverified<C: DeserializeOwned + Serialize>( 182 + fn decode_unverified<H: DeserializeOwned + Serialize, C: DeserializeOwned + Serialize>( 160 183 token: impl AsRef<[u8]>, 161 - ) -> Result<Token<C>, Error> { 184 + ) -> Result<Token<H, C>, Error> { 162 185 let (header, claims, _) = split_token(token.as_ref())?; 163 186 164 187 let header = parse(header)?; ··· 174 197 /// 175 198 /// Returns an error if the token cannot be deserialized or if the signature 176 199 /// verification fails. 177 - pub fn decode<C: DeserializeOwned + Serialize>( 178 - token: impl AsRef<[u8]>, 179 - verification_key: &dyn VerificationKey, 180 - ) -> Result<Token<C>, Error> { 200 + fn decode<T, H, C>(token: T, verification_key: &dyn VerificationKey) -> Result<Token<H, C>, Error> 201 + where 202 + T: AsRef<[u8]>, 203 + H: TokenHeader, 204 + C: TokenClaims, 205 + { 181 206 let token = token.as_ref(); 182 207 let (header, claims, signature) = split_token(token)?; 183 208 184 209 // The message to verify is the base64 encoded header and claims section. 185 210 let message = &token[..=header.len() + claims.len()]; 186 - let header: Header = parse(header)?; 211 + let header: H = parse(header)?; 187 212 188 213 if verification_key 189 214 .algorithm() 190 - .is_some_and(|&alg| alg != header.alg) 215 + .is_some_and(|algorithm| algorithm != header.algorithm()) 191 216 { 192 - return Err(Error::UnsupportedAlgorithm(header.alg)); 217 + return Err(Error::UnsupportedAlgorithm(*header.algorithm())); 193 218 } 194 219 195 220 // Decode the signature bytes and verify. 196 - let signature = Encoding.decode(signature)?; 221 + let signature = ENCODING.decode(signature)?; 197 222 verification_key.verify_sig(message, &signature)?; 198 223 199 224 let claims = parse(claims)?; ··· 224 245 claims: C, 225 246 key_pair: &EcdsaKeyPair, 226 247 ) -> Result<String, EncodeError> { 227 - use data_encoding::BASE64URL_NOPAD as Encoding; 228 - 229 248 let mut token = String::new(); 230 249 token.push_str( 231 - &Encoding.encode( 250 + &ENCODING.encode( 232 251 serde_json::to_string(&header) 233 252 .expect("JWT header should be serializable as JSON") 234 253 .as_bytes(), 235 254 ), 236 255 ); 237 256 token.push('.'); 238 - token.push_str(&Encoding.encode(serde_json::to_string(&claims)?.as_bytes())); 257 + token.push_str(&ENCODING.encode(serde_json::to_string(&claims)?.as_bytes())); 239 258 240 259 let signature = key_pair.sign(&SystemRandom::new(), token.as_bytes())?; 241 260 242 261 token.push('.'); 243 - token.push_str(&Encoding.encode(signature.as_ref())); 262 + token.push_str(&ENCODING.encode(signature.as_ref())); 244 263 Ok(token) 245 264 } 246 265 ··· 246 269 #[must_use] 247 270 pub fn generate_jti() -> String { 248 271 let jti: [u8; 16] = rand::random(); 249 - data_encoding::BASE64URL_NOPAD.encode(&jti) 272 + ENCODING.encode(&jti) 250 273 } 251 274 252 275 #[cfg(test)] ··· 256 279 257 280 use super::Algorithm; 258 281 use super::Error; 259 - use super::Token; 260 282 use super::Type; 261 283 262 284 #[test] ··· 272 296 assert_eq!(s, b"signature"); 273 297 } 274 298 275 - const TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE3NTg2NjE4ODUsImlzcyI6ImRpZDpwbGM6NjVnaGE0dDNhdnBmcHptdnBid292c3M3IiwiYXVkIjoiZGlkOndlYjpnb3JkaWFuLWRldjo1NTU1IiwiZXhwIjoxNzU4NjYxOTQ1LCJseG0iOiJzaC50YW5nbGVkLnJlcG8uY3JlYXRlIiwianRpIjoiY2Y0ZDE5YTIwNDE0YWMzMjk2NTI3NzBkYzIzYjUzNTYifQ.llMTh_dC72uV3A9STs8yTFAo8jO9XUJnK-m8eA4wZ0EZXeLpxQn3oviFH22eh9_SEKtj9y0YXCfWCafVJre8qg"; 299 + const TOKEN: &str = 300 + "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ. 301 + eyJpYXQiOjE3NTg2NjE4ODUsImlzcyI6ImRpZDpwbGM6NjVnaGE0dDNhdnBmcHptdnBid292c3M3IiwiYXVkIjoiZGlkOndlYjpnb3JkaWFuLWRldjo1NTU1IiwiZXhwIjoxNzU4NjYxOTQ1LCJseG0iOiJzaC50YW5nbGVkLnJlcG8uY3JlYXRlIiwianRpIjoiY2Y0ZDE5YTIwNDE0YWMzMjk2NTI3NzBkYzIzYjUzNTYifQ. 302 + llMTh_dC72uV3A9STs8yTFAo8jO9XUJnK-m8eA4wZ0EZXeLpxQn3oviFH22eh9_SEKtj9y0YXCfWCafVJre8qg" 303 + ; 276 304 277 305 #[test] 278 306 fn can_decode_token() { 307 + use crate::service_auth::Token; 308 + 279 309 let Token { header, claims } = Token::decode_unverified(TOKEN).unwrap(); 280 310 281 - assert_eq!(header.typ, Type::Jwt); 282 - assert_eq!(header.alg, Algorithm::ES256K); 311 + assert_eq!(header.typ, Type::JWT); 312 + assert_eq!(header.algorithm, Algorithm::ES256K); 283 313 assert_eq!( 284 - claims.aud.as_ref(), 314 + claims.audience, 285 315 Did::from_static("did:web:gordian-dev:5555") 286 316 ); 287 317 assert_eq!( 288 - claims.iss.as_ref(), 318 + claims.issuer, 289 319 Did::from_static("did:plc:65gha4t3avpfpzmvpbwovss7") 290 320 ); 291 321 } 292 322 293 323 #[test] 294 324 fn parse_rejects_junk_after_struct() { 325 + use crate::service_auth::Claims; 326 + 295 327 let claims = r#"{"iss":"did:plc:65gha4t3avpfpzmvpbwovss7","aud":"did:web:gordian.incus","iat":0,"exp":10,"jti":"totally_random_bytes"} "#; 296 - let encoded = super::Encoding.encode(claims.as_bytes()); 328 + let encoded = super::ENCODING.encode(claims.as_bytes()); 297 329 assert!(matches!( 298 - super::parse::<super::Claims>(encoded.as_bytes()), 330 + super::parse::<Claims>(encoded.as_bytes()), 299 331 Err(Error::SerializationMismatch) 300 332 )); 301 333 } 302 334 303 335 #[test] 304 336 fn can_verify_token() { 337 + use crate::service_auth::Token; 338 + 305 339 let vm = VerificationMethod::Multikey { 306 340 id: "".to_string(), 307 341 controller: "did:web:test".try_into().unwrap(), ··· 320 334 321 335 let Token { header, claims } = Token::decode(TOKEN, &vm).unwrap(); 322 336 323 - assert_eq!(header.typ, Type::Jwt); 324 - assert_eq!(header.alg, Algorithm::ES256K); 337 + assert_eq!(header.typ, Type::JWT); 338 + assert_eq!(header.algorithm, Algorithm::ES256K); 325 339 assert_eq!( 326 - claims.aud.as_ref(), 340 + claims.audience, 327 341 Did::from_static("did:web:gordian-dev:5555") 328 342 ); 329 343 }
+1
crates/gordian-auth/src/lib.rs
··· 5 5 pub mod jwt; 6 6 pub mod pkce; 7 7 pub mod resources; 8 + pub mod service_auth; 8 9 pub mod supported; 9 10 pub mod types; 10 11
+84
crates/gordian-auth/src/service_auth.rs
··· 1 + use gordian_types::DidBuf; 2 + use gordian_types::Nsid; 3 + use serde::Deserialize; 4 + use serde::Serialize; 5 + 6 + use crate::jwt; 7 + use crate::jwt::Algorithm; 8 + use crate::jwt::TokenClaims; 9 + use crate::jwt::Type; 10 + 11 + /// Service Authorization JWT header. 12 + #[must_use] 13 + #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] 14 + pub struct Header { 15 + pub typ: Type, 16 + 17 + #[serde(rename = "alg")] 18 + pub algorithm: Algorithm, 19 + } 20 + 21 + impl jwt::TokenHeader for Header { 22 + fn algorithm(&self) -> &Algorithm { 23 + &self.algorithm 24 + } 25 + } 26 + 27 + impl Header { 28 + /// Create a JWT header with the specified `algorithm`. 29 + /// 30 + /// # Example 31 + /// 32 + /// ```rust 33 + /// # use gordian_auth::service_auth::Header; 34 + /// use gordian_auth::jwt::Algorithm; 35 + /// use gordian_auth::jwt::Type; 36 + /// 37 + /// let header = Header::new(Algorithm::ES256K); 38 + /// assert_eq!(header.typ, Type::JWT); 39 + /// assert_eq!(header.algorithm, Algorithm::ES256K); 40 + /// ``` 41 + pub const fn new(algorithm: Algorithm) -> Self { 42 + Self { 43 + typ: Type::JWT, 44 + algorithm, 45 + } 46 + } 47 + } 48 + 49 + /// Service Authorization Claims. 50 + /// 51 + /// <https://atproto.com/specs/xrpc#inter-service-authentication-jwt> 52 + #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] 53 + pub struct Claims { 54 + /// Account DID associated with the service that the request is being 55 + /// sent to. 56 + #[serde(rename = "iss")] 57 + pub issuer: DidBuf, 58 + 59 + /// Service DID associated with the service that the request is being 60 + /// sent to. 61 + #[serde(rename = "aud")] 62 + pub audience: DidBuf, 63 + 64 + /// Token creation time as a UNIX timestamp. 65 + #[serde(rename = "iat")] 66 + pub issued_at: i64, 67 + 68 + /// Token expiration time as a UNIX timestamp. 69 + #[serde(rename = "exp")] 70 + pub expires_at: i64, 71 + 72 + /// Lexicon method NSID. 73 + // The specification claims this is optional. We require it. 74 + #[serde(rename = "lxm")] 75 + pub method: Box<Nsid>, 76 + 77 + /// An opaque string uniquely identifying the token. 78 + #[serde(rename = "jti")] 79 + pub identifier: Box<str>, 80 + } 81 + 82 + impl TokenClaims for Claims {} 83 + 84 + pub type Token = jwt::Token<Header, Claims>;
+13 -15
crates/gordian-cred/src/commands/git_credential.rs
··· 174 174 ); 175 175 176 176 // // We found a key, construct our JWT claims. 177 - let iss = account_did.clone(); 178 - let aud = format!("did:web:{knot}").parse::<DidBuf>().or_raise(err)?; 179 - let iat = OffsetDateTime::now_utc().unix_timestamp(); 180 - let exp = iat + 45; 177 + let issuer = account_did.clone(); 178 + let audience = format!("did:web:{knot}").parse::<DidBuf>().or_raise(err)?; 179 + let issued_at = OffsetDateTime::now_utc().unix_timestamp(); 180 + let expires_at = issued_at + 45; 181 181 let jti: [u8; 16] = rand::random(); 182 182 let jti: Box<str> = BASE32HEX_NOPAD.encode(&jti).to_ascii_lowercase().into(); 183 183 184 - let claims = gordian_auth::jwt::Claims { 185 - iss, 186 - aud, 187 - iat, 188 - exp, 189 - lxm: Some( 190 - "sh.tangled.repo.gitReceivePack" 191 - .try_into() 192 - .expect("nsid should be valid"), 193 - ), 194 - jti: jti.clone(), 184 + let claims = gordian_auth::service_auth::Claims { 185 + issuer, 186 + audience, 187 + issued_at, 188 + expires_at, 189 + method: "sh.tangled.repo.gitReceivePack" 190 + .try_into() 191 + .expect("sh.tangled.repo.gitReceivePack is a valid NSID"), 192 + identifier: jti.clone(), 195 193 }; 196 194 197 195 let header = match &agent_identity.key_data() {
+1 -4
crates/gordian-knot/src/extract/if_none_match.rs
··· 69 69 /// assert!(!EntityTag::weak("1").strong_eq(&EntityTag::strong("1"))); 70 70 /// assert!(EntityTag::strong("1").strong_eq(&EntityTag::strong("1"))); 71 71 /// ``` 72 - /// 73 72 pub fn strong_eq(&self, other: &Self) -> bool { 74 73 !self.weak && !other.weak && self.value == other.value 75 74 } ··· 84 85 /// assert!(EntityTag::weak("1").weak_eq(&EntityTag::strong("1"))); 85 86 /// assert!(EntityTag::strong("1").weak_eq(&EntityTag::strong("1"))); 86 87 /// ``` 87 - /// 88 88 pub fn weak_eq(&self, other: &Self) -> bool { 89 89 self.value == other.value 90 90 } ··· 270 272 use axum::response::IntoResponse; 271 273 use tower::ServiceExt as _; 272 274 275 + use super::IfNoneMatch; 273 276 use crate::extract::if_none_match::EntityTag; 274 277 use crate::extract::if_none_match::StrongEq; 275 278 use crate::extract::if_none_match::WeakEq as _; 276 - 277 - use super::IfNoneMatch; 278 279 279 280 async fn weak_0815(if_none_match: IfNoneMatch) -> impl IntoResponse { 280 281 if if_none_match.match_weak("0815") {
+4 -4
crates/gordian-knot/src/model.rs
··· 12 12 13 13 use axum::extract::FromRef; 14 14 use futures_util::future::BoxFuture; 15 - use gordian_auth::jwt; 15 + use gordian_auth::service_auth; 16 16 use gordian_identity::HttpClient; 17 17 use gordian_identity::Resolver; 18 18 use gordian_lexicon::sh_tangled::knot::Member; ··· 101 101 } 102 102 } 103 103 104 - impl AuthorizationClaimsStore<jwt::Claims> for Knot { 104 + impl AuthorizationClaimsStore<service_auth::Claims> for Knot { 105 105 fn get_unexpired_claims<'a: 'b, 'b>( 106 106 &'a self, 107 107 jti: &'b str, 108 108 now: i64, 109 - ) -> BoxFuture<'b, Result<Option<jwt::Claims>, AuthorizationClaimsStoreError>> { 109 + ) -> BoxFuture<'b, Result<Option<service_auth::Claims>, AuthorizationClaimsStoreError>> { 110 110 self.inner.get_unexpired_claims(jti, now) 111 111 } 112 112 113 113 fn store_claims( 114 114 &self, 115 - claims: jwt::Claims, 115 + claims: service_auth::Claims, 116 116 now: i64, 117 117 ) -> BoxFuture<'_, Result<(), AuthorizationClaimsStoreError>> { 118 118 self.inner.store_claims(claims, now)
+5 -5
crates/gordian-knot/src/model/knot_state.rs
··· 11 11 12 12 use futures_util::FutureExt; 13 13 use futures_util::future::BoxFuture; 14 - use gordian_auth::jwt; 14 + use gordian_auth::service_auth; 15 15 use gordian_identity::HttpClient; 16 16 use gordian_identity::Resolver; 17 17 use gordian_lexicon::com::atproto::repo::list_records::Record; ··· 480 480 } 481 481 } 482 482 483 - impl AuthorizationClaimsStore<jwt::Claims> for KnotState { 483 + impl AuthorizationClaimsStore<service_auth::Claims> for KnotState { 484 484 fn get_unexpired_claims<'a: 'b, 'b>( 485 485 &'a self, 486 486 jti: &'b str, 487 487 now: i64, 488 - ) -> BoxFuture<'b, Result<Option<jwt::Claims>, AuthorizationClaimsStoreError>> { 488 + ) -> BoxFuture<'b, Result<Option<service_auth::Claims>, AuthorizationClaimsStoreError>> { 489 489 async move { 490 490 let claims = self.database().get_claims(jti, now).await.ok().flatten(); 491 491 492 492 // If the claims have expired, remove them. 493 - if matches!(&claims, Some(claims) if claims.exp < now) { 493 + if matches!(&claims, Some(claims) if claims.expires_at < now) { 494 494 self.database() 495 495 .delete_claims(jti) 496 496 .await ··· 506 506 507 507 fn store_claims( 508 508 &self, 509 - claims: jwt::Claims, 509 + claims: service_auth::Claims, 510 510 now: i64, 511 511 ) -> BoxFuture<'_, Result<(), AuthorizationClaimsStoreError>> { 512 512 async move {
+7 -8
crates/gordian-knot/src/public/git/authorization.rs
··· 4 4 use axum::http::request::Parts; 5 5 use gordian_auth::IntoVerificationKey; 6 6 use gordian_auth::OpenSshKey; 7 - use gordian_auth::jwt::Claims; 8 - use gordian_auth::jwt::Token; 9 - use gordian_auth::jwt::decode; 7 + use gordian_auth::service_auth::Claims; 8 + use gordian_auth::service_auth::Token; 10 9 use gordian_identity::Resolver; 11 10 use gordian_types::Nsid; 12 11 use time::OffsetDateTime; ··· 67 68 // the verification methods into public keys. 68 69 69 70 let (resolved_did, doc) = resolver 70 - .resolve(unverified_claims.iss.as_str()) 71 + .resolve(unverified_claims.issuer.as_str()) 71 72 .await 72 73 .map_err(|error| Error::forbidden(&knot, error.to_string()))?; 73 74 74 - assert_eq!(unverified_claims.iss, resolved_did); 75 + assert_eq!(unverified_claims.issuer, resolved_did); 75 76 76 77 let verification_keys = doc 77 78 .verification_method ··· 81 82 // Try to decode and verify the JWT using any one of the verification keys 82 83 // we have for the DID. 83 84 for verification_key in verification_keys { 84 - if let Ok(token) = decode::<Claims>(credential, &verification_key) { 85 + if let Ok(token) = Token::decode(credential, &verification_key) { 85 86 // Store the JWT so it cannot be re-used within the claim period. 86 87 knot.store_claims(token.claims.clone(), now).await?; 87 88 return Ok(Self(token.claims)); ··· 92 93 // with claimed issuer. 93 94 let public_keys = knot 94 95 .database() 95 - .public_keys_for_did(&unverified_claims.iss) 96 + .public_keys_for_did(&unverified_claims.issuer) 96 97 .await 97 98 .unwrap_or_default() 98 99 .into_iter() ··· 101 102 // Try to decode and verify the JWT using any one of the public keys 102 103 // we have for the DID. 103 104 for verification_key in public_keys { 104 - if let Ok(token) = decode::<Claims>(credential, &verification_key) { 105 + if let Ok(token) = Token::decode(credential, &verification_key) { 105 106 // Store the JWT so it cannot be re-used within the claim period. 106 107 knot.store_claims(token.claims.clone(), now).await?; 107 108 return Ok(Self(token.claims));
+15 -9
crates/gordian-knot/src/public/git/receive_pack.rs
··· 38 38 let repository = TangledRepository::from_git_request(&mut parts, &knot).await?; 39 39 let GitAuthorization(auth) = GitAuthorization::from_request_parts(&mut parts, &knot).await?; 40 40 41 - if !knot.can_push(repository.repository_key(), &auth.iss).await { 42 - tracing::error!(did = %auth.iss, "push denied"); 41 + if !knot 42 + .can_push(repository.repository_key(), &auth.issuer) 43 + .await 44 + { 45 + tracing::error!(did = %auth.issuer, "push denied"); 43 46 return Err(super::Error::forbidden( 44 47 &knot, 45 48 format!( 46 49 "'{}' does not have permission to push to this repository", 47 - auth.iss 50 + auth.issuer 48 51 ), 49 52 ))?; 50 53 } ··· 62 59 command 63 60 .option_env("GIT_PROTOCOL", git_protocol) 64 61 .option_env("X_REQUEST_ID", request_id) 65 - .env(private::ENV_USER_DID, auth.iss.as_str()) 62 + .env(private::ENV_USER_DID, auth.issuer.as_str()) 66 63 .args([ 67 64 "-c", 68 65 &nonce_seed, ··· 97 94 let repository = TangledRepository::from_git_request(&mut parts, &knot).await?; 98 95 let GitAuthorization(auth) = GitAuthorization::from_request_parts(&mut parts, &knot).await?; 99 96 100 - if !knot.can_push(repository.repository_key(), &auth.iss).await { 101 - tracing::error!(did = %auth.iss, "push denied"); 97 + if !knot 98 + .can_push(repository.repository_key(), &auth.issuer) 99 + .await 100 + { 101 + tracing::error!(did = %auth.issuer, "push denied"); 102 102 return Err(super::Error::forbidden( 103 103 &knot, 104 104 format!( 105 105 "'{}' does not have permission to push to this repository", 106 - auth.iss 106 + auth.issuer 107 107 ), 108 108 ))?; 109 109 } ··· 115 109 let allowed_signers_path = std::env::current_dir() 116 110 .unwrap() 117 111 .join("allowed_signers") 118 - .join(auth.iss.as_str()); 112 + .join(auth.issuer.as_str()); 119 113 120 114 let mut allowed_signers_option = OsString::with_capacity( 121 115 "gpg.ssh.allowedSignersFile=".len() + allowed_signers_path.as_os_str().len(), ··· 127 121 command 128 122 .option_env("GIT_PROTOCOL", git_protocol) 129 123 .option_env("X_REQUEST_ID", request_id) 130 - .env(private::ENV_USER_DID, auth.iss.as_str()) 124 + .env(private::ENV_USER_DID, auth.issuer.as_str()) 131 125 .args(["-c", &nonce_seed, "-c"]) 132 126 .arg(&allowed_signers_option) 133 127 .args(["receive-pack", "--stateless-rpc"])
+4 -4
crates/gordian-knot/src/public/xrpc/sh_tangled/repo/impl_create.rs
··· 40 40 let policy = RepositoryCreatePolicy; 41 41 let can_create = policy 42 42 .evaluate_access( 43 - &claims.iss.as_ref(), 43 + &claims.issuer.as_ref(), 44 44 &Action::RepositoryCreate, 45 45 &knot, 46 46 &knot, ··· 50 50 if !matches!(can_create, Granted) { 51 51 return Err(errors::Forbidden(format!( 52 52 "'{}' does not have permission to create repositories on this knot", 53 - claims.iss 53 + claims.issuer 54 54 )))?; 55 55 } 56 56 ··· 58 58 let response = atrepo::fetch_record_bytes( 59 59 knot.resolver(), 60 60 knot.http(), 61 - &claims.iss, 61 + &claims.issuer, 62 62 "sh.tangled.repo", 63 63 &params.rkey, 64 64 ) 65 65 .await 66 - .inspect_err(|error| tracing::error!(?error, did = %claims.iss, rkey = %params.rkey, "unable to fetch record")) 66 + .inspect_err(|error| tracing::error!(?error, did = %claims.issuer, rkey = %params.rkey, "unable to fetch record")) 67 67 .map_err(errors::RepoError)?; 68 68 69 69 let record = serde_json::from_slice::<Record>(&response)
+2 -2
crates/gordian-knot/src/public/xrpc/sh_tangled/repo/impl_delete.rs
··· 38 38 let repository = RepositoryRef::new(&params.did, &params.rkey); 39 39 let can_delete = policy 40 40 .evaluate_access( 41 - &claims.iss.as_ref(), 41 + &claims.issuer.as_ref(), 42 42 &Action::RepositoryDelete, 43 43 &repository, 44 44 &knot, ··· 48 48 if !matches!(can_delete, Granted) { 49 49 return Err(errors::Forbidden(format!( 50 50 "'{}' does not have permission to delete repository '{}/{}'", 51 - claims.iss, params.did, params.rkey 51 + claims.issuer, params.did, params.rkey 52 52 )))?; 53 53 } 54 54
+2 -2
crates/gordian-knot/src/public/xrpc/sh_tangled/repo/impl_set_default_branch.rs
··· 53 53 let policy = RepositoryEditPolicy; 54 54 let can_create = policy 55 55 .evaluate_access( 56 - &claims.iss.as_ref(), 56 + &claims.issuer.as_ref(), 57 57 &Action::RepositoryEdit, 58 58 &RepositoryRef::from(&repo_key), 59 59 &knot, ··· 63 63 if !matches!(can_create, Granted) { 64 64 return Err(errors::Forbidden(format!( 65 65 "'{}' does not have permission to modify repositories on this knot", 66 - claims.iss 66 + claims.issuer 67 67 )))?; 68 68 } 69 69
+15 -17
crates/gordian-knot/src/services/authorization.rs
··· 7 7 use axum::http::request::Parts; 8 8 use futures_util::future::BoxFuture; 9 9 use gordian_auth::IntoVerificationKey as _; 10 - use gordian_auth::jwt::Claims; 11 - use gordian_auth::jwt::Token; 12 - use gordian_auth::jwt::decode; 10 + use gordian_auth::service_auth::Claims; 11 + use gordian_auth::service_auth::Token; 13 12 use gordian_identity::Resolver; 14 13 use gordian_types::Nsid; 15 14 use time::OffsetDateTime; ··· 55 56 const LEXICON_METHOD: &'static Nsid; 56 57 57 58 fn verify_iat(now: i64, claims: &Claims) -> Result<i64, VerificationError> { 58 - match claims.iat { 59 + match claims.issued_at { 59 60 iat if iat <= now => Ok(iat), 60 61 _ => Err(VerificationError::UseBeforeIssue), 61 62 } 62 63 } 63 64 64 65 fn verify_exp(now: i64, claims: &Claims) -> Result<i64, VerificationError> { 65 - match claims.exp { 66 + match claims.expires_at { 66 67 exp if exp > now => Ok(exp), 67 68 _ => Err(VerificationError::UseAfterExpiry), 68 69 } ··· 70 71 71 72 /// Verify [`Claims::lxm`] matches the required value. 72 73 fn verify_lexicon_method(claims: &Claims) -> Result<&'static str, VerificationError> { 73 - match claims.lxm.as_deref() { 74 - Some(lxm) if lxm == Self::LEXICON_METHOD => Ok(Self::LEXICON_METHOD), 75 - _ => Err(VerificationError::LexiconMethod), 74 + if claims.method != Self::LEXICON_METHOD { 75 + return Err(VerificationError::LexiconMethod); 76 76 } 77 + 78 + Ok(Self::LEXICON_METHOD) 77 79 } 78 80 79 81 fn verify_audience<'a>( 80 82 audience: &gordian_types::Did, 81 83 claims: &'a Claims, 82 84 ) -> Result<&'a gordian_types::Did, VerificationError> { 83 - match claims.aud == audience { 84 - true => Ok(&claims.aud), 85 + match claims.audience == audience { 86 + true => Ok(&claims.audience), 85 87 false => Err(VerificationError::WrongAudience), 86 88 } 87 89 } ··· 93 93 claims: &Claims, 94 94 ) -> impl Future<Output = Result<(), VerificationError>> + Send { 95 95 async move { 96 - match store.get_unexpired_claims(&claims.jti, now).await? { 97 - Some(stored_claims) if stored_claims.exp < now => Ok(()), 96 + match store.get_unexpired_claims(&claims.identifier, now).await? { 97 + Some(stored_claims) if stored_claims.expires_at < now => Ok(()), 98 98 None => Ok(()), 99 99 _ => Err(VerificationError::Reused), 100 100 } ··· 169 169 // the verification methods into public keys. 170 170 171 171 let (resolved_did, doc) = resolver 172 - .resolve(unverified_claims.iss.as_str()) 172 + .resolve(unverified_claims.issuer.as_str()) 173 173 .await 174 174 .map_err(errors::Forbidden)?; 175 175 176 - assert_eq!(unverified_claims.iss, resolved_did); 176 + assert_eq!(unverified_claims.issuer, resolved_did); 177 177 178 178 // @QUESTION Should we check all verification keys, or just the first? 179 179 let verification_keys = doc ··· 186 186 // Try to decode and verify the JWT using any one of the public keys 187 187 // we have for the DID. 188 188 for verification_key in verification_keys { 189 - if let Ok(token) = decode::<Claims>(credential, &verification_key) { 190 - let claims = token.claims; 191 - 189 + if let Ok(Token { header: _, claims }) = Token::decode(credential, &verification_key) { 192 190 // Store the JWT so it cannot be re-used. 193 191 knot.store_claims(claims.clone(), now) 194 192 .await
+9 -5
crates/gordian-knot/src/services/database.rs
··· 3 3 4 4 use futures_util::StreamExt; 5 5 use futures_util::stream::BoxStream; 6 - use gordian_auth::jwt; 6 + use gordian_auth::service_auth; 7 7 use gordian_jetstream::Value; 8 8 use gordian_lexicon::sh_tangled::PublicKey; 9 9 use gordian_lexicon::sh_tangled::knot::Member; ··· 426 426 .boxed() 427 427 } 428 428 429 - pub async fn store_claims(&self, claims: jwt::Claims, now: i64) -> Result<(), DataStoreError> { 429 + pub async fn store_claims( 430 + &self, 431 + claims: service_auth::Claims, 432 + now: i64, 433 + ) -> Result<(), DataStoreError> { 430 434 let mut transaction = self.db.begin().await?; 431 435 432 436 // First delete any expired claims. ··· 441 437 .execute(&mut *transaction) 442 438 .await?; 443 439 444 - let id = &claims.jti; 440 + let id = &claims.identifier; 445 441 let claims = serde_json::to_value(&claims)?; 446 442 sqlx::query!("INSERT INTO claim (id, claims) VALUES (?, ?)", id, claims) 447 443 .execute(&mut *transaction) ··· 455 451 &self, 456 452 id: &str, 457 453 now: i64, 458 - ) -> Result<Option<jwt::Claims>, DataStoreError> { 454 + ) -> Result<Option<service_auth::Claims>, DataStoreError> { 459 455 let claims = sqlx::query!( 460 456 r#"SELECT claims as "claims: Value" FROM claim WHERE id = ? AND json_extract(claims, '$.exp') >= ?"#, 461 457 id, now 462 458 ) 463 459 .fetch_optional(&self.db) 464 460 .await? 465 - .map(|record| serde_json::from_value::<jwt::Claims>(record.claims)) 461 + .map(|record| serde_json::from_value::<service_auth::Claims>(record.claims)) 466 462 .transpose()?; 467 463 468 464 Ok(claims)
+34 -28
crates/gordian-knot/src/tests.rs
··· 1 1 use axum::body::Body; 2 2 use axum::http::Request; 3 3 use axum::http::StatusCode; 4 - use gordian_auth::jwt::Claims; 4 + use gordian_auth::service_auth::Claims; 5 5 use gordian_lexicon::sh_tangled; 6 6 use gordian_types::Did; 7 7 use gordian_types::Tid; ··· 108 108 use axum::http::Response; 109 109 use axum::http::header; 110 110 use gordian_pds::Pds; 111 + use gordian_types::Nsid; 111 112 112 113 use super::super::public; 113 114 use super::*; ··· 116 115 use crate::nsid::SH_TANGLED_REPO_DELETE; 117 116 use crate::types::repository_spec::RepositoryPath; 118 117 119 - fn make_claims<F>(iss: &Did, aud: &Did, modify_claims: F) -> Claims 118 + fn make_claims<F>(iss: &Did, aud: &Did, lxm: &Nsid, modify_claims: F) -> Claims 120 119 where 121 120 F: FnOnce(&mut Claims), 122 121 { ··· 126 125 .to_lowercase(); 127 126 128 127 let mut claims = Claims { 129 - iss: iss.into(), 130 - aud: aud.into(), 131 - iat: OffsetDateTime::now_utc().unix_timestamp(), 132 - exp: OffsetDateTime::now_utc().unix_timestamp() + 10, 133 - lxm: None, 134 - jti: jti.into(), 128 + issuer: iss.into(), 129 + audience: aud.into(), 130 + issued_at: OffsetDateTime::now_utc().unix_timestamp(), 131 + expires_at: OffsetDateTime::now_utc().unix_timestamp() + 10, 132 + method: lxm.into_boxed(), 133 + identifier: jti.into(), 135 134 }; 136 135 137 136 modify_claims(&mut claims); 138 137 claims 139 138 } 140 139 141 - async fn service_auth_with<F>(pds: &Pds, iss: &Did, aud: &Did, modify_claims: F) -> HeaderValue 140 + async fn service_auth_with<F>( 141 + pds: &Pds, 142 + iss: &Did, 143 + aud: &Did, 144 + lxm: &Nsid, 145 + modify_claims: F, 146 + ) -> HeaderValue 142 147 where 143 148 F: FnOnce(&mut Claims), 144 149 { 145 - let claims = make_claims(iss, aud, modify_claims); 150 + let claims = make_claims(iss, aud, lxm, modify_claims); 146 151 let authorization = pds.service_auth(&claims).await; 147 152 HeaderValue::from_str(&authorization).unwrap() 148 153 } ··· 198 191 source: None, 199 192 }; 200 193 201 - let auth = service_auth_with(&pds, &did, &knot.instance, |claims| { 202 - claims.lxm = Some(SH_TANGLED_REPO_CREATE.into_boxed()); 203 - modify_claims(claims); 204 - }) 194 + let auth = service_auth_with( 195 + &pds, 196 + &did, 197 + &knot.instance, 198 + SH_TANGLED_REPO_CREATE, 199 + |claims| { 200 + modify_claims(claims); 201 + }, 202 + ) 205 203 .await; 206 204 207 205 let response = public::router() ··· 405 393 assert_eq!( 406 394 create_repo_with(&knot, pds, did, &rkey, "test-repo", None, |claims| { 407 395 // 408 - claims.iat = OffsetDateTime::now_utc().unix_timestamp() + 60; 396 + claims.issued_at = OffsetDateTime::now_utc().unix_timestamp() + 60; 409 397 }) 410 398 .await 411 399 .status(), ··· 435 423 assert_eq!( 436 424 create_repo_with(&knot, pds, did, &rkey, "test-repo", None, |claims| { 437 425 // 438 - claims.exp = OffsetDateTime::now_utc().unix_timestamp() - 1; 426 + claims.expires_at = OffsetDateTime::now_utc().unix_timestamp() - 1; 439 427 }) 440 428 .await 441 429 .status(), ··· 500 488 assert!(repo_exists_in_db(&knot, &did, &rkey).await); 501 489 502 490 // Or with the wrong lxm. 503 - let auth = service_auth_with(&pds, &did, &knot.instance(), |claims| { 504 - claims.lxm = Some(SH_TANGLED_REPO_CREATE.into_boxed()); 505 - }) 506 - .await; 491 + let auth = 492 + service_auth_with(&pds, &did, &knot.instance(), SH_TANGLED_REPO_CREATE, |_| {}).await; 507 493 508 494 assert_eq!( 509 495 public::router() ··· 527 517 528 518 // Valid auth, empty request body. 529 519 // Or with the wrong auth. 530 - let auth = service_auth_with(&pds, &did, &knot.instance(), |claims| { 531 - claims.lxm = Some(SH_TANGLED_REPO_DELETE.into_boxed()); 532 - }) 533 - .await; 520 + let auth = 521 + service_auth_with(&pds, &did, &knot.instance(), SH_TANGLED_REPO_DELETE, |_| {}).await; 534 522 assert_eq!( 535 523 public::router() 536 524 .with_state(knot.clone()) ··· 552 544 assert!(repo_exists_in_db(&knot, &did, &rkey).await); 553 545 554 546 // Or with the wrong auth. 555 - let auth = service_auth_with(&pds, &did, &knot.instance(), |claims| { 556 - claims.lxm = Some("sh.tangled.repo.delete".try_into().unwrap()); 557 - }) 558 - .await; 547 + let auth = 548 + service_auth_with(&pds, &did, &knot.instance(), SH_TANGLED_REPO_DELETE, |_| {}).await; 559 549 560 550 assert_eq!( 561 551 public::router()
+4 -8
crates/gordian-pds/src/state.rs
··· 9 9 use aws_lc_rs::signature::KeyPair as _; 10 10 use futures_util::FutureExt as _; 11 11 use gordian_auth::jwt; 12 + use gordian_auth::service_auth; 12 13 use gordian_identity::DidDocument; 13 14 use gordian_types::DidBuf; 14 15 use gordian_types::Tid; ··· 123 122 } 124 123 125 124 // Create an inter-service auth header for an account in the fake PDS. 126 - pub async fn service_auth(&self, claims: &jwt::Claims) -> String { 125 + pub async fn service_auth(&self, claims: &service_auth::Claims) -> String { 127 126 use sqlx::Row as _; 128 127 129 - let header = jwt::Header { 130 - typ: jwt::Type::Jwt, 131 - alg: jwt::Algorithm::ES256K, 132 - ..Default::default() 133 - }; 134 - 128 + let header = service_auth::Header::new(jwt::Algorithm::ES256K); 135 129 let result = sqlx::query("SELECT key FROM identity WHERE did = ?") 136 - .bind(claims.iss.as_ref()) 130 + .bind(claims.issuer.as_ref()) 137 131 .fetch_one(self.db()) 138 132 .await 139 133 .unwrap();