this repo has no description
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}