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>
413 lines
15 KiB
Rust
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();
|
|
}
|
|
}
|