remove OIDC/Postgres, replace ApiError w/ AppError, move params to api-types
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user