use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use async_trait::async_trait;
use http::{HeaderMap, HeaderName, HeaderValue};
use uuid::Uuid;
use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache};
use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt};
use graph_core::identity::ForceTokenRefresh;
use graph_error::{AuthExecutionError, AuthExecutionResult, AuthorizationFailure, IdentityResult};
use crate::identity::credentials::app_config::AppConfig;
#[cfg(feature = "openssl")]
use crate::identity::X509Certificate;
use crate::identity::{
tracing_targets::CREDENTIAL_EXECUTOR, Authority, AzureCloudInstance,
ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, Token,
TokenCredentialExecutor,
};
use crate::oauth_serializer::{AuthParameter, AuthSerializer};
pub(crate) static CLIENT_ASSERTION_TYPE: &str =
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
credential_builder!(
ClientCertificateCredentialBuilder,
ConfidentialClientApplication<ClientCertificateCredential>
);
#[derive(Clone)]
pub struct ClientCertificateCredential {
pub(crate) app_config: AppConfig,
pub(crate) client_assertion_type: String,
pub(crate) client_assertion: String,
token_cache: InMemoryCacheStore<Token>,
}
impl ClientCertificateCredential {
#[cfg(feature = "openssl")]
pub fn new<T: AsRef<str>>(
client_id: T,
x509: &X509Certificate,
) -> IdentityResult<ClientCertificateCredential> {
let mut builder = ClientCertificateCredentialBuilder::new(client_id.as_ref());
builder.with_certificate(x509)?;
Ok(builder.credential)
}
pub fn builder<T: AsRef<str>>(client_id: T) -> ClientCertificateCredentialBuilder {
ClientCertificateCredentialBuilder::new(client_id)
}
pub fn authorization_url_builder<T: AsRef<str>>(
client_id: T,
) -> ClientCredentialsAuthorizationUrlParameterBuilder {
ClientCredentialsAuthorizationUrlParameterBuilder::new(client_id)
}
fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> {
let response = self.execute()?;
if !response.status().is_success() {
return Err(AuthExecutionError::silent_token_auth(
response.into_http_response()?,
));
}
let new_token: Token = response.json()?;
self.token_cache.store(cache_id, new_token.clone());
Ok(new_token)
}
async fn execute_cached_token_refresh_async(
&mut self,
cache_id: String,
) -> AuthExecutionResult<Token> {
let response = self.execute_async().await?;
if !response.status().is_success() {
return Err(AuthExecutionError::silent_token_auth(
response.into_http_response_async().await?,
));
}
let new_token: Token = response.json().await?;
self.token_cache.store(cache_id, new_token.clone());
Ok(new_token)
}
}
impl Debug for ClientCertificateCredential {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientCertificateCredential")
.field("app_config", &self.app_config)
.finish()
}
}
#[async_trait]
impl TokenCache for ClientCertificateCredential {
type Token = Token;
#[tracing::instrument]
fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> {
let cache_id = self.app_config.cache_id.to_string();
if let Some(token) = self.token_cache.get(cache_id.as_str()) {
if token.is_expired_sub(time::Duration::minutes(5)) {
tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
self.execute_cached_token_refresh(cache_id)
} else {
tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache");
Ok(token)
}
} else {
tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None");
self.execute_cached_token_refresh(cache_id)
}
}
#[tracing::instrument]
async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> {
let cache_id = self.app_config.cache_id.to_string();
if let Some(token) = self.token_cache.get(cache_id.as_str()) {
if token.is_expired_sub(time::Duration::minutes(5)) {
tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token refresh");
self.execute_cached_token_refresh_async(cache_id).await
} else {
tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache");
Ok(token.clone())
}
} else {
tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request");
self.execute_cached_token_refresh_async(cache_id).await
}
}
fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) {
self.app_config.force_token_refresh = force_token_refresh;
}
}
#[async_trait]
impl TokenCredentialExecutor for ClientCertificateCredential {
fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> {
let mut serializer = AuthSerializer::new();
let client_id = self.app_config.client_id.to_string();
if client_id.is_empty() || self.app_config.client_id.is_nil() {
return AuthorizationFailure::result(AuthParameter::ClientId.alias());
}
if self.client_assertion.trim().is_empty() {
return AuthorizationFailure::result(AuthParameter::ClientAssertion.alias());
}
if self.client_assertion_type.trim().is_empty() {
self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned();
}
serializer
.client_id(client_id.as_str())
.client_assertion(self.client_assertion.as_str())
.client_assertion_type(self.client_assertion_type.as_str())
.grant_type("client_credentials")
.set_scope(self.app_config.scope.clone());
serializer.as_credential_map(
vec![AuthParameter::Scope],
vec![
AuthParameter::ClientId,
AuthParameter::GrantType,
AuthParameter::ClientAssertion,
AuthParameter::ClientAssertionType,
],
)
}
fn client_id(&self) -> &Uuid {
&self.app_config.client_id
}
fn authority(&self) -> Authority {
self.app_config.authority.clone()
}
fn azure_cloud_instance(&self) -> AzureCloudInstance {
self.app_config.azure_cloud_instance
}
fn app_config(&self) -> &AppConfig {
&self.app_config
}
}
#[derive(Clone)]
pub struct ClientCertificateCredentialBuilder {
credential: ClientCertificateCredential,
}
impl ClientCertificateCredentialBuilder {
fn new<T: AsRef<str>>(client_id: T) -> ClientCertificateCredentialBuilder {
ClientCertificateCredentialBuilder {
credential: ClientCertificateCredential {
app_config: AppConfig::builder(client_id.as_ref())
.scope(vec!["https://graph.microsoft.com/.default"])
.build(),
client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(),
client_assertion: Default::default(),
token_cache: Default::default(),
},
}
}
#[cfg(feature = "openssl")]
pub(crate) fn new_with_certificate(
x509: &X509Certificate,
mut app_config: AppConfig,
) -> IdentityResult<ClientCertificateCredentialBuilder> {
app_config
.scope
.insert("https://graph.microsoft.com/.default".into());
let mut credential_builder = ClientCertificateCredentialBuilder {
credential: ClientCertificateCredential {
app_config,
client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(),
client_assertion: Default::default(),
token_cache: Default::default(),
},
};
credential_builder.with_certificate(x509)?;
Ok(credential_builder)
}
#[cfg(feature = "openssl")]
pub fn with_certificate(&mut self, certificate: &X509Certificate) -> IdentityResult<&mut Self> {
if let Some(tenant_id) = self.credential.app_config.authority.tenant_id() {
self.with_client_assertion(certificate.sign_with_tenant(Some(tenant_id.clone()))?);
} else {
self.with_client_assertion(certificate.sign_with_tenant(None)?);
}
Ok(self)
}
#[allow(dead_code)]
fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self {
self.credential.client_assertion = client_assertion.as_ref().to_owned();
self
}
pub fn credential(self) -> ClientCertificateCredential {
self.credential
}
}
impl From<ClientCertificateCredential> for ClientCertificateCredentialBuilder {
fn from(credential: ClientCertificateCredential) -> Self {
ClientCertificateCredentialBuilder { credential }
}
}
impl From<ClientCertificateCredentialBuilder> for ClientCertificateCredential {
fn from(builder: ClientCertificateCredentialBuilder) -> Self {
builder.credential
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_uuid_fake() {
let client_id_uuid = Uuid::new_v4();
let builder = ClientCertificateCredentialBuilder::new(client_id_uuid.to_string());
assert_eq!(builder.credential.app_config.client_id, client_id_uuid);
}
#[test]
#[should_panic]
fn test_123_uuid() {
let builder = ClientCertificateCredentialBuilder::new("123");
assert_eq!(
builder.credential.app_config.client_id,
Uuid::try_parse("123").unwrap()
);
}
#[test]
fn credential_builder() {
let builder =
ClientCertificateCredentialBuilder::new("4ef900be-dfd9-4da6-b224-0011e46c54dd");
assert_eq!(
builder.credential.app_config.client_id.to_string(),
"4ef900be-dfd9-4da6-b224-0011e46c54dd"
);
}
}