Files
foxhunt/services/api/src/metrics/auth_metrics.rs
jgrusewski e50ea55064 feat: create services/api/ — unified gRPC gateway with tonic-web
Copied from api_gateway, removed REST handlers (port 8080),
added tonic-web + CORS for grpc-web browser access.
Binary renamed: api-gateway → api

Changes:
- Package name: api-gateway → api
- Deleted src/handlers/ (REST ML endpoints on port 8080)
- Added tonic-web 0.13 + tower-http CORS layer
- Server::builder().accept_http1(true) for grpc-web
- CORS_ORIGINS env var (default http://localhost:5173)
- Metrics server on port 9091 (axum) preserved
- All 95 lib tests pass, 0 clippy warnings
- Added services/api to workspace members

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:32:46 +01:00

413 lines
15 KiB
Rust

//! Authentication Metrics for 6-Layer Security
//!
//! Tracks performance and errors for:
//! - JWT extraction and validation
//! - Revocation checking (Redis)
//! - RBAC permission checks (cached)
//! - Rate limiting (token bucket)
//! - MFA verification
//! - Audit logging
use prometheus::{Counter, CounterVec, Histogram, HistogramOpts, IntGauge, Opts, Registry};
/// Authentication metrics for all 6 layers
pub struct AuthMetrics {
// === Request Counters ===
/// Total authentication requests
pub auth_requests_total: Counter,
/// Successful authentications
pub auth_requests_success: Counter,
/// Failed authentications
pub auth_requests_failure: Counter,
// === Layer-Specific Latencies (microseconds) ===
/// JWT extraction and parsing latency
pub jwt_extraction_duration_us: Histogram,
/// JWT signature validation latency
pub jwt_validation_duration_us: Histogram,
/// JWT revocation check latency (Redis)
pub revocation_check_duration_us: Histogram,
/// RBAC permission check latency
pub rbac_check_duration_us: Histogram,
/// Rate limit check latency
pub rate_limit_check_duration_us: Histogram,
/// MFA verification latency (TOTP)
pub mfa_verification_duration_us: Histogram,
/// Total auth pipeline latency
pub auth_total_duration_us: Histogram,
// === Error Counters by Type ===
/// Missing Authorization header
pub auth_errors_missing_jwt: Counter,
/// Malformed JWT token
pub auth_errors_invalid_jwt: Counter,
/// JWT signature verification failed
pub auth_errors_signature_failed: Counter,
/// JWT expired
pub auth_errors_expired_jwt: Counter,
/// JWT revoked (blacklisted)
pub auth_errors_revoked_jwt: Counter,
/// RBAC permission denied
pub auth_errors_permission_denied: Counter,
/// Rate limit exceeded
pub auth_errors_rate_limited: Counter,
/// MFA verification failed
pub auth_errors_mfa_failed: Counter,
/// Redis connection errors
pub auth_errors_redis_failure: Counter,
// === Current State Gauges ===
/// Number of active JWT tokens (estimated)
pub active_jwt_tokens: IntGauge,
/// Number of revoked tokens in cache
pub revoked_tokens_cached: IntGauge,
/// RBAC permission cache size
pub rbac_cache_size: IntGauge,
/// Rate limiter entries
pub rate_limiter_entries: IntGauge,
// === Per-User Metrics ===
/// Requests per user (labeled by user_id)
pub requests_by_user: CounterVec,
/// Auth failures per user
pub auth_failures_by_user: CounterVec,
/// Rate limit hits per user
pub rate_limits_by_user: CounterVec,
// === Cache Performance ===
/// JWT decoding key cache hits
pub jwt_cache_hits: Counter,
/// JWT decoding key cache misses
pub jwt_cache_misses: Counter,
/// RBAC permission cache hits
pub rbac_cache_hits: Counter,
/// RBAC permission cache misses
pub rbac_cache_misses: Counter,
// === Performance Targets ===
/// Counter for auth requests meeting <10μs SLA
pub auth_sla_met: Counter,
/// Counter for auth requests exceeding <10μs SLA
pub auth_sla_exceeded: Counter,
}
impl AuthMetrics {
/// Create new authentication metrics and register with Prometheus
pub fn new(registry: &Registry) -> Result<Self, prometheus::Error> {
// === Request Counters ===
let auth_requests_total = Counter::with_opts(Opts::new(
"api_auth_requests_total",
"Total authentication requests",
))?;
registry.register(Box::new(auth_requests_total.clone()))?;
let auth_requests_success = Counter::with_opts(Opts::new(
"api_auth_requests_success",
"Successful authentication requests",
))?;
registry.register(Box::new(auth_requests_success.clone()))?;
let auth_requests_failure = Counter::with_opts(Opts::new(
"api_auth_requests_failure",
"Failed authentication requests",
))?;
registry.register(Box::new(auth_requests_failure.clone()))?;
// === Layer-Specific Latencies (microseconds) ===
// Buckets: 0.1, 0.5, 1, 2, 5, 10, 20, 50, 100 microseconds
let latency_buckets = vec![0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0];
let jwt_extraction_duration_us = Histogram::with_opts(
HistogramOpts::new(
"api_jwt_extraction_duration_microseconds",
"JWT extraction and parsing latency in microseconds",
)
.buckets(latency_buckets.clone()),
)?;
registry.register(Box::new(jwt_extraction_duration_us.clone()))?;
let jwt_validation_duration_us = Histogram::with_opts(
HistogramOpts::new(
"api_jwt_validation_duration_microseconds",
"JWT signature validation latency in microseconds",
)
.buckets(latency_buckets.clone()),
)?;
registry.register(Box::new(jwt_validation_duration_us.clone()))?;
let revocation_check_duration_us = Histogram::with_opts(
HistogramOpts::new(
"api_revocation_check_duration_microseconds",
"JWT revocation check latency in microseconds (Redis)",
)
.buckets(latency_buckets),
)?;
registry.register(Box::new(revocation_check_duration_us.clone()))?;
let rbac_check_duration_us = Histogram::with_opts(
HistogramOpts::new(
"api_rbac_check_duration_microseconds",
"RBAC permission check latency in microseconds",
)
.buckets(vec![0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0]), // Sub-microsecond buckets
)?;
registry.register(Box::new(rbac_check_duration_us.clone()))?;
let rate_limit_check_duration_us = Histogram::with_opts(
HistogramOpts::new(
"api_rate_limit_check_duration_microseconds",
"Rate limit check latency in microseconds",
)
.buckets(vec![0.01, 0.05, 0.1, 0.2, 0.5, 1.0]), // Ultra-fast atomic operations
)?;
registry.register(Box::new(rate_limit_check_duration_us.clone()))?;
let mfa_verification_duration_us = Histogram::with_opts(
HistogramOpts::new(
"api_mfa_verification_duration_microseconds",
"MFA (TOTP) verification latency in microseconds",
)
.buckets(vec![10.0, 50.0, 100.0, 500.0, 1000.0]), // TOTP is slower
)?;
registry.register(Box::new(mfa_verification_duration_us.clone()))?;
let auth_total_duration_us = Histogram::with_opts(
HistogramOpts::new(
"api_auth_total_duration_microseconds",
"Total authentication pipeline latency in microseconds",
)
.buckets(vec![1.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0]),
)?;
registry.register(Box::new(auth_total_duration_us.clone()))?;
// === Error Counters ===
let auth_errors_missing_jwt = Counter::with_opts(Opts::new(
"api_auth_errors_missing_jwt",
"Missing Authorization header errors",
))?;
registry.register(Box::new(auth_errors_missing_jwt.clone()))?;
let auth_errors_invalid_jwt = Counter::with_opts(Opts::new(
"api_auth_errors_invalid_jwt",
"Malformed JWT token errors",
))?;
registry.register(Box::new(auth_errors_invalid_jwt.clone()))?;
let auth_errors_signature_failed = Counter::with_opts(Opts::new(
"api_auth_errors_signature_failed",
"JWT signature verification failures",
))?;
registry.register(Box::new(auth_errors_signature_failed.clone()))?;
let auth_errors_expired_jwt = Counter::with_opts(Opts::new(
"api_auth_errors_expired_jwt",
"Expired JWT token errors",
))?;
registry.register(Box::new(auth_errors_expired_jwt.clone()))?;
let auth_errors_revoked_jwt = Counter::with_opts(Opts::new(
"api_auth_errors_revoked_jwt",
"Revoked JWT token errors (blacklisted)",
))?;
registry.register(Box::new(auth_errors_revoked_jwt.clone()))?;
let auth_errors_permission_denied = Counter::with_opts(Opts::new(
"api_auth_errors_permission_denied",
"RBAC permission denied errors",
))?;
registry.register(Box::new(auth_errors_permission_denied.clone()))?;
let auth_errors_rate_limited = Counter::with_opts(Opts::new(
"api_auth_errors_rate_limited",
"Rate limit exceeded errors",
))?;
registry.register(Box::new(auth_errors_rate_limited.clone()))?;
let auth_errors_mfa_failed = Counter::with_opts(Opts::new(
"api_auth_errors_mfa_failed",
"MFA verification failures",
))?;
registry.register(Box::new(auth_errors_mfa_failed.clone()))?;
let auth_errors_redis_failure = Counter::with_opts(Opts::new(
"api_auth_errors_redis_failure",
"Redis connection/operation failures",
))?;
registry.register(Box::new(auth_errors_redis_failure.clone()))?;
// === Current State Gauges ===
let active_jwt_tokens = IntGauge::with_opts(Opts::new(
"api_active_jwt_tokens",
"Number of active JWT tokens (estimated)",
))?;
registry.register(Box::new(active_jwt_tokens.clone()))?;
let revoked_tokens_cached = IntGauge::with_opts(Opts::new(
"api_revoked_tokens_cached",
"Number of revoked tokens in Redis cache",
))?;
registry.register(Box::new(revoked_tokens_cached.clone()))?;
let rbac_cache_size = IntGauge::with_opts(Opts::new(
"api_rbac_cache_size",
"RBAC permission cache entries",
))?;
registry.register(Box::new(rbac_cache_size.clone()))?;
let rate_limiter_entries = IntGauge::with_opts(Opts::new(
"api_rate_limiter_entries",
"Rate limiter tracked users",
))?;
registry.register(Box::new(rate_limiter_entries.clone()))?;
// === Per-User Metrics ===
let requests_by_user = CounterVec::new(
Opts::new("api_requests_by_user", "Requests per user"),
&["user_id"],
)?;
registry.register(Box::new(requests_by_user.clone()))?;
let auth_failures_by_user = CounterVec::new(
Opts::new(
"api_auth_failures_by_user",
"Auth failures per user",
),
&["user_id", "reason"],
)?;
registry.register(Box::new(auth_failures_by_user.clone()))?;
let rate_limits_by_user = CounterVec::new(
Opts::new(
"api_rate_limits_by_user",
"Rate limit hits per user",
),
&["user_id"],
)?;
registry.register(Box::new(rate_limits_by_user.clone()))?;
// === Cache Performance ===
let jwt_cache_hits = Counter::with_opts(Opts::new(
"api_jwt_cache_hits",
"JWT decoding key cache hits",
))?;
registry.register(Box::new(jwt_cache_hits.clone()))?;
let jwt_cache_misses = Counter::with_opts(Opts::new(
"api_jwt_cache_misses",
"JWT decoding key cache misses",
))?;
registry.register(Box::new(jwt_cache_misses.clone()))?;
let rbac_cache_hits = Counter::with_opts(Opts::new(
"api_rbac_cache_hits",
"RBAC permission cache hits",
))?;
registry.register(Box::new(rbac_cache_hits.clone()))?;
let rbac_cache_misses = Counter::with_opts(Opts::new(
"api_rbac_cache_misses",
"RBAC permission cache misses",
))?;
registry.register(Box::new(rbac_cache_misses.clone()))?;
// === Performance SLA ===
let auth_sla_met = Counter::with_opts(Opts::new(
"api_auth_sla_met",
"Auth requests meeting <10μs SLA",
))?;
registry.register(Box::new(auth_sla_met.clone()))?;
let auth_sla_exceeded = Counter::with_opts(Opts::new(
"api_auth_sla_exceeded",
"Auth requests exceeding <10μs SLA",
))?;
registry.register(Box::new(auth_sla_exceeded.clone()))?;
Ok(Self {
auth_requests_total,
auth_requests_success,
auth_requests_failure,
jwt_extraction_duration_us,
jwt_validation_duration_us,
revocation_check_duration_us,
rbac_check_duration_us,
rate_limit_check_duration_us,
mfa_verification_duration_us,
auth_total_duration_us,
auth_errors_missing_jwt,
auth_errors_invalid_jwt,
auth_errors_signature_failed,
auth_errors_expired_jwt,
auth_errors_revoked_jwt,
auth_errors_permission_denied,
auth_errors_rate_limited,
auth_errors_mfa_failed,
auth_errors_redis_failure,
active_jwt_tokens,
revoked_tokens_cached,
rbac_cache_size,
rate_limiter_entries,
requests_by_user,
auth_failures_by_user,
rate_limits_by_user,
jwt_cache_hits,
jwt_cache_misses,
rbac_cache_hits,
rbac_cache_misses,
auth_sla_met,
auth_sla_exceeded,
})
}
/// Record successful authentication with latency breakdown
pub fn record_success(&self, total_duration_us: f64) {
self.auth_requests_total.inc();
self.auth_requests_success.inc();
self.auth_total_duration_us.observe(total_duration_us);
// Check SLA: <10μs target
if total_duration_us < 10.0 {
self.auth_sla_met.inc();
} else {
self.auth_sla_exceeded.inc();
}
}
/// Record authentication failure with reason
pub fn record_failure(&self, reason: &str, user_id: Option<&str>) {
self.auth_requests_total.inc();
self.auth_requests_failure.inc();
// Increment specific error counter
match reason {
"missing_jwt" => self.auth_errors_missing_jwt.inc(),
"invalid_jwt" => self.auth_errors_invalid_jwt.inc(),
"signature_failed" => self.auth_errors_signature_failed.inc(),
"expired_jwt" => self.auth_errors_expired_jwt.inc(),
"revoked_jwt" => self.auth_errors_revoked_jwt.inc(),
"permission_denied" => self.auth_errors_permission_denied.inc(),
"rate_limited" => self.auth_errors_rate_limited.inc(),
"mfa_failed" => self.auth_errors_mfa_failed.inc(),
"redis_failure" => self.auth_errors_redis_failure.inc(),
_ => {}, // Unknown error type
}
// Track per-user failures
if let Some(uid) = user_id {
self.auth_failures_by_user
.with_label_values(&[uid, reason])
.inc();
}
}
/// Record per-user request
pub fn record_user_request(&self, user_id: &str) {
self.requests_by_user.with_label_values(&[user_id]).inc();
}
/// Record rate limit hit for user
pub fn record_rate_limit(&self, user_id: &str) {
self.rate_limits_by_user.with_label_values(&[user_id]).inc();
}
}