use crate::common::{EmailAddress, GrantId, Provider};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct TokenExchangeRequest {
pub code: String,
pub grant_type: String,
pub client_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code_verifier: Option<String>,
pub redirect_uri: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TokenExchangeResponse {
pub access_token: String,
pub token_type: String,
pub grant_id: GrantId,
pub scope: String,
pub email: EmailAddress,
pub provider: Provider,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CodeChallengeMethod {
Plain,
S256,
}
#[derive(Debug, Clone)]
pub struct PkceCodePair {
pub verifier: String,
pub challenge: String,
pub method: CodeChallengeMethod,
}
impl PkceCodePair {
pub fn generate() -> Result<Self, PkceError> {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use sha2::{Digest, Sha256};
let mut verifier_bytes = [0u8; 32];
getrandom::getrandom(&mut verifier_bytes)
.map_err(|e| PkceError::RandomGeneration(e.to_string()))?;
let verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
let challenge_bytes = hasher.finalize();
let challenge = URL_SAFE_NO_PAD.encode(challenge_bytes);
Ok(Self {
verifier,
challenge,
method: CodeChallengeMethod::S256,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum PkceError {
#[error("Failed to generate random bytes: {0}")]
RandomGeneration(String),
}
#[derive(Debug, Clone, Serialize)]
pub struct ProviderDetectRequest {
pub email: EmailAddress,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ProviderDetectResponse {
pub provider: Provider,
pub detected: bool,
#[serde(rename = "type")]
pub provider_type: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pkce_generation() {
let pkce = PkceCodePair::generate().unwrap();
assert!(!pkce.verifier.is_empty());
assert!(!pkce.challenge.is_empty());
assert_eq!(pkce.method, CodeChallengeMethod::S256);
}
#[test]
fn test_pkce_verifier_length() {
let pkce = PkceCodePair::generate().unwrap();
assert_eq!(pkce.verifier.len(), 43);
}
#[test]
fn test_pkce_challenge_deterministic() {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use sha2::{Digest, Sha256};
let pkce = PkceCodePair::generate().unwrap();
let mut hasher = Sha256::new();
hasher.update(pkce.verifier.as_bytes());
let expected_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
assert_eq!(pkce.challenge, expected_challenge);
}
#[test]
fn test_token_exchange_request_serialization() {
let request = TokenExchangeRequest {
code: "auth_code_123".to_string(),
grant_type: "authorization_code".to_string(),
client_id: "client_123".to_string(),
client_secret: Some("secret_123".to_string()),
code_verifier: None,
redirect_uri: "http://localhost:3000/callback".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("auth_code_123"));
assert!(json.contains("authorization_code"));
assert!(json.contains("client_123"));
}
#[test]
fn test_code_challenge_method_serialization() {
let s256 = CodeChallengeMethod::S256;
let json = serde_json::to_string(&s256).unwrap();
assert_eq!(json, "\"s256\"");
let plain = CodeChallengeMethod::Plain;
let json = serde_json::to_string(&plain).unwrap();
assert_eq!(json, "\"plain\"");
}
#[test]
fn test_provider_detect_request_serialization() {
let request = ProviderDetectRequest {
email: EmailAddress::new("user@gmail.com").unwrap(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("user@gmail.com"));
}
}