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