feat(auth): Enhance Google OAuth integration with PKCE support and CSRF validation improvements

This commit is contained in:
MythEclipse
2025-08-12 16:11:15 +07:00
parent adf3ef8e28
commit a25e896fa2
6 changed files with 357 additions and 28 deletions
+83
View File
@@ -10,6 +10,13 @@ struct CsrfPayload {
pub random: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct OAuthCsrfPayload {
pub timestamp: u64,
pub random: String,
pub pkce_verifier: String,
}
/// Generate a signed CSRF token that can be validated without server-side storage
pub fn generate_csrf_token(secret: &str) -> Result<String, Error> {
let timestamp = SystemTime::now()
@@ -38,6 +45,35 @@ pub fn generate_csrf_token(secret: &str) -> Result<String, Error> {
Ok(format!("{}.{}", payload_b64, signature))
}
/// Generate a signed OAuth CSRF token with PKCE verifier
pub fn generate_oauth_csrf_token(secret: &str, pkce_verifier: &str) -> Result<String, Error> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| Error::Auth("Failed to get timestamp".to_string()))?
.as_secs();
let random = uuid::Uuid::new_v4().to_string();
let payload = OAuthCsrfPayload {
timestamp,
random,
pkce_verifier: pkce_verifier.to_string(),
};
let payload_json = serde_json::to_string(&payload)
.map_err(|_| Error::Auth("Failed to serialize OAuth CSRF payload".to_string()))?;
let payload_b64 = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
// Create signature
let mut hasher = Sha256::new();
hasher.update(payload_b64.as_bytes());
hasher.update(secret.as_bytes());
let signature = URL_SAFE_NO_PAD.encode(hasher.finalize());
Ok(format!("{}.{}", payload_b64, signature))
}
/// Validate a CSRF token
pub fn validate_csrf_token(token: &str, secret: &str, max_age_seconds: u64) -> Result<(), Error> {
let parts: Vec<&str> = token.split('.').collect();
@@ -85,6 +121,53 @@ pub fn validate_csrf_token(token: &str, secret: &str, max_age_seconds: u64) -> R
Ok(())
}
/// Validate OAuth CSRF token and extract PKCE verifier
pub fn validate_oauth_csrf_token(token: &str, secret: &str, max_age_seconds: u64) -> Result<String, Error> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 2 {
return Err(Error::Auth("Invalid OAuth CSRF token format".to_string()));
}
let payload_b64 = parts[0];
let provided_signature = parts[1];
// Verify signature
let mut hasher = Sha256::new();
hasher.update(payload_b64.as_bytes());
hasher.update(secret.as_bytes());
let expected_signature = URL_SAFE_NO_PAD.encode(hasher.finalize());
if provided_signature != expected_signature {
return Err(Error::Auth("Invalid OAuth CSRF token signature".to_string()));
}
// Decode and validate payload
let payload_json = URL_SAFE_NO_PAD.decode(payload_b64)
.map_err(|_| Error::Auth("Failed to decode OAuth CSRF token".to_string()))?;
let payload_str = String::from_utf8(payload_json)
.map_err(|_| Error::Auth("Invalid OAuth CSRF token encoding".to_string()))?;
let payload: OAuthCsrfPayload = serde_json::from_str(&payload_str)
.map_err(|_| Error::Auth("Failed to parse OAuth CSRF token".to_string()))?;
// Check timestamp
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| Error::Auth("Failed to get current timestamp".to_string()))?
.as_secs();
if now > payload.timestamp + max_age_seconds {
return Err(Error::Auth("OAuth CSRF token has expired".to_string()));
}
if payload.timestamp > now + 60 { // Allow 1 minute clock skew
return Err(Error::Auth("OAuth CSRF token timestamp is in the future".to_string()));
}
Ok(payload.pkce_verifier)
}
#[cfg(test)]
mod tests {
use super::*;