use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing;
use crate::error::{PepError, Result};
use crate::oidc_client::{OidcClient, TokenResponse};
#[allow(async_fn_in_trait)]
pub trait TokenProvider: Send + Sync {
async fn get_token(&self) -> Result<String>;
}
#[derive(Clone, Debug)]
struct CachedToken {
token: String,
expires_at: Instant,
}
impl CachedToken {
fn from_response(token_response: &TokenResponse) -> Self {
let expires_in = token_response.expires_in.unwrap_or(900);
let buffer_secs = 30;
let effective_secs = expires_in.saturating_sub(buffer_secs);
Self {
token: token_response.access_token.clone(),
expires_at: Instant::now() + Duration::from_secs(effective_secs),
}
}
fn is_valid(&self) -> bool {
Instant::now() < self.expires_at
}
}
#[derive(Clone)]
pub struct StaticTokenProvider {
token: String,
}
impl std::fmt::Debug for StaticTokenProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StaticTokenProvider")
.field("token", &format!("{}...", &self.token[..self.token.len().min(8)]))
.finish()
}
}
impl StaticTokenProvider {
pub fn new(token: String) -> Self {
Self { token }
}
}
impl TokenProvider for StaticTokenProvider {
async fn get_token(&self) -> Result<String> {
Ok(self.token.clone())
}
}
#[derive(Debug, Clone)]
pub struct ServiceAccountConfig {
pub service_token: String,
pub issuer_url: String,
pub client_id: String,
pub client_secret: Option<String>,
pub audience: String,
pub scope: Option<String>,
}
#[derive(Clone)]
pub struct ServiceAccountTokenProvider {
oidc_client: OidcClient,
config: ServiceAccountConfig,
cache: Arc<RwLock<Option<CachedToken>>>,
}
impl std::fmt::Debug for ServiceAccountTokenProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServiceAccountTokenProvider")
.field("audience", &self.config.audience)
.finish()
}
}
impl ServiceAccountTokenProvider {
pub fn new(config: ServiceAccountConfig) -> Self {
Self {
oidc_client: OidcClient::new(),
config,
cache: Arc::new(RwLock::new(None)),
}
}
pub fn with_client(oidc_client: OidcClient, config: ServiceAccountConfig) -> Self {
Self {
oidc_client,
config,
cache: Arc::new(RwLock::new(None)),
}
}
async fn exchange(&self) -> Result<CachedToken> {
tracing::debug!(
"Exchanging service account token for audience '{}'",
self.config.audience
);
let response = self
.oidc_client
.exchange_token(
&self.config.issuer_url,
&self.config.client_id,
self.config.client_secret.as_deref(),
&self.config.service_token,
&self.config.audience,
self.config.scope.as_deref(),
)
.await?;
tracing::info!(
"Token exchange successful, expires_in={:?}s",
response.expires_in
);
Ok(CachedToken::from_response(&response))
}
}
impl TokenProvider for ServiceAccountTokenProvider {
async fn get_token(&self) -> Result<String> {
{
let cache = self.cache.read().await;
if let Some(cached) = cache.as_ref() {
if cached.is_valid() {
return Ok(cached.token.clone());
}
}
}
let cached = self.exchange().await?;
let token = cached.token.clone();
{
let mut cache = self.cache.write().await;
*cache = Some(cached);
}
Ok(token)
}
}
#[derive(Debug, Clone)]
pub struct InteractiveConfig {
pub issuer_url: String,
pub client_id: String,
pub client_secret: Option<String>,
pub redirect_uri: String,
pub scope: String,
}
#[derive(Clone)]
pub struct InteractiveTokenProvider {
#[allow(dead_code)]
config: InteractiveConfig,
#[allow(dead_code)]
oidc_client: OidcClient,
cache: Arc<RwLock<Option<CachedToken>>>,
}
impl std::fmt::Debug for InteractiveTokenProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InteractiveTokenProvider")
.field("issuer_url", &self.config.issuer_url)
.finish()
}
}
impl InteractiveTokenProvider {
pub fn new(config: InteractiveConfig) -> Self {
Self {
oidc_client: OidcClient::new(),
config,
cache: Arc::new(RwLock::new(None)),
}
}
}
impl TokenProvider for InteractiveTokenProvider {
async fn get_token(&self) -> Result<String> {
{
let cache = self.cache.read().await;
if let Some(cached) = cache.as_ref() {
if cached.is_valid() {
return Ok(cached.token.clone());
}
}
}
Err(PepError::BadRequest(
"InteractiveTokenProvider: not yet implemented".to_string(),
))
}
}
#[derive(Clone, Debug)]
pub enum TokenProviderEnum {
Static(StaticTokenProvider),
ServiceAccount(ServiceAccountTokenProvider),
Interactive(InteractiveTokenProvider),
}
impl TokenProvider for TokenProviderEnum {
async fn get_token(&self) -> Result<String> {
match self {
Self::Static(p) => p.get_token().await,
Self::ServiceAccount(p) => p.get_token().await,
Self::Interactive(p) => p.get_token().await,
}
}
}
impl From<String> for TokenProviderEnum {
fn from(token: String) -> Self {
Self::Static(StaticTokenProvider::new(token))
}
}
impl From<Option<String>> for TokenProviderEnum {
fn from(token: Option<String>) -> Self {
Self::Static(StaticTokenProvider::new(token.unwrap_or_default()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_static_token_provider() {
let rt = tokio::runtime::Runtime::new().unwrap();
let provider = StaticTokenProvider::new("test-token".to_string());
let token = rt.block_on(provider.get_token()).unwrap();
assert_eq!(token, "test-token");
}
#[test]
fn test_cached_token_from_response() {
let response = TokenResponse {
access_token: "abc123".to_string(),
token_type: "Bearer".to_string(),
expires_in: Some(900),
refresh_token: None,
id_token: None,
scope: None,
};
let cached = CachedToken::from_response(&response);
assert_eq!(cached.token, "abc123");
assert!(cached.is_valid());
}
#[test]
fn test_cached_token_expiry() {
let response = TokenResponse {
access_token: "abc123".to_string(),
token_type: "Bearer".to_string(),
expires_in: Some(0), refresh_token: None,
id_token: None,
scope: None,
};
let cached = CachedToken::from_response(&response);
assert!(!cached.is_valid());
}
#[test]
fn test_token_provider_enum_static() {
let rt = tokio::runtime::Runtime::new().unwrap();
let provider: TokenProviderEnum = TokenProviderEnum::Static(
StaticTokenProvider::new("enum-test".to_string()),
);
let token = rt.block_on(provider.get_token()).unwrap();
assert_eq!(token, "enum-test");
}
#[test]
fn test_token_provider_enum_from_string() {
let rt = tokio::runtime::Runtime::new().unwrap();
let provider: TokenProviderEnum = "direct-string".to_string().into();
let token = rt.block_on(provider.get_token()).unwrap();
assert_eq!(token, "direct-string");
}
#[test]
fn test_token_provider_enum_from_option() {
let rt = tokio::runtime::Runtime::new().unwrap();
let provider: TokenProviderEnum = Some("some-token".to_string()).into();
let token = rt.block_on(provider.get_token()).unwrap();
assert_eq!(token, "some-token");
}
#[test]
fn test_service_account_config_builder() {
let config = ServiceAccountConfig {
service_token: "svc-token".to_string(),
issuer_url: "https://idm.example.com/oauth2/openid/pdt-api".to_string(),
client_id: "pdt-api".to_string(),
client_secret: None,
audience: "pdt-api".to_string(),
scope: Some("openid profile email".to_string()),
};
let _provider = ServiceAccountTokenProvider::new(config);
}
}