this repo has no description
1use crate::config::AuthConfig; 2use crate::oauth::OAuthError; 3use base64::Engine; 4use base64::engine::general_purpose::URL_SAFE_NO_PAD; 5use chrono::Utc; 6use hmac::Mac; 7use sha2::{Digest, Sha256}; 8use subtle::ConstantTimeEq; 9 10const ACCESS_TOKEN_EXPIRY_SECONDS: i64 = 300; 11 12pub struct TokenClaims { 13 pub jti: String, 14 pub exp: i64, 15 pub iat: i64, 16} 17 18pub fn verify_pkce(code_challenge: &str, code_verifier: &str) -> Result<(), OAuthError> { 19 let mut hasher = Sha256::new(); 20 hasher.update(code_verifier.as_bytes()); 21 let hash = hasher.finalize(); 22 let computed_challenge = URL_SAFE_NO_PAD.encode(hash); 23 if !bool::from( 24 computed_challenge 25 .as_bytes() 26 .ct_eq(code_challenge.as_bytes()), 27 ) { 28 return Err(OAuthError::InvalidGrant( 29 "PKCE verification failed".to_string(), 30 )); 31 } 32 Ok(()) 33} 34 35pub fn create_access_token( 36 token_id: &str, 37 sub: &str, 38 dpop_jkt: Option<&str>, 39 scope: Option<&str>, 40) -> Result<String, OAuthError> { 41 use serde_json::json; 42 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 43 let issuer = format!("https://{}", pds_hostname); 44 let now = Utc::now().timestamp(); 45 let exp = now + ACCESS_TOKEN_EXPIRY_SECONDS; 46 let actual_scope = scope.unwrap_or("atproto"); 47 let mut payload = json!({ 48 "iss": issuer, 49 "sub": sub, 50 "aud": issuer, 51 "iat": now, 52 "exp": exp, 53 "jti": token_id, 54 "scope": actual_scope 55 }); 56 if let Some(jkt) = dpop_jkt { 57 payload["cnf"] = json!({ "jkt": jkt }); 58 } 59 let header = json!({ 60 "alg": "HS256", 61 "typ": "at+jwt" 62 }); 63 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 64 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 65 let signing_input = format!("{}.{}", header_b64, payload_b64); 66 let config = AuthConfig::get(); 67 type HmacSha256 = hmac::Hmac<Sha256>; 68 let mut mac = HmacSha256::new_from_slice(config.jwt_secret().as_bytes()) 69 .map_err(|_| OAuthError::ServerError("HMAC key error".to_string()))?; 70 mac.update(signing_input.as_bytes()); 71 let signature = mac.finalize().into_bytes(); 72 let signature_b64 = URL_SAFE_NO_PAD.encode(signature); 73 Ok(format!("{}.{}", signing_input, signature_b64)) 74} 75 76pub fn extract_token_claims(token: &str) -> Result<TokenClaims, OAuthError> { 77 let parts: Vec<&str> = token.split('.').collect(); 78 if parts.len() != 3 { 79 return Err(OAuthError::InvalidToken("Invalid token format".to_string())); 80 } 81 let header_bytes = URL_SAFE_NO_PAD 82 .decode(parts[0]) 83 .map_err(|_| OAuthError::InvalidToken("Invalid token encoding".to_string()))?; 84 let header: serde_json::Value = serde_json::from_slice(&header_bytes) 85 .map_err(|_| OAuthError::InvalidToken("Invalid token header".to_string()))?; 86 if header.get("typ").and_then(|t| t.as_str()) != Some("at+jwt") { 87 return Err(OAuthError::InvalidToken( 88 "Not an OAuth access token".to_string(), 89 )); 90 } 91 if header.get("alg").and_then(|a| a.as_str()) != Some("HS256") { 92 return Err(OAuthError::InvalidToken( 93 "Unsupported algorithm".to_string(), 94 )); 95 } 96 let config = AuthConfig::get(); 97 let secret = config.jwt_secret(); 98 let signing_input = format!("{}.{}", parts[0], parts[1]); 99 let provided_sig = URL_SAFE_NO_PAD 100 .decode(parts[2]) 101 .map_err(|_| OAuthError::InvalidToken("Invalid signature encoding".to_string()))?; 102 type HmacSha256 = hmac::Hmac<Sha256>; 103 let mut mac = HmacSha256::new_from_slice(secret.as_bytes()) 104 .map_err(|_| OAuthError::ServerError("HMAC initialization failed".to_string()))?; 105 mac.update(signing_input.as_bytes()); 106 let expected_sig = mac.finalize().into_bytes(); 107 if !bool::from(expected_sig.ct_eq(&provided_sig)) { 108 return Err(OAuthError::InvalidToken( 109 "Invalid token signature".to_string(), 110 )); 111 } 112 let payload_bytes = URL_SAFE_NO_PAD 113 .decode(parts[1]) 114 .map_err(|_| OAuthError::InvalidToken("Invalid payload encoding".to_string()))?; 115 let payload: serde_json::Value = serde_json::from_slice(&payload_bytes) 116 .map_err(|_| OAuthError::InvalidToken("Invalid token payload".to_string()))?; 117 let jti = payload 118 .get("jti") 119 .and_then(|j| j.as_str()) 120 .ok_or_else(|| OAuthError::InvalidToken("Missing jti claim".to_string()))? 121 .to_string(); 122 let exp = payload 123 .get("exp") 124 .and_then(|e| e.as_i64()) 125 .ok_or_else(|| OAuthError::InvalidToken("Missing exp claim".to_string()))?; 126 let iat = payload 127 .get("iat") 128 .and_then(|i| i.as_i64()) 129 .ok_or_else(|| OAuthError::InvalidToken("Missing iat claim".to_string()))?; 130 Ok(TokenClaims { jti, exp, iat }) 131}