use nodedb_types::id::DatabaseId;
use tracing::debug;
use crate::control::security::catalog::StoredOidcProvider;
use crate::control::security::identity::database_set::DatabaseSet;
use crate::control::security::identity::{
AuthMethod, AuthenticatedIdentity, Role, roles_from_external_claims,
};
use crate::control::security::jwt::JwtError;
use crate::control::security::util::base64_url_decode;
use crate::control::state::SharedState;
use crate::types::TenantId;
use super::claim_mapping::apply_claim_mapping;
pub async fn verify_bearer_token(
state: &SharedState,
token: &str,
) -> crate::Result<AuthenticatedIdentity> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(jwt_error_to_crate_error(JwtError::MalformedToken));
}
let payload_bytes = base64_url_decode(parts[1])
.ok_or_else(|| jwt_error_to_crate_error(JwtError::DecodingError))?;
let claims: crate::control::security::jwt::JwtClaims = sonic_rs::from_slice(&payload_bytes)
.map_err(|_| jwt_error_to_crate_error(JwtError::InvalidClaims))?;
let catalog = state.credentials.catalog();
let provider = select_catalog_provider(
catalog
.list_oidc_providers()
.map_err(|_| jwt_error_to_crate_error(JwtError::InvalidIssuer))?,
&claims.iss,
&claims.aud,
)
.map_err(jwt_error_to_crate_error)?;
let jwks = state
.jwks_registry
.as_ref()
.ok_or_else(|| jwt_error_to_crate_error(JwtError::UnsupportedAlgorithm))?;
let verified_claims = jwks
.validate_with_catalog_provider(&provider.provider_name, &provider.jwks_uri, token)
.await
.map_err(jwt_error_to_crate_error)?;
validate_selected_provider_claims(&provider, &verified_claims)
.map_err(jwt_error_to_crate_error)?;
let tenant_id = provider
.tenant_id
.ok_or(crate::Error::OidcProviderTenantUnbound)?;
let tenant_exists = catalog
.load_all_tenants()
.map_err(|_| crate::Error::OidcProviderTenantUnavailable { tenant_id })?
.into_iter()
.any(|tenant| tenant.tenant_id == tenant_id);
if !tenant_exists {
return Err(crate::Error::OidcProviderTenantUnavailable { tenant_id });
}
let mapping = apply_claim_mapping(&verified_claims, &provider.claim_mapping);
let default_db = mapping
.default_database
.map(DatabaseId::new)
.ok_or_else(|| crate::Error::OidcNoDefaultDatabase {
sub: verified_claims.sub.clone(),
})?;
let mut accessible: smallvec::SmallVec<[DatabaseId; 4]> = smallvec::smallvec![default_db];
for &db_raw in &mapping.accessible_databases {
let db = DatabaseId::new(db_raw);
if !accessible.contains(&db) {
accessible.push(db);
}
}
let roles: Vec<Role> = roles_from_external_claims(&mapping.roles, false);
let username = if verified_claims.sub.is_empty() {
format!("oidc_{}", verified_claims.user_id)
} else {
verified_claims.sub.clone()
};
debug!(
provider = %provider.provider_name,
sub = %verified_claims.sub,
iss = %verified_claims.iss,
default_db = %default_db.as_u64(),
"OIDC login succeeded"
);
Ok(AuthenticatedIdentity {
user_id: verified_claims.user_id,
username,
tenant_id: TenantId::new(tenant_id),
auth_method: AuthMethod::OidcBearer,
roles,
is_superuser: false,
default_database: Some(default_db),
accessible_databases: DatabaseSet::Some(accessible),
})
}
fn select_catalog_provider(
providers: Vec<StoredOidcProvider>,
iss: &str,
aud: &str,
) -> Result<StoredOidcProvider, JwtError> {
if iss.is_empty() {
return Err(JwtError::InvalidIssuer);
}
let issuer_providers: Vec<StoredOidcProvider> = providers
.into_iter()
.filter(|provider| provider.issuer == iss)
.collect();
if issuer_providers.is_empty() {
return Err(JwtError::InvalidIssuer);
}
let wildcard_count = issuer_providers
.iter()
.filter(|provider| provider.audience.as_deref().is_none_or(str::is_empty))
.count();
if wildcard_count > 0 && issuer_providers.len() > 1 {
return Err(JwtError::InvalidIssuer);
}
let mut exact_matches: Vec<StoredOidcProvider> = issuer_providers
.iter()
.filter(|provider| {
provider
.audience
.as_deref()
.is_some_and(|expected| !expected.is_empty() && expected == aud)
})
.cloned()
.collect();
if exact_matches.len() > 1 {
return Err(JwtError::InvalidIssuer);
}
if let Some(provider) = exact_matches.pop() {
return Ok(provider);
}
if wildcard_count == 1 {
return issuer_providers
.into_iter()
.next()
.ok_or(JwtError::InvalidIssuer);
}
Err(JwtError::InvalidAudience)
}
fn validate_selected_provider_claims(
provider: &StoredOidcProvider,
claims: &crate::control::security::jwt::JwtClaims,
) -> Result<(), JwtError> {
if claims.iss != provider.issuer {
return Err(JwtError::InvalidIssuer);
}
if provider
.audience
.as_deref()
.is_some_and(|expected| !expected.is_empty() && claims.aud != expected)
{
return Err(JwtError::InvalidAudience);
}
Ok(())
}
fn jwt_error_to_crate_error(_error: JwtError) -> crate::Error {
crate::Error::BadRequest {
detail: "OIDC authentication failed".into(),
}
}