use std::fmt::{Debug, Formatter};
use reqwest::Client;
use secrecy::{ExposeSecret as _, SecretString};
use url::Url;
use crate::http::{Accepted as _, shared_client};
use crate::{
CredentialResult, OAuth2ClientAuthentication, OAuth2Grant, OAuth2TokenRequest,
OAuth2TokenResponse, Token,
};
#[derive(Clone)]
pub struct ClientCredentials {
client_id: String,
client_secret: SecretString,
}
impl ClientCredentials {
pub fn new(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
Self {
client_id: client_id.into(),
client_secret: SecretString::from(client_secret.into()),
}
}
fn client_id(&self) -> &str {
&self.client_id
}
fn client_secret(&self) -> &str {
self.client_secret.expose_secret()
}
}
impl Debug for ClientCredentials {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientCredentials")
.field("client_id", &self.client_id)
.finish_non_exhaustive()
}
}
pub struct ClientCredentialsProvider {
client: Client,
api_url: Url,
credentials: Option<ClientCredentials>,
}
impl ClientCredentialsProvider {
pub fn new(api_url: Url) -> Self {
ClientCredentialsProvider {
client: shared_client(),
api_url,
credentials: None,
}
}
pub fn with_credentials(api_url: Url, credentials: ClientCredentials) -> Self {
ClientCredentialsProvider {
client: shared_client(),
api_url,
credentials: Some(credentials),
}
}
}
impl Debug for ClientCredentialsProvider {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientCredentialsProvider").finish()
}
}
impl ClientCredentialsProvider {
pub async fn token(&self) -> CredentialResult<Option<Token>> {
let (client_id, client_secret) = match &self.credentials {
Some(credentials) => (
credentials.client_id().to_owned(),
credentials.client_secret().to_owned(),
),
None => {
let Ok(client_id) = std::env::var("SPIRAL_CLIENT_ID") else {
return Ok(None);
};
let Ok(client_secret) = std::env::var("SPIRAL_CLIENT_SECRET") else {
bail!("SPIRAL_CLIENT_SECRET environment variable expected but not set")
};
(client_id, client_secret)
}
};
let response = self
.client
.post(
self.api_url
.join("/oauth/token")
.map_err(|e| invalid!("invalid url {e}"))?,
)
.form(&OAuth2TokenRequest {
client: OAuth2ClientAuthentication {
client_id: Some(client_id),
client_secret: Some(client_secret),
..Default::default()
},
grant: OAuth2Grant::ClientCredentials {
resource: None,
authorization_details: None,
},
})
.send()
.await?
.accepted()
.await?
.json::<OAuth2TokenResponse>()
.await?;
Ok(Some(response.into_access_token()))
}
}
impl_token_source!(ClientCredentialsProvider);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_credentials_debug_output_redacts_the_secret() {
let credentials = ClientCredentials::new("client-id", "client-secret");
let debug = format!("{credentials:?}");
assert!(debug.contains("client-id"));
assert!(!debug.contains("client-secret"));
}
}