this repo has no description
at main 16 kB view raw
1use chrono::{DateTime, Utc}; 2use serde::{Deserialize, Serialize}; 3use serde_json::Value as JsonValue; 4 5#[derive(Debug, Clone, Serialize, Deserialize)] 6pub struct RequestId(pub String); 7 8#[derive(Debug, Clone, Serialize, Deserialize)] 9pub struct TokenId(pub String); 10 11#[derive(Debug, Clone, Serialize, Deserialize)] 12pub struct DeviceId(pub String); 13 14#[derive(Debug, Clone, Serialize, Deserialize)] 15pub struct SessionId(pub String); 16 17#[derive(Debug, Clone, Serialize, Deserialize)] 18pub struct Code(pub String); 19 20#[derive(Debug, Clone, Serialize, Deserialize)] 21pub struct RefreshToken(pub String); 22 23impl RequestId { 24 pub fn generate() -> Self { 25 Self(format!( 26 "urn:ietf:params:oauth:request_uri:{}", 27 uuid::Uuid::new_v4() 28 )) 29 } 30} 31 32impl TokenId { 33 pub fn generate() -> Self { 34 Self(uuid::Uuid::new_v4().to_string()) 35 } 36} 37 38impl DeviceId { 39 pub fn generate() -> Self { 40 Self(uuid::Uuid::new_v4().to_string()) 41 } 42} 43 44impl SessionId { 45 pub fn generate() -> Self { 46 Self(uuid::Uuid::new_v4().to_string()) 47 } 48} 49 50impl Code { 51 pub fn generate() -> Self { 52 use rand::Rng; 53 let bytes: [u8; 32] = rand::thread_rng().r#gen(); 54 Self(base64::Engine::encode( 55 &base64::engine::general_purpose::URL_SAFE_NO_PAD, 56 bytes, 57 )) 58 } 59} 60 61impl RefreshToken { 62 pub fn generate() -> Self { 63 use rand::Rng; 64 let bytes: [u8; 32] = rand::thread_rng().r#gen(); 65 Self(base64::Engine::encode( 66 &base64::engine::general_purpose::URL_SAFE_NO_PAD, 67 bytes, 68 )) 69 } 70} 71 72#[derive(Debug, Clone, Serialize, Deserialize)] 73#[serde(tag = "method")] 74pub enum ClientAuth { 75 #[serde(rename = "none")] 76 None, 77 #[serde(rename = "client_secret_basic")] 78 SecretBasic { client_secret: String }, 79 #[serde(rename = "client_secret_post")] 80 SecretPost { client_secret: String }, 81 #[serde(rename = "private_key_jwt")] 82 PrivateKeyJwt { client_assertion: String }, 83} 84 85#[derive(Debug, Clone, Serialize, Deserialize)] 86pub struct AuthorizationRequestParameters { 87 pub response_type: String, 88 pub client_id: String, 89 pub redirect_uri: String, 90 pub scope: Option<String>, 91 pub state: Option<String>, 92 pub code_challenge: String, 93 pub code_challenge_method: String, 94 pub response_mode: Option<String>, 95 pub login_hint: Option<String>, 96 pub dpop_jkt: Option<String>, 97 #[serde(flatten)] 98 pub extra: Option<JsonValue>, 99} 100 101#[derive(Debug, Clone)] 102pub struct RequestData { 103 pub client_id: String, 104 pub client_auth: Option<ClientAuth>, 105 pub parameters: AuthorizationRequestParameters, 106 pub expires_at: DateTime<Utc>, 107 pub did: Option<String>, 108 pub device_id: Option<String>, 109 pub code: Option<String>, 110 pub controller_did: Option<String>, 111} 112 113#[derive(Debug, Clone)] 114pub struct DeviceData { 115 pub session_id: String, 116 pub user_agent: Option<String>, 117 pub ip_address: String, 118 pub last_seen_at: DateTime<Utc>, 119} 120 121#[derive(Debug, Clone)] 122pub struct TokenData { 123 pub did: String, 124 pub token_id: String, 125 pub created_at: DateTime<Utc>, 126 pub updated_at: DateTime<Utc>, 127 pub expires_at: DateTime<Utc>, 128 pub client_id: String, 129 pub client_auth: ClientAuth, 130 pub device_id: Option<String>, 131 pub parameters: AuthorizationRequestParameters, 132 pub details: Option<JsonValue>, 133 pub code: Option<String>, 134 pub current_refresh_token: Option<String>, 135 pub scope: Option<String>, 136 pub controller_did: Option<String>, 137} 138 139#[derive(Debug, Clone, Serialize, Deserialize)] 140pub struct AuthorizedClientData { 141 pub scope: Option<String>, 142 pub remember: bool, 143} 144 145#[derive(Debug, Clone, Serialize, Deserialize)] 146pub struct OAuthClientMetadata { 147 pub client_id: String, 148 pub client_name: Option<String>, 149 pub client_uri: Option<String>, 150 pub logo_uri: Option<String>, 151 pub redirect_uris: Vec<String>, 152 pub grant_types: Option<Vec<String>>, 153 pub response_types: Option<Vec<String>>, 154 pub scope: Option<String>, 155 pub token_endpoint_auth_method: Option<String>, 156 pub dpop_bound_access_tokens: Option<bool>, 157 pub jwks: Option<JsonValue>, 158 pub jwks_uri: Option<String>, 159 pub application_type: Option<String>, 160} 161 162#[derive(Debug, Clone, Serialize, Deserialize)] 163pub struct ProtectedResourceMetadata { 164 pub resource: String, 165 pub authorization_servers: Vec<String>, 166 pub bearer_methods_supported: Vec<String>, 167 pub scopes_supported: Vec<String>, 168 pub resource_documentation: Option<String>, 169} 170 171#[derive(Debug, Clone, Serialize, Deserialize)] 172pub struct AuthorizationServerMetadata { 173 pub issuer: String, 174 pub authorization_endpoint: String, 175 pub token_endpoint: String, 176 pub jwks_uri: String, 177 pub registration_endpoint: Option<String>, 178 pub scopes_supported: Option<Vec<String>>, 179 pub response_types_supported: Vec<String>, 180 pub response_modes_supported: Option<Vec<String>>, 181 pub grant_types_supported: Option<Vec<String>>, 182 pub token_endpoint_auth_methods_supported: Option<Vec<String>>, 183 pub code_challenge_methods_supported: Option<Vec<String>>, 184 pub pushed_authorization_request_endpoint: Option<String>, 185 pub require_pushed_authorization_requests: Option<bool>, 186 pub dpop_signing_alg_values_supported: Option<Vec<String>>, 187 pub authorization_response_iss_parameter_supported: Option<bool>, 188} 189 190#[derive(Debug, Clone, Serialize, Deserialize)] 191pub struct ParResponse { 192 pub request_uri: String, 193 pub expires_in: u64, 194} 195 196#[derive(Debug, Clone, Serialize, Deserialize)] 197pub struct TokenResponse { 198 pub access_token: String, 199 pub token_type: String, 200 pub expires_in: u64, 201 #[serde(skip_serializing_if = "Option::is_none")] 202 pub refresh_token: Option<String>, 203 #[serde(skip_serializing_if = "Option::is_none")] 204 pub scope: Option<String>, 205 #[serde(skip_serializing_if = "Option::is_none")] 206 pub sub: Option<String>, 207} 208 209#[derive(Debug, Clone, Serialize, Deserialize)] 210pub struct TokenRequest { 211 pub grant_type: String, 212 pub code: Option<String>, 213 pub redirect_uri: Option<String>, 214 pub code_verifier: Option<String>, 215 pub refresh_token: Option<String>, 216 pub client_id: Option<String>, 217 pub client_secret: Option<String>, 218} 219 220#[derive(Debug, Clone, Serialize, Deserialize)] 221pub struct DPoPClaims { 222 pub jti: String, 223 pub htm: String, 224 pub htu: String, 225 pub iat: i64, 226 #[serde(skip_serializing_if = "Option::is_none")] 227 pub ath: Option<String>, 228 #[serde(skip_serializing_if = "Option::is_none")] 229 pub nonce: Option<String>, 230} 231 232#[derive(Debug, Clone, Serialize, Deserialize)] 233pub struct JwkPublicKey { 234 pub kty: String, 235 pub crv: Option<String>, 236 pub x: Option<String>, 237 pub y: Option<String>, 238 #[serde(rename = "use")] 239 pub key_use: Option<String>, 240 pub kid: Option<String>, 241 pub alg: Option<String>, 242} 243 244#[derive(Debug, Clone, Serialize, Deserialize)] 245pub struct Jwks { 246 pub keys: Vec<JwkPublicKey>, 247} 248 249#[derive(Debug, Clone, PartialEq, Eq)] 250pub enum AuthFlowState { 251 Pending, 252 Authenticated { 253 did: String, 254 device_id: Option<String>, 255 }, 256 Authorized { 257 did: String, 258 device_id: Option<String>, 259 code: String, 260 }, 261 Expired, 262} 263 264impl AuthFlowState { 265 pub fn from_request_data(data: &RequestData) -> Self { 266 if data.expires_at < chrono::Utc::now() { 267 return AuthFlowState::Expired; 268 } 269 match (&data.did, &data.code) { 270 (Some(did), Some(code)) => AuthFlowState::Authorized { 271 did: did.clone(), 272 device_id: data.device_id.clone(), 273 code: code.clone(), 274 }, 275 (Some(did), None) => AuthFlowState::Authenticated { 276 did: did.clone(), 277 device_id: data.device_id.clone(), 278 }, 279 (None, _) => AuthFlowState::Pending, 280 } 281 } 282 283 pub fn is_pending(&self) -> bool { 284 matches!(self, AuthFlowState::Pending) 285 } 286 287 pub fn is_authenticated(&self) -> bool { 288 matches!(self, AuthFlowState::Authenticated { .. }) 289 } 290 291 pub fn is_authorized(&self) -> bool { 292 matches!(self, AuthFlowState::Authorized { .. }) 293 } 294 295 pub fn is_expired(&self) -> bool { 296 matches!(self, AuthFlowState::Expired) 297 } 298 299 pub fn can_authenticate(&self) -> bool { 300 matches!(self, AuthFlowState::Pending) 301 } 302 303 pub fn can_authorize(&self) -> bool { 304 matches!(self, AuthFlowState::Authenticated { .. }) 305 } 306 307 pub fn can_exchange(&self) -> bool { 308 matches!(self, AuthFlowState::Authorized { .. }) 309 } 310 311 pub fn did(&self) -> Option<&str> { 312 match self { 313 AuthFlowState::Authenticated { did, .. } | AuthFlowState::Authorized { did, .. } => { 314 Some(did) 315 } 316 _ => None, 317 } 318 } 319 320 pub fn code(&self) -> Option<&str> { 321 match self { 322 AuthFlowState::Authorized { code, .. } => Some(code), 323 _ => None, 324 } 325 } 326} 327 328impl std::fmt::Display for AuthFlowState { 329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 330 match self { 331 AuthFlowState::Pending => write!(f, "pending"), 332 AuthFlowState::Authenticated { did, .. } => write!(f, "authenticated ({})", did), 333 AuthFlowState::Authorized { did, code, .. } => { 334 write!( 335 f, 336 "authorized ({}, code={}...)", 337 did, 338 &code[..8.min(code.len())] 339 ) 340 } 341 AuthFlowState::Expired => write!(f, "expired"), 342 } 343 } 344} 345 346#[derive(Debug, Clone, PartialEq, Eq)] 347pub enum RefreshTokenState { 348 Valid, 349 Used { 350 at: chrono::DateTime<chrono::Utc>, 351 }, 352 InGracePeriod { 353 rotated_at: chrono::DateTime<chrono::Utc>, 354 }, 355 Expired, 356 Revoked, 357} 358 359impl RefreshTokenState { 360 pub fn is_valid(&self) -> bool { 361 matches!(self, RefreshTokenState::Valid) 362 } 363 364 pub fn is_usable(&self) -> bool { 365 matches!( 366 self, 367 RefreshTokenState::Valid | RefreshTokenState::InGracePeriod { .. } 368 ) 369 } 370 371 pub fn is_used(&self) -> bool { 372 matches!(self, RefreshTokenState::Used { .. }) 373 } 374 375 pub fn is_in_grace_period(&self) -> bool { 376 matches!(self, RefreshTokenState::InGracePeriod { .. }) 377 } 378 379 pub fn is_expired(&self) -> bool { 380 matches!(self, RefreshTokenState::Expired) 381 } 382 383 pub fn is_revoked(&self) -> bool { 384 matches!(self, RefreshTokenState::Revoked) 385 } 386} 387 388impl std::fmt::Display for RefreshTokenState { 389 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 390 match self { 391 RefreshTokenState::Valid => write!(f, "valid"), 392 RefreshTokenState::Used { at } => write!(f, "used ({})", at), 393 RefreshTokenState::InGracePeriod { rotated_at } => { 394 write!(f, "grace period (rotated {})", rotated_at) 395 } 396 RefreshTokenState::Expired => write!(f, "expired"), 397 RefreshTokenState::Revoked => write!(f, "revoked"), 398 } 399 } 400} 401 402#[cfg(test)] 403mod tests { 404 use super::*; 405 use chrono::{Duration, Utc}; 406 407 fn make_request_data( 408 did: Option<String>, 409 code: Option<String>, 410 expires_in: Duration, 411 ) -> RequestData { 412 RequestData { 413 client_id: "test-client".into(), 414 client_auth: None, 415 parameters: AuthorizationRequestParameters { 416 response_type: "code".into(), 417 client_id: "test-client".into(), 418 redirect_uri: "https://example.com/callback".into(), 419 scope: Some("atproto".into()), 420 state: None, 421 code_challenge: "test".into(), 422 code_challenge_method: "S256".into(), 423 response_mode: None, 424 login_hint: None, 425 dpop_jkt: None, 426 extra: None, 427 }, 428 expires_at: Utc::now() + expires_in, 429 did, 430 device_id: None, 431 code, 432 controller_did: None, 433 } 434 } 435 436 #[test] 437 fn test_auth_flow_state_pending() { 438 let data = make_request_data(None, None, Duration::minutes(5)); 439 let state = AuthFlowState::from_request_data(&data); 440 assert!(state.is_pending()); 441 assert!(!state.is_authenticated()); 442 assert!(!state.is_authorized()); 443 assert!(!state.is_expired()); 444 assert!(state.can_authenticate()); 445 assert!(!state.can_authorize()); 446 assert!(!state.can_exchange()); 447 assert!(state.did().is_none()); 448 assert!(state.code().is_none()); 449 } 450 451 #[test] 452 fn test_auth_flow_state_authenticated() { 453 let data = make_request_data(Some("did:plc:test".into()), None, Duration::minutes(5)); 454 let state = AuthFlowState::from_request_data(&data); 455 assert!(!state.is_pending()); 456 assert!(state.is_authenticated()); 457 assert!(!state.is_authorized()); 458 assert!(!state.is_expired()); 459 assert!(!state.can_authenticate()); 460 assert!(state.can_authorize()); 461 assert!(!state.can_exchange()); 462 assert_eq!(state.did(), Some("did:plc:test")); 463 assert!(state.code().is_none()); 464 } 465 466 #[test] 467 fn test_auth_flow_state_authorized() { 468 let data = make_request_data( 469 Some("did:plc:test".into()), 470 Some("auth-code-123".into()), 471 Duration::minutes(5), 472 ); 473 let state = AuthFlowState::from_request_data(&data); 474 assert!(!state.is_pending()); 475 assert!(!state.is_authenticated()); 476 assert!(state.is_authorized()); 477 assert!(!state.is_expired()); 478 assert!(!state.can_authenticate()); 479 assert!(!state.can_authorize()); 480 assert!(state.can_exchange()); 481 assert_eq!(state.did(), Some("did:plc:test")); 482 assert_eq!(state.code(), Some("auth-code-123")); 483 } 484 485 #[test] 486 fn test_auth_flow_state_expired() { 487 let data = make_request_data( 488 Some("did:plc:test".into()), 489 Some("code".into()), 490 Duration::minutes(-1), 491 ); 492 let state = AuthFlowState::from_request_data(&data); 493 assert!(state.is_expired()); 494 assert!(!state.can_authenticate()); 495 assert!(!state.can_authorize()); 496 assert!(!state.can_exchange()); 497 } 498 499 #[test] 500 fn test_refresh_token_state_valid() { 501 let state = RefreshTokenState::Valid; 502 assert!(state.is_valid()); 503 assert!(state.is_usable()); 504 assert!(!state.is_used()); 505 assert!(!state.is_in_grace_period()); 506 assert!(!state.is_expired()); 507 assert!(!state.is_revoked()); 508 } 509 510 #[test] 511 fn test_refresh_token_state_grace_period() { 512 let state = RefreshTokenState::InGracePeriod { 513 rotated_at: Utc::now(), 514 }; 515 assert!(!state.is_valid()); 516 assert!(state.is_usable()); 517 assert!(!state.is_used()); 518 assert!(state.is_in_grace_period()); 519 } 520 521 #[test] 522 fn test_refresh_token_state_used() { 523 let state = RefreshTokenState::Used { at: Utc::now() }; 524 assert!(!state.is_valid()); 525 assert!(!state.is_usable()); 526 assert!(state.is_used()); 527 } 528 529 #[test] 530 fn test_refresh_token_state_expired() { 531 let state = RefreshTokenState::Expired; 532 assert!(!state.is_usable()); 533 assert!(state.is_expired()); 534 } 535 536 #[test] 537 fn test_refresh_token_state_revoked() { 538 let state = RefreshTokenState::Revoked; 539 assert!(!state.is_usable()); 540 assert!(state.is_revoked()); 541 } 542}