use std::fmt;
use crate::crypto::zeroize::Zeroizing;
use crate::encoding::url_encode_component;
use crate::util::log::debug;
use super::config::OAuthConfig;
#[doc(alias = "token_exchange")]
pub struct TokenExchangeRequest {
body: Zeroizing<String>,
content_type: &'static str,
}
impl TokenExchangeRequest {
#[must_use]
pub fn new(config: &OAuthConfig, code: &str, pkce_verifier: &str) -> Self {
let mut body = String::with_capacity(512);
body.push_str("grant_type=authorization_code");
body.push_str("&code=");
body.push_str(&url_encode_component(code));
body.push_str("&redirect_uri=");
body.push_str(&url_encode_component(config.redirect_uri()));
body.push_str("&client_id=");
body.push_str(&url_encode_component(config.client_id()));
body.push_str("&client_secret=");
body.push_str(&url_encode_component(config.client_secret()));
body.push_str("&code_verifier=");
body.push_str(&url_encode_component(pkce_verifier));
debug!(
endpoint = %config.token_endpoint(),
"oauth: token exchange request built"
);
Self {
body: Zeroizing::new(body),
content_type: "application/x-www-form-urlencoded",
}
}
#[must_use]
#[inline]
pub fn body(&self) -> &str {
&self.body
}
#[must_use]
#[inline]
pub fn content_type(&self) -> &str {
self.content_type
}
}
impl fmt::Debug for TokenExchangeRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TokenExchangeRequest")
.field("body", &"[REDACTED]")
.field("content_type", &self.content_type)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oauth::config::OAuthConfig;
fn test_config() -> OAuthConfig {
OAuthConfig::builder("my-client-id", "my-secret")
.authorization_endpoint("https://auth.example.com/authorize")
.token_endpoint("https://auth.example.com/token")
.redirect_uri("https://myapp.example.com/callback")
.scope("openid")
.build()
.unwrap()
}
#[test]
fn body_contains_grant_type() {
let config = test_config();
let req = TokenExchangeRequest::new(&config, "auth-code-123", "verifier-456");
assert!(
req.body().contains("grant_type=authorization_code"),
"body should contain grant_type: {}",
req.body(),
);
}
#[test]
fn body_contains_code() {
let config = test_config();
let req = TokenExchangeRequest::new(&config, "auth-code-123", "verifier-456");
assert!(
req.body().contains("code=auth-code-123"),
"body should contain code: {}",
req.body(),
);
}
#[test]
fn body_contains_redirect_uri() {
let config = test_config();
let req = TokenExchangeRequest::new(&config, "auth-code-123", "verifier-456");
assert!(
req.body().contains("redirect_uri="),
"body should contain redirect_uri: {}",
req.body(),
);
}
#[test]
fn body_contains_client_id() {
let config = test_config();
let req = TokenExchangeRequest::new(&config, "auth-code-123", "verifier-456");
assert!(
req.body().contains("client_id=my-client-id"),
"body should contain client_id: {}",
req.body(),
);
}
#[test]
fn body_contains_client_secret() {
let config = test_config();
let req = TokenExchangeRequest::new(&config, "auth-code-123", "verifier-456");
assert!(
req.body().contains("client_secret=my-secret"),
"body should contain client_secret: {}",
req.body(),
);
}
#[test]
fn body_contains_code_verifier() {
let config = test_config();
let req = TokenExchangeRequest::new(&config, "auth-code-123", "verifier-456");
assert!(
req.body().contains("code_verifier=verifier-456"),
"body should contain code_verifier: {}",
req.body(),
);
}
#[test]
fn content_type_is_form_urlencoded() {
let config = test_config();
let req = TokenExchangeRequest::new(&config, "code", "verifier");
assert_eq!(req.content_type(), "application/x-www-form-urlencoded",);
}
#[test]
fn code_with_special_chars_is_encoded() {
let config = test_config();
let req = TokenExchangeRequest::new(&config, "code=with&special", "verifier");
assert!(
req.body().contains("code=code%3Dwith%26special"),
"code with special characters should be URL-encoded: {}",
req.body(),
);
}
#[test]
fn verifier_with_special_chars_is_encoded() {
let config = test_config();
let req = TokenExchangeRequest::new(&config, "code", "verifier=special&chars");
assert!(
req.body()
.contains("code_verifier=verifier%3Dspecial%26chars"),
"verifier with special characters should be URL-encoded: {}",
req.body(),
);
}
}