use crate::auth::{error::AuthError, state::OAuthToken};
use crate::client_assertion::{self, CLIENT_ASSERTION_TYPE_JWT_BEARER};
use jsonwebtoken::{Algorithm, EncodingKey};
enum ClientAuth {
Secret(String),
PrivateKeyJwt {
encoding_key: EncodingKey,
alg: Algorithm,
kid: Option<String>,
},
}
pub struct ClientCredentialsFlow {
client_id: String,
auth: ClientAuth,
token_url: String,
http_client: reqwest::Client,
}
impl ClientCredentialsFlow {
pub fn new(client_id: String, client_secret: String, token_url: String) -> Self {
Self {
client_id,
auth: ClientAuth::Secret(client_secret),
token_url,
http_client: reqwest::Client::new(),
}
}
pub fn new_private_key_jwt(
client_id: String,
signing_key: EncodingKey,
alg: Algorithm,
token_url: String,
) -> Self {
Self {
client_id,
auth: ClientAuth::PrivateKeyJwt {
encoding_key: signing_key,
alg,
kid: None,
},
token_url,
http_client: reqwest::Client::new(),
}
}
pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
if let ClientAuth::PrivateKeyJwt { kid: slot, .. } = &mut self.auth {
*slot = Some(kid.into());
}
self
}
#[tracing::instrument(skip(self, scopes), fields(client_id = %self.client_id))]
pub async fn get_token(&self, scopes: Option<&[&str]>) -> Result<OAuthToken, AuthError> {
let mut params: Vec<(&str, String)> = vec![
("grant_type", "client_credentials".to_string()),
("client_id", self.client_id.clone()),
];
match &self.auth {
ClientAuth::Secret(secret) => {
tracing::debug!("authenticating with client_secret_post");
params.push(("client_secret", secret.clone()));
}
ClientAuth::PrivateKeyJwt {
encoding_key,
alg,
kid,
} => {
tracing::debug!("authenticating with private_key_jwt; minting a fresh assertion");
let assertion = client_assertion::mint_client_assertion(
&self.client_id,
&self.token_url,
encoding_key,
*alg,
kid.as_deref(),
client_assertion::MAX_CLIENT_ASSERTION_LIFETIME_SECS,
)
.map_err(|e| {
tracing::error!(error = %e, "failed to mint private_key_jwt client assertion");
e
})?;
params.push((
"client_assertion_type",
CLIENT_ASSERTION_TYPE_JWT_BEARER.to_string(),
));
params.push(("client_assertion", assertion));
}
}
if let Some(s) = scopes {
params.push(("scope", s.join(" ")));
}
let response = self
.http_client
.post(&self.token_url)
.header("Accept", "application/json")
.form(¶ms)
.send()
.await
.map_err(|e| {
tracing::error!(error = %e, "network error requesting token");
AuthError::Network
})?;
if !response.status().is_success() {
let error_text = response.text().await.unwrap_or_default();
tracing::warn!(error = %error_text, "token request failed");
return Err(AuthError::Provider(format!(
"Token request failed: {error_text}"
)));
}
response.json::<OAuthToken>().await.map_err(|e| {
tracing::error!(error = %e, "failed to parse token response");
AuthError::Provider(format!("Failed to parse token response: {e}"))
})
}
}