remove OIDC/Postgres, replace ApiError w/ AppError, move params to api-types

This commit is contained in:
2026-07-12 05:14:17 +02:00
parent c0e685a4ee
commit 9b18d3ff6d
42 changed files with 243 additions and 3367 deletions

View File

@@ -6,7 +6,6 @@ edition = "2024"
[features]
default = ["jwt"]
jwt = ["dep:jsonwebtoken"]
oidc = ["dep:openidconnect", "dep:reqwest", "dep:url"]
[dependencies]
domain = { workspace = true }
@@ -18,10 +17,5 @@ serde_json = { workspace = true }
# JWT deps
jsonwebtoken = { workspace = true, optional = true }
# OIDC deps (optional)
openidconnect = { version = "4", optional = true }
reqwest = { workspace = true, optional = true }
url = { workspace = true, optional = true }
# Password hashing
password-auth = "1"

View File

@@ -3,13 +3,7 @@ pub mod password;
#[cfg(feature = "jwt")]
pub mod jwt;
#[cfg(feature = "oidc")]
pub mod oidc;
pub use password::PasswordAuthService;
#[cfg(feature = "jwt")]
pub use jwt::{JwtClaims, JwtConfig, JwtError, JwtValidator};
#[cfg(feature = "oidc")]
pub use oidc::{OidcService, OidcState, OidcUser};

View File

@@ -1,245 +0,0 @@
use domain::{
AuthorizationCode, AuthorizationUrlData, ClientId, ClientSecret, CsrfToken, IssuerUrl,
OidcNonce, PkceVerifier, RedirectUrl, ResourceId,
};
use openidconnect::{
AccessTokenHash, Client, EmptyAdditionalClaims, EndpointMaybeSet, EndpointNotSet, EndpointSet,
OAuth2TokenResponse, PkceCodeChallenge, Scope, StandardErrorResponse, TokenResponse,
UserInfoClaims,
core::{
CoreAuthDisplay, CoreAuthPrompt, CoreAuthenticationFlow, CoreClient, CoreErrorResponseType,
CoreGenderClaim, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreProviderMetadata,
CoreRevocableToken, CoreRevocationErrorResponse, CoreTokenIntrospectionResponse,
CoreTokenResponse,
},
reqwest,
};
use serde::{Deserialize, Serialize};
pub type OidcClient = Client<
EmptyAdditionalClaims,
CoreAuthDisplay,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJsonWebKey,
CoreAuthPrompt,
StandardErrorResponse<CoreErrorResponseType>,
CoreTokenResponse,
CoreTokenIntrospectionResponse,
CoreRevocableToken,
CoreRevocationErrorResponse,
EndpointSet,
EndpointNotSet,
EndpointNotSet,
EndpointNotSet,
EndpointMaybeSet,
EndpointMaybeSet,
>;
#[derive(Debug, thiserror::Error)]
pub enum OidcError {
#[error("OIDC discovery failed: {0}")]
Discovery(String),
#[error("Token exchange failed: {0}")]
TokenExchange(String),
#[error("ID token verification failed: {0}")]
IdTokenVerification(String),
#[error("Missing ID token in response")]
MissingIdToken,
#[error("Invalid access token hash")]
InvalidAccessTokenHash,
#[error("User has no email address")]
MissingEmail,
#[error("HTTP client error: {0}")]
Http(String),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OidcState {
pub csrf_token: CsrfToken,
pub nonce: OidcNonce,
pub pkce_verifier: PkceVerifier,
}
#[derive(Debug)]
pub struct OidcUser {
pub subject: String,
pub email: String,
}
#[derive(Clone)]
pub struct OidcService {
client: OidcClient,
http_client: reqwest::Client,
resource_id: Option<ResourceId>,
}
impl OidcService {
pub async fn new(
issuer: IssuerUrl,
client_id: ClientId,
client_secret: Option<ClientSecret>,
redirect_url: RedirectUrl,
resource_id: Option<ResourceId>,
) -> Result<Self, OidcError> {
tracing::debug!("OIDC setup: client_id={client_id}, redirect={redirect_url}");
tracing::debug!(
"OIDC setup: secret={}",
if client_secret.is_some() {
"SET"
} else {
"NONE"
}
);
let http_client = reqwest::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(|e| OidcError::Http(e.to_string()))?;
let provider_metadata = CoreProviderMetadata::discover_async(
openidconnect::IssuerUrl::new(issuer.as_ref().to_string())
.map_err(|e| OidcError::Discovery(e.to_string()))?,
&http_client,
)
.await
.map_err(|e| OidcError::Discovery(e.to_string()))?;
let oidc_client_id = openidconnect::ClientId::new(client_id.as_ref().to_string());
let oidc_client_secret = client_secret
.as_ref()
.filter(|s| !s.is_empty())
.map(|s| openidconnect::ClientSecret::new(s.as_ref().to_string()));
let oidc_redirect_url =
openidconnect::RedirectUrl::new(redirect_url.as_ref().to_string())
.map_err(|e| OidcError::Discovery(e.to_string()))?;
let client = CoreClient::from_provider_metadata(
provider_metadata,
oidc_client_id,
oidc_client_secret,
)
.set_redirect_uri(oidc_redirect_url);
Ok(Self {
client,
http_client,
resource_id,
})
}
pub fn get_authorization_url(&self) -> (AuthorizationUrlData, OidcState) {
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let (auth_url, csrf_token, nonce) = self
.client
.authorize_url(
CoreAuthenticationFlow::AuthorizationCode,
openidconnect::CsrfToken::new_random,
openidconnect::Nonce::new_random,
)
.add_scope(Scope::new("profile".to_string()))
.add_scope(Scope::new("email".to_string()))
.set_pkce_challenge(pkce_challenge)
.url();
let oidc_state = OidcState {
csrf_token: CsrfToken::new(csrf_token.secret().to_string()),
nonce: OidcNonce::new(nonce.secret().to_string()),
pkce_verifier: PkceVerifier::new(pkce_verifier.secret().to_string()),
};
let auth_data = AuthorizationUrlData {
url: auth_url.into(),
csrf_token: oidc_state.csrf_token.clone(),
nonce: oidc_state.nonce.clone(),
pkce_verifier: oidc_state.pkce_verifier.clone(),
};
(auth_data, oidc_state)
}
pub async fn resolve_callback(
&self,
code: AuthorizationCode,
nonce: OidcNonce,
pkce_verifier: PkceVerifier,
) -> Result<OidcUser, OidcError> {
let oidc_pkce_verifier =
openidconnect::PkceCodeVerifier::new(pkce_verifier.as_ref().to_string());
let oidc_nonce = openidconnect::Nonce::new(nonce.as_ref().to_string());
let token_response = self
.client
.exchange_code(openidconnect::AuthorizationCode::new(
code.as_ref().to_string(),
))
.map_err(|e| OidcError::TokenExchange(e.to_string()))?
.set_pkce_verifier(oidc_pkce_verifier)
.request_async(&self.http_client)
.await
.map_err(|e| OidcError::TokenExchange(e.to_string()))?;
let id_token = token_response
.id_token()
.ok_or(OidcError::MissingIdToken)?;
let mut id_token_verifier = self.client.id_token_verifier().clone();
if let Some(resource_id) = &self.resource_id {
let trusted = resource_id.as_ref().to_string();
id_token_verifier =
id_token_verifier.set_other_audience_verifier_fn(move |aud| aud.as_str() == trusted);
}
let claims = id_token
.claims(&id_token_verifier, &oidc_nonce)
.map_err(|e| OidcError::IdTokenVerification(e.to_string()))?;
if let Some(expected_hash) = claims.access_token_hash() {
let actual_hash = AccessTokenHash::from_token(
token_response.access_token(),
id_token
.signing_alg()
.map_err(|e| OidcError::IdTokenVerification(e.to_string()))?,
id_token
.signing_key(&id_token_verifier)
.map_err(|e| OidcError::IdTokenVerification(e.to_string()))?,
)
.map_err(|e| OidcError::IdTokenVerification(e.to_string()))?;
if actual_hash != *expected_hash {
return Err(OidcError::InvalidAccessTokenHash);
}
}
let email = if let Some(email) = claims.email() {
Some(email.as_str().to_string())
} else {
tracing::debug!("Email missing in ID token, fetching UserInfo");
let user_info: UserInfoClaims<EmptyAdditionalClaims, CoreGenderClaim> = self
.client
.user_info(token_response.access_token().clone(), None)
.map_err(|e| OidcError::TokenExchange(e.to_string()))?
.request_async(&self.http_client)
.await
.map_err(|e| OidcError::TokenExchange(e.to_string()))?;
user_info.email().map(|e| e.as_str().to_string())
};
let email = email.ok_or(OidcError::MissingEmail)?;
Ok(OidcUser {
subject: claims.subject().to_string(),
email,
})
}
}