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