use async_trait::async_trait;
use openidconnect::AdditionalClaims;
use openidconnect::Client;
use openidconnect::ClientId;
use openidconnect::ClientSecret;
use openidconnect::CsrfToken;
use openidconnect::EmptyExtraTokenFields;
use openidconnect::EndpointMaybeSet;
use openidconnect::EndpointNotSet;
use openidconnect::EndpointSet;
use openidconnect::IdTokenFields;
use openidconnect::IssuerUrl;
use openidconnect::Nonce;
use openidconnect::RedirectUrl;
use openidconnect::StandardErrorResponse;
use openidconnect::StandardTokenResponse;
use openidconnect::TokenResponse;
use openidconnect::core::CoreAuthDisplay;
use openidconnect::core::CoreAuthPrompt;
use openidconnect::core::CoreAuthenticationFlow;
use openidconnect::core::CoreErrorResponseType;
use openidconnect::core::CoreGenderClaim;
use openidconnect::core::CoreJsonWebKey;
use openidconnect::core::CoreJweContentEncryptionAlgorithm;
use openidconnect::core::CoreJwsSigningAlgorithm;
use openidconnect::core::CoreProviderMetadata;
use openidconnect::core::CoreRevocableToken;
use openidconnect::core::CoreRevocationErrorResponse;
use openidconnect::core::CoreTokenIntrospectionResponse;
use openidconnect::core::CoreTokenType;
use serde::Deserialize;
use serde::Serialize;
use url::Url;
use super::super::super::domain::AuthorizeData;
use super::super::super::domain::CallbackRequestParams;
use super::super::super::domain::OidcSessionPersisted;
use super::super::super::ports::authentication_engine::AuthenticationEngine;
use super::utils::extract_user_email;
use super::utils::extract_user_groups;
use super::utils::extract_user_id;
use super::utils::extract_user_name;
use crate::authentication::ports::authentication_engine::AuthenticationEngineError;
use crate::user::domain::User;
#[derive(Debug, Clone)]
pub struct OidcAuthenticationEngine {
oidc_client: PotreeAuthClient,
groups_claim: String,
}
impl OidcAuthenticationEngine {
pub async fn new(
idp_url: Url,
redirect_url: Url,
client_id: String,
client_secret: String,
groups_claim: String,
) -> Result<Self, AuthenticationEngineError> {
let idp_url = IssuerUrl::from_url(idp_url);
let redirect_url = RedirectUrl::from_url(redirect_url);
let client_id = ClientId::new(client_id);
let client_secret = ClientSecret::new(client_secret);
let http_client = reqwest::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(|e| AuthenticationEngineError::Infrastructure {
message: format!("unable to build OIDC http client: {e}"),
})?;
let provider_metadata = CoreProviderMetadata::discover_async(idp_url, &http_client)
.await
.map_err(|e| AuthenticationEngineError::IdpExchange {
message: format!("unable to perform OIDC discovery: {e}"),
})?;
let oidc_client = PotreeAuthClient::from_provider_metadata(
provider_metadata,
client_id,
Some(client_secret),
)
.set_redirect_uri(redirect_url);
Ok(Self {
oidc_client,
groups_claim,
})
}
#[tracing::instrument]
async fn login(&self) -> Result<AuthorizeData, AuthenticationEngineError> {
let (auth_url, state, nonce) = self
.oidc_client
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.url();
Ok(AuthorizeData {
auth_url,
persisted_data: OidcSessionPersisted { state, nonce },
})
}
#[tracing::instrument]
async fn callback(
&self,
callback_params: CallbackRequestParams,
persisted_data: OidcSessionPersisted,
) -> Result<User, AuthenticationEngineError> {
let http_client = reqwest::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(|e| AuthenticationEngineError::Infrastructure {
message: format!("unable to build OIDC http client: {e}"),
})?;
if callback_params.state.secret() != persisted_data.state.secret() {
return Err(AuthenticationEngineError::Validation {
message: "the session and request `state`s do not match".to_owned(),
});
}
let token_response = self
.oidc_client
.exchange_code(callback_params.code)
.map_err(|e| AuthenticationEngineError::Infrastructure {
message: format!("unable to initialize OIDC code exchange client: {e}"),
})?
.request_async(&http_client)
.await
.map_err(|e| AuthenticationEngineError::IdpExchange {
message: format!("unable to perform token request: {e}"),
})?;
let id_token_claims = token_response
.id_token()
.ok_or(AuthenticationEngineError::Validation {
message: "IdP did not return id_token".to_owned(),
})?
.claims(&self.oidc_client.id_token_verifier(), &persisted_data.nonce)
.map_err(|err| AuthenticationEngineError::Validation {
message: format!("unable to extract claims from id_token: {err}"),
})?;
Ok(User {
id: extract_user_id(id_token_claims),
name: extract_user_name(id_token_claims)?,
email: extract_user_email(id_token_claims)?,
groups: extract_user_groups(id_token_claims, &self.groups_claim),
})
}
}
#[async_trait]
impl AuthenticationEngine for OidcAuthenticationEngine {
async fn authorize(&self) -> Result<AuthorizeData, AuthenticationEngineError> {
Self::login(self).await
}
async fn callback(
&self,
callback_params: CallbackRequestParams,
persisted_data: OidcSessionPersisted,
) -> Result<User, AuthenticationEngineError> {
Self::callback(self, callback_params, persisted_data).await
}
}
pub type PotreeAuthTokenFields = IdTokenFields<
PotreeAuthClaims,
EmptyExtraTokenFields,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
>;
pub type PotreeAuthTokenResponse = StandardTokenResponse<PotreeAuthTokenFields, CoreTokenType>;
type PotreeAuthClient<
HasAuthUrl = EndpointSet,
HasDeviceAuthUrl = EndpointNotSet,
HasIntrospectionUrl = EndpointNotSet,
HasRevocationUrl = EndpointNotSet,
HasUserInfoUrl = EndpointMaybeSet,
HasTokenUrl = EndpointMaybeSet,
> = Client<
PotreeAuthClaims,
CoreAuthDisplay,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJsonWebKey,
CoreAuthPrompt,
StandardErrorResponse<CoreErrorResponseType>,
PotreeAuthTokenResponse,
CoreTokenIntrospectionResponse,
CoreRevocableToken,
CoreRevocationErrorResponse,
HasAuthUrl,
HasDeviceAuthUrl,
HasIntrospectionUrl,
HasRevocationUrl,
HasTokenUrl,
HasUserInfoUrl,
>;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PotreeAuthClaims(pub serde_json::Value);
impl AdditionalClaims for PotreeAuthClaims {}