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