photoncloud-monorepo/iam/crates/iam-authn/src/provider.rs
centra 8f94aee1fa Fix R8: Convert submodule gitlinks to regular directories
- Remove gitlinks (160000 mode) for chainfire, flaredb, iam
- Add workspace contents as regular tracked files
- Update flake.nix to use simple paths instead of builtins.fetchGit

This resolves the nix build failure where submodule directories
appeared empty in the nix store.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 16:51:20 +09:00

245 lines
7.5 KiB
Rust

//! Authentication provider trait and implementations
//!
//! Defines a unified interface for different authentication methods.
use async_trait::async_trait;
use std::sync::Arc;
use iam_types::{
AuthMethod, Error, IamError, InternalTokenClaims, JwtClaims, PrincipalRef, Result,
};
use crate::jwt::JwtVerifier;
use crate::mtls::{CertificateInfo, MtlsVerifier};
use crate::token::InternalTokenService;
/// Result of authentication
#[derive(Debug, Clone)]
pub struct AuthnResult {
/// Authenticated principal reference
pub principal_ref: PrincipalRef,
/// Authentication method used
pub auth_method: AuthMethod,
/// Node ID (for service accounts)
pub node_id: Option<String>,
/// Organization ID
pub org_id: Option<String>,
/// Project ID
pub project_id: Option<String>,
/// Groups (from JWT)
pub groups: Vec<String>,
/// Original claims (if JWT)
pub jwt_claims: Option<JwtClaims>,
/// Internal token claims (if internal token)
pub internal_claims: Option<InternalTokenClaims>,
}
/// Authentication credentials
#[derive(Debug, Clone)]
pub enum AuthnCredentials {
/// Bearer token (JWT or internal token)
BearerToken(String),
/// mTLS certificate info
Certificate(CertificateInfo),
/// API key
ApiKey(String),
}
/// Authentication provider trait
#[async_trait]
pub trait AuthnProvider: Send + Sync {
/// Authenticate using the provided credentials
async fn authenticate(&self, credentials: &AuthnCredentials) -> Result<AuthnResult>;
}
/// Combined authentication provider supporting multiple methods
pub struct CombinedAuthProvider {
jwt_verifier: Option<Arc<JwtVerifier>>,
mtls_verifier: Option<Arc<MtlsVerifier>>,
internal_token_service: Option<Arc<InternalTokenService>>,
}
impl CombinedAuthProvider {
/// Create a new combined auth provider
pub fn new() -> Self {
Self {
jwt_verifier: None,
mtls_verifier: None,
internal_token_service: None,
}
}
/// Add JWT verifier
pub fn with_jwt(mut self, verifier: JwtVerifier) -> Self {
self.jwt_verifier = Some(Arc::new(verifier));
self
}
/// Add mTLS verifier
pub fn with_mtls(mut self, verifier: MtlsVerifier) -> Self {
self.mtls_verifier = Some(Arc::new(verifier));
self
}
/// Add internal token service
pub fn with_internal_token(mut self, service: InternalTokenService) -> Self {
self.internal_token_service = Some(Arc::new(service));
self
}
/// Authenticate a bearer token
async fn authenticate_bearer(&self, token: &str) -> Result<AuthnResult> {
// Try internal token first (faster, local verification)
if let Some(internal_service) = &self.internal_token_service {
if let Ok(claims) = internal_service.verify(token).await {
return Ok(AuthnResult {
principal_ref: PrincipalRef::new(
claims.principal_kind.clone(),
&claims.principal_id,
),
auth_method: AuthMethod::Internal,
node_id: claims.node_id.clone(),
org_id: claims.org_id.clone(),
project_id: claims.project_id.clone(),
groups: vec![],
jwt_claims: None,
internal_claims: Some(claims),
});
}
}
// Try JWT verification
if let Some(jwt_verifier) = &self.jwt_verifier {
let claims = jwt_verifier.verify(token).await?;
// Map JWT sub to principal
let principal_ref = PrincipalRef::user(&claims.sub);
return Ok(AuthnResult {
principal_ref,
auth_method: AuthMethod::Jwt,
node_id: None,
org_id: claims.org_id.clone(),
project_id: claims.project_id.clone(),
groups: claims.groups.clone(),
jwt_claims: Some(claims),
internal_claims: None,
});
}
Err(Error::Iam(IamError::AuthnFailed(
"No authentication provider configured for bearer tokens".into(),
)))
}
/// Authenticate using certificate
fn authenticate_certificate(&self, cert_info: &CertificateInfo) -> Result<AuthnResult> {
let mtls_verifier = self.mtls_verifier.as_ref().ok_or_else(|| {
Error::Iam(IamError::AuthnFailed(
"mTLS authentication not configured".into(),
))
})?;
let result = mtls_verifier.verify(cert_info)?;
Ok(AuthnResult {
principal_ref: result.principal_ref,
auth_method: AuthMethod::Mtls,
node_id: result.node_id,
org_id: None,
project_id: None,
groups: vec![],
jwt_claims: None,
internal_claims: None,
})
}
/// Authenticate using API key
async fn authenticate_api_key(&self, _api_key: &str) -> Result<AuthnResult> {
// API key authentication would typically:
// 1. Look up the API key in the store
// 2. Verify it's valid and not expired
// 3. Return the associated principal
// For now, this is a stub
Err(Error::Iam(IamError::AuthnFailed(
"API key authentication not yet implemented".into(),
)))
}
}
impl Default for CombinedAuthProvider {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl AuthnProvider for CombinedAuthProvider {
async fn authenticate(&self, credentials: &AuthnCredentials) -> Result<AuthnResult> {
match credentials {
AuthnCredentials::BearerToken(token) => self.authenticate_bearer(token).await,
AuthnCredentials::Certificate(cert_info) => self.authenticate_certificate(cert_info),
AuthnCredentials::ApiKey(key) => self.authenticate_api_key(key).await,
}
}
}
/// Extract authentication credentials from HTTP headers
pub fn extract_credentials_from_headers(
authorization: Option<&str>,
cert_info: Option<CertificateInfo>,
) -> Option<AuthnCredentials> {
// Check for mTLS first (if certificate is provided)
if let Some(cert) = cert_info {
return Some(AuthnCredentials::Certificate(cert));
}
// Check Authorization header
if let Some(auth_header) = authorization {
if let Some(token) = auth_header.strip_prefix("Bearer ") {
return Some(AuthnCredentials::BearerToken(token.to_string()));
}
if let Some(key) = auth_header.strip_prefix("ApiKey ") {
return Some(AuthnCredentials::ApiKey(key.to_string()));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_combined_provider_no_config() {
let provider = CombinedAuthProvider::new();
let result = provider
.authenticate(&AuthnCredentials::BearerToken("some-token".into()))
.await;
assert!(result.is_err());
}
#[test]
fn test_extract_bearer_token() {
let creds = extract_credentials_from_headers(Some("Bearer abc123"), None);
assert!(matches!(
creds,
Some(AuthnCredentials::BearerToken(t)) if t == "abc123"
));
}
#[test]
fn test_extract_api_key() {
let creds = extract_credentials_from_headers(Some("ApiKey secret-key"), None);
assert!(matches!(
creds,
Some(AuthnCredentials::ApiKey(k)) if k == "secret-key"
));
}
}