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