use std::sync::Arc;
use log::{debug, warn};
use openidconnect::{
AccessToken, ClientId, ClientSecret, IntrospectionUrl, IssuerUrl,
TokenIntrospectionResponse as _, core::CoreClient,
};
use pib_service_api_auth::{
ApiAuth, Error,
user::{Issuer, OidcSub, UserInfo},
};
use pib_service_inventory::{InventoryProvider, NewUser, UpdateUser};
use pib_service_api_auth::Result;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::ClientWrapper;
#[derive(Debug, Clone)]
struct Configuration {
issuer_url: IssuerUrl,
client_id: ClientId,
client_secret: ClientSecret,
}
#[derive(Debug, Clone)]
pub struct OidcAuth {
inventory_provider: Arc<dyn InventoryProvider>,
configuration: Arc<Configuration>,
}
impl OidcAuth {
pub fn new(
issuer_url: IssuerUrl,
client_id: ClientId,
client_secret: ClientSecret,
inventory_provider: Arc<dyn InventoryProvider>,
) -> Self {
Self {
configuration: Arc::new(Configuration {
issuer_url,
client_id,
client_secret,
}),
inventory_provider,
}
}
}
#[async_trait::async_trait]
impl ApiAuth for OidcAuth {
async fn authorize(
&self,
authorization_header: http::HeaderValue,
) -> Result<Option<UserInfo>, Error> {
let Some(OidcUserInfo {
issuer,
sub,
display_name,
}) = self.configuration.check_auth(authorization_header).await?
else {
return Ok(None);
};
let mut inventory = self.inventory_provider.get_inventory().await.unwrap();
let pib_service_inventory::User {
id,
is_superuser,
sub,
display_name,
} = match inventory.get_user_by_sub(sub.to_string()).await {
Ok(user) if user.display_name.is_none() => {
if let Some(display_name) = display_name.clone() {
inventory
.update_user(
UpdateUser::new(user.id)
.display_name(display_name)
.sub(user.sub.clone()),
)
.await
.unwrap()
} else {
user
}
}
Ok(user) => user,
Err(e) if e.is_not_found() => inventory
.create_user(NewUser {
sub: sub.to_string(),
display_name: display_name.clone(),
is_superuser: false,
})
.await
.unwrap(),
Err(e) => {
panic!("Error reading user information from inventory: {e}");
}
};
Ok(Some(UserInfo {
id,
issuer,
sub: OidcSub(sub),
display_name,
is_superuser,
}))
}
}
impl Configuration {
async fn check_auth(
&self,
authorization_header: http::HeaderValue,
) -> Result<Option<OidcUserInfo>> {
const HEADER_IDENTIFIER: &str = "bearer ";
let authorization_header = authorization_header.to_str().unwrap();
if !authorization_header
.to_lowercase()
.starts_with(HEADER_IDENTIFIER)
{
panic!("Authorization header must start with \"bearer\" (lower- or upper-case)");
}
let (_, token) = authorization_header.split_at(HEADER_IDENTIFIER.len());
let http_client = ClientWrapper(
reqwest::ClientBuilder::new()
.connection_verbose(true)
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(Error::from_source)?,
);
#[derive(Debug, Clone, Deserialize, Serialize)]
struct AdditionalProviderMetadata {
pub introspection_endpoint: Option<Url>,
}
impl openidconnect::AdditionalProviderMetadata for AdditionalProviderMetadata {}
type ProviderMetadata = openidconnect::ProviderMetadata<
AdditionalProviderMetadata,
openidconnect::core::CoreAuthDisplay,
openidconnect::core::CoreClientAuthMethod,
openidconnect::core::CoreClaimName,
openidconnect::core::CoreClaimType,
openidconnect::core::CoreGrantType,
openidconnect::core::CoreJweContentEncryptionAlgorithm,
openidconnect::core::CoreJweKeyManagementAlgorithm,
openidconnect::core::CoreJsonWebKey,
openidconnect::core::CoreResponseMode,
openidconnect::core::CoreResponseType,
openidconnect::core::CoreSubjectIdentifierType,
>;
let provider_metadata =
ProviderMetadata::discover_async(self.issuer_url.clone(), &http_client)
.await
.map_err(Error::from_source)?;
let Some(introspection_url) = provider_metadata
.additional_metadata()
.introspection_endpoint
.clone()
else {
panic!("OIDC service does not support token introspection");
};
let client = CoreClient::from_provider_metadata(
provider_metadata,
self.client_id.clone(),
Some(self.client_secret.clone()),
)
.set_introspection_url(IntrospectionUrl::from_url(introspection_url.clone()));
let token = AccessToken::new(token.to_string());
debug!("Starting introspection of access token at URL {introspection_url}");
let introspection = client
.introspect(&token)
.request_async(&http_client)
.await
.map_err(Error::from_source)?;
debug!("Introspection response: {introspection:?}");
if !introspection.active() {
debug!("Access token is not active");
return Ok(None);
}
let Some(iss) = introspection.iss() else {
warn!("Received an access token without an issuer");
return Ok(None);
};
let Some(sub) = introspection.sub() else {
warn!("Received an access token without a subject");
return Ok(None);
};
let display_name = introspection.username().map(|s| s.to_string());
let issuer = Issuer(iss.to_string());
let sub = OidcSub(sub.to_string());
Ok(Some(OidcUserInfo {
issuer,
sub,
display_name,
}))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct OidcUserInfo {
issuer: Issuer,
sub: OidcSub,
display_name: Option<String>,
}