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