use std::{
collections::HashMap,
time::{SystemTime, UNIX_EPOCH},
};
use authority::Authority;
use base64::{engine::general_purpose, Engine};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{authority, Error};
const CLIENT_ID: &str = "client_id";
const SCOPES: &str = "scope";
const GRANT_TYPE: &str = "grant_type";
const CLIENT_CREDENTIALS_GRANT: &str = "client_credentials";
const CLIENT_SECRET: &str = "client_secret";
const ASSERTION: &str = "client_assertion";
const ASSERTION_TYPE: &str = "client_assertion_type";
const CLIENT_ASSERTION_GRANT_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
pub enum ClientCredential {
ClientSecret(String),
Certificate(Certificate),
}
impl ClientCredential {
pub fn from_secret(secret: String) -> Self {
ClientCredential::ClientSecret(secret)
}
pub fn from_certificate(private_key: Vec<u8>, thumbprint: String) -> Self {
ClientCredential::Certificate(Certificate {
private_key,
thumbprint,
})
}
}
pub struct Certificate {
private_key: Vec<u8>,
thumbprint: String,
}
pub struct ConfidentialClient {
client_id: String,
authority: Authority,
credential: ClientCredential,
http_client: Client,
}
impl ConfidentialClient {
pub async fn new(
client_id: &str,
authority: &str,
credential: ClientCredential,
) -> Result<ConfidentialClient, Error> {
ConfidentialClient::with_http_client(client_id, authority, credential, Client::new()).await
}
pub async fn with_http_client(
client_id: &str,
authority: &str,
credential: ClientCredential,
http_client: Client,
) -> Result<ConfidentialClient, Error> {
let authority = Authority::new(authority, &http_client).await?;
Ok(ConfidentialClient {
client_id: client_id.to_string(),
authority,
credential,
http_client,
})
}
pub async fn acquire_token_silent(&self, scopes: &[&str]) -> Result<TokenResponse, Error> {
let mut params = HashMap::new();
let assertion;
match &self.credential {
ClientCredential::ClientSecret(client_secret) => {
params.insert(CLIENT_SECRET, client_secret.as_str());
}
ClientCredential::Certificate(certificate) => {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let audience = &self.authority.token_endpoint;
let issuer = &self.client_id;
let uuid = Uuid::new_v4().to_string();
let claims = AssertionClaims {
aud: audience,
sub: issuer,
iss: issuer,
exp: 600 + now,
iat: now,
jti: uuid.as_str(),
};
let encoding_key = EncodingKey::from_rsa_pem(&certificate.private_key)?;
let mut header = Header::new(Algorithm::RS256);
let sha1_thumbprint =
general_purpose::STANDARD.encode(hex::decode(&certificate.thumbprint)?);
header.x5t = Some(sha1_thumbprint);
assertion = encode(&header, &claims, &encoding_key)?;
params.insert(ASSERTION, &assertion);
params.insert(ASSERTION_TYPE, CLIENT_ASSERTION_GRANT_TYPE);
}
}
params.insert(CLIENT_ID, &self.client_id);
let scope = scopes.join(" ");
params.insert(SCOPES, &scope);
params.insert(GRANT_TYPE, CLIENT_CREDENTIALS_GRANT);
let response = self
.http_client
.post(&self.authority.token_endpoint)
.form(¶ms)
.send()
.await?;
let response: TokenResponse = response.json().await?;
Ok(response)
}
}
#[derive(Deserialize, Debug)]
pub struct TokenResponse {
pub expires_in: Option<u64>,
pub ext_expires_in: Option<u64>,
pub access_token: Option<String>,
pub refresh_token: Option<String>,
pub id_token: Option<String>,
pub error: Option<String>,
pub error_description: Option<String>,
pub error_codes: Option<Vec<usize>>,
pub timestamp: Option<String>,
pub trace_id: Option<String>,
pub correlation_id: Option<String>,
}
#[derive(Debug, Serialize)]
struct AssertionClaims<'a> {
aud: &'a str,
sub: &'a str,
iss: &'a str,
jti: &'a str,
iat: u64,
exp: u64,
}