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