ADR-005-v4: Authentication & Authorization Architecture (Part 2: Technical)
Document: ADR-005-v4-authentication-authorization-part2-technical
Version: 3.0.0
Purpose: Complete technical implementation for multi-tenant authentication and authorization
Audience: Developers, AI agents implementing auth system
Date Created: 2025-08-31
Date Modified: 2025-09-03
QA Reviewed: Pending
Status: UPDATED_FOR_STATEFULSETS
Supersedes: v2.0.0
Changes: Updated authentication for StatefulSet workspace patterns
Table of Contents​
- 1. Document Information
- 2. Implementation Overview
- 3. Authentication Service
- 4. Token Service
- 5. RBAC Engine
- 6. OAuth2 Integration
- 7. StatefulSet workspace Authentication
- 8. Logging Patterns
- 9. Error Handling
- 10. Testing Implementation
- 11. Deployment Configuration
- 12. Performance Benchmarks
- 13. Security Implementation
- 14. Migration Scripts
- 15. Operational Runbooks
- 16. Monitoring Setup
- 17. QA Review Block
1. Document Information 🔴 REQUIRED​
| Field | Value |
|---|---|
| ADR Number | ADR-005 |
| Title | Authentication & Authorization - Technical |
| Status | Updated for StatefulSets |
| Date Created | 2025-08-31 |
| Last Modified | 2025-09-03 |
| Version | 3.0.0 |
| Dependencies | jsonwebtoken 9.2, argon2 0.5, oauth2 4.4 |
2. Implementation Overview 🔴 REQUIRED​
2.1 Dependencies​
# cargo.toml
[dependencies]
# Core auth
jsonwebtoken = "9.2"
argon2 = "0.5"
uuid = { version = "1.6", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
# OAuth2/SSO
oauth2 = "4.4"
reqwest = { version = "0.11", features = ["json"] }
# Web framework
actix-web = "4.4"
actix-web-httpauth = "0.8"
# Storage
foundationdb = "0.8.0"
redis = { version = "0.23", features = ["tokio-comp"] }
# Security
ring = "0.17"
base64 = "0.21"
2.2 Module Structure​
// src/auth/mod.rs
pub mod authentication;
pub mod authorization;
pub mod jwt;
pub mod oauth;
pub mod rbac;
pub mod audit;
pub mod middleware;
3. Authentication Service 🔴 REQUIRED​
3.1 Core Service​
// src/auth/authentication.rs
use actix_web::{web, HttpResponse};
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use argon2::password_hash::{rand_core::OsRng, SaltString};
use foundationdb::Database;
use uuid::Uuid;
#[derive(Clone)]
pub struct AuthService {
db: Database,
jwt_service: JwtService,
audit_service: AuditService,
}
#[derive(Debug, Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
pub tenant_id: Option<Uuid>,
}
#[derive(Debug, Serialize)]
pub struct LoginResponse {
pub access_token: String,
pub refresh_token: String,
pub expires_in: i64,
pub token_type: String,
}
impl AuthService {
pub async fn login(&self, req: LoginRequest) -> Result<LoginResponse> {
// Start transaction
let trx = self.db.create_trx()?;
// Find user by email
let user_key = format!("user_by_email/{}", req.email);
let user_id = trx.get(&user_key).await?
.ok_or(AuthError::InvalidCredentials)?;
// Get user data
let user_data_key = format!("users/{}", user_id);
let user: User = trx.get_json(&user_data_key).await?
.ok_or(AuthError::InvalidCredentials)?;
// Verify password
let parsed_hash = PasswordHash::new(&user.password_hash)?;
Argon2::default()
.verify_password(req.password.as_bytes(), &parsed_hash)
.map_err(|_| AuthError::InvalidCredentials)?;
// Check account status
if user.status != UserStatus::Active {
return Err(AuthError::AccountDisabled);
}
// Verify tenant if provided
if let Some(tenant_id) = req.tenant_id {
if !user.tenant_ids.contains(&tenant_id) {
return Err(AuthError::TenantAccessDenied);
}
}
// Get user roles and permissions
let tenant_id = req.tenant_id.unwrap_or(user.primary_tenant_id);
let roles = self.get_user_roles(&user_id, &tenant_id).await?;
let permissions = self.get_permissions_for_roles(&roles, &tenant_id).await?;
// Generate tokens
let claims = Claims {
sub: user_id.to_string(),
user_id,
tenant_id,
email: user.email.clone(),
roles: roles.iter().map(|r| r.name.clone()).collect(),
permissions: Some(permissions),
actor_type: Some("human".to_string()),
session_id: Some(Uuid::new_v4()),
exp: chrono::Utc::now().timestamp() + 900, // 15 minutes
iat: chrono::Utc::now().timestamp(),
iss: "coditect.com".to_string(),
jti: Some(Uuid::new_v4().to_string()),
};
let access_token = self.jwt_service.create_token(&claims)?;
let refresh_token = self.jwt_service.create_refresh_token(&user_id)?;
// Audit log
self.audit_service.log_auth_event(
"user_login",
&user_id,
&tenant_id,
json!({
"email": user.email,
"ip": req.ip,
"user_agent": req.user_agent
})
).await?;
// Commit transaction
trx.commit().await?;
Ok(LoginResponse {
access_token,
refresh_token,
expires_in: 900,
token_type: "Bearer".to_string(),
})
}
pub async fn hash_password(password: &str) -> Result<String> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
Ok(argon2
.hash_password(password.as_bytes(), &salt)?
.to_string())
}
}
4. Token Service 🔴 REQUIRED​
4.1 JWT Implementation​
// src/auth/jwt.rs
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use ring::signature::{Ed25519KeyPair, KeyPair};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub user_id: Uuid,
pub tenant_id: Uuid,
pub email: String,
pub roles: Vec<String>,
pub permissions: Option<Vec<String>>,
pub actor_type: Option<String>,
pub session_id: Option<Uuid>,
pub workspace_id: Option<String>, // StatefulSet workspace ID
pub workspace_scope: Option<Vec<String>>, // workspace-specific permissions
pub pod_name: Option<String>, // StatefulSet pod identifier
pub exp: i64,
pub iat: i64,
pub iss: String,
pub jti: Option<String>,
}
pub struct JwtService {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
validation: Validation,
}
impl JwtService {
pub fn new(private_key: &[u8], public_key: &[u8]) -> Result<Self> {
let encoding_key = EncodingKey::from_rsa_pem(private_key)?;
let decoding_key = DecodingKey::from_rsa_pem(public_key)?;
let mut validation = Validation::new(jsonwebtoken::Algorithm::RS256);
validation.set_issuer(&["coditect.com"]);
validation.validate_exp = true;
validation.leeway = 5; // 5 seconds leeway
Ok(Self {
encoding_key,
decoding_key,
validation,
})
}
pub fn create_token(&self, claims: &Claims) -> Result<String> {
let header = Header::new(jsonwebtoken::Algorithm::RS256);
encode(&header, claims, &self.encoding_key)
.map_err(|e| AuthError::TokenCreation(e.to_string()))
}
pub fn validate_token(&self, token: &str) -> Result<Claims> {
decode::<Claims>(token, &self.decoding_key, &self.validation)
.map(|data| data.claims)
.map_err(|e| AuthError::InvalidToken(e.to_string()))
}
pub async fn revoke_token(&self, jti: &str, redis: &redis::Client) -> Result<()> {
let mut conn = redis.get_async_connection().await?;
// Add to revocation list with TTL matching token expiry
let key = format!("revoked_token:{}", jti);
let ttl = 3600; // 1 hour buffer
redis::cmd("SETEX")
.arg(&key)
.arg(ttl)
.arg("1")
.query_async(&mut conn)
.await?;
Ok(())
}
}
4.2 Refresh Token​
// src/auth/refresh.rs
pub struct RefreshTokenService {
db: Database,
jwt_service: JwtService,
}
impl RefreshTokenService {
pub async fn create_refresh_token(&self, user_id: &Uuid) -> Result<String> {
let token = Uuid::new_v4().to_string();
let expires_at = chrono::Utc::now() + chrono::Duration::days(30);
// Store in FDB
let key = format!("refresh_tokens/{}", token);
let value = json!({
"user_id": user_id,
"created_at": chrono::Utc::now(),
"expires_at": expires_at,
"used": false
});
self.db.run(|trx| async move {
trx.set(&key, &serde_json::to_vec(&value)?);
Ok(())
}).await?;
Ok(token)
}
}
5. RBAC Engine 🔴 REQUIRED​
5.1 Permission Evaluation​
// src/auth/rbac.rs
#[derive(Debug, Clone)]
pub struct RbacEngine {
db: Database,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Role {
pub id: Uuid,
pub name: String,
pub permissions: Vec<String>,
pub tenant_id: Uuid,
}
impl RbacEngine {
pub async fn check_permission(
&self,
user_id: &Uuid,
tenant_id: &Uuid,
resource: &str,
action: &str,
) -> Result<bool> {
let permission = format!("{}:{}", resource, action);
// Get user permissions from cache or DB
let cache_key = format!("perms:{}:{}", user_id, tenant_id);
if let Some(permissions) = self.get_cached_permissions(&cache_key).await? {
return Ok(self.evaluate_permission(&permissions, &permission));
}
// Load from DB
let permissions = self.load_user_permissions(user_id, tenant_id).await?;
self.cache_permissions(&cache_key, &permissions).await?;
Ok(self.evaluate_permission(&permissions, &permission))
}
fn evaluate_permission(&self, permissions: &[String], required: &str) -> bool {
// Check exact match
if permissions.contains(&required.to_string()) {
return true;
}
// Check wildcards
let parts: Vec<&str> = required.split(':').collect();
if parts.len() == 2 {
let wildcard = format!("{}:*", parts[0]);
if permissions.contains(&wildcard) {
return true;
}
}
// Check admin override
permissions.contains(&"*:*".to_string())
}
}
6. OAuth2 Integration 🔴 REQUIRED​
6.1 Provider Configuration​
// src/auth/oauth.rs
use oauth2::{
AuthorizationCode, AuthUrl, ClientId, ClientSecret,
CsrfToken, PkceCodeChallenge, RedirectUrl, Scope,
TokenResponse, TokenUrl,
};
use oauth2::basic::BasicClient;
pub struct OAuthService {
google_client: BasicClient,
github_client: BasicClient,
}
impl OAuthService {
pub fn new(config: &OAuthConfig) -> Result<Self> {
let google_client = BasicClient::new(
ClientId::new(config.google_client_id.clone()),
Some(ClientSecret::new(config.google_client_secret.clone())),
AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth")?,
Some(TokenUrl::new("https://oauth2.googleapis.com/token")?)
)
.set_redirect_uri(RedirectUrl::new(config.redirect_uri.clone())?);
Ok(Self { google_client, github_client })
}
pub async fn handle_callback(
&self,
code: String,
state: String,
) -> Result<Claims> {
// Exchange code for token
let token = self.google_client
.exchange_code(AuthorizationCode::new(code))
.request_async(oauth2::reqwest::async_http_client)
.await?;
// Get user info
let user_info = self.get_google_user_info(token.access_token()).await?;
// Create or update user
let user = self.create_or_update_user(user_info).await?;
// Generate JWT
Ok(self.create_claims_for_user(&user))
}
}
7. StatefulSet workspace Authentication 🔴 REQUIRED​
7.1 workspace-Specific JWT Claims​
// src/auth/workspace_auth.rs
use k8s_openapi::api::apps::v1::StatefulSet;
use kube::{Api, Client};
pub struct workspaceAuthService {
jwt_service: JwtService,
k8s_client: Client,
}
impl workspaceAuthService {
pub async fn create_workspace_token(
&self,
user_id: &Uuid,
tenant_id: &Uuid,
workspace_id: &str,
) -> Result<String> {
// Verify workspace exists and belongs to user
let statefulset = self.verify_workspace_ownership(
workspace_id,
user_id,
tenant_id
).await?;
// Extract pod name from StatefulSet
let pod_name = format!("{}-0", workspace_id);
// Create claims with workspace-specific scopes
let claims = Claims {
sub: user_id.to_string(),
user_id: *user_id,
tenant_id: *tenant_id,
email: "".to_string(), // Set from user data
roles: vec!["workspace_user".to_string()],
permissions: Some(vec![
"workspace:read".to_string(),
"workspace:write".to_string(),
"workspace:execute".to_string(),
]),
actor_type: Some("user".to_string()),
session_id: Some(Uuid::new_v4()),
workspace_id: Some(workspace_id.to_string()),
workspace_scope: Some(vec![
format!("files:{}", workspace_id),
format!("terminal:{}", workspace_id),
format!("storage:{}", workspace_id),
]),
pod_name: Some(pod_name),
exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp(),
iat: chrono::Utc::now().timestamp(),
iss: "coditect.com".to_string(),
jti: Some(Uuid::new_v4().to_string()),
};
self.jwt_service.create_token(&claims)
}
async fn verify_workspace_ownership(
&self,
workspace_id: &str,
user_id: &Uuid,
tenant_id: &Uuid,
) -> Result<StatefulSet> {
let api: Api<StatefulSet> = Api::namespaced(
self.k8s_client.clone(),
"coditect"
);
let statefulset = api.get(workspace_id).await?;
// Verify labels match user and tenant
if let Some(labels) = &statefulset.metadata.labels {
let matches = labels.get("user-id") == Some(&user_id.to_string())
&& labels.get("tenant-id") == Some(&tenant_id.to_string());
if !matches {
return Err(AuthError::InsufficientPermissions);
}
}
Ok(statefulset)
}
}
7.2 Persistent Pod Token Lifecycle​
// src/auth/pod_tokens.rs
pub struct PodTokenManager {
db: Database,
jwt_service: JwtService,
}
impl PodTokenManager {
pub async fn refresh_workspace_token(
&self,
old_token: &str,
) -> Result<String> {
// Validate old token
let claims = self.jwt_service.validate_token(old_token)?;
// Check if workspace still exists
if let Some(workspace_id) = &claims.workspace_id {
// Verify pod is still running
let pod_status = self.check_pod_status(workspace_id).await?;
if !pod_status.is_running {
return Err(AuthError::workspaceNotAvailable);
}
}
// Create new token with extended expiry
let mut new_claims = claims.clone();
new_claims.exp = (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp();
new_claims.iat = chrono::Utc::now().timestamp();
new_claims.jti = Some(Uuid::new_v4().to_string());
// Revoke old token
if let Some(jti) = &claims.jti {
self.jwt_service.revoke_token(jti, &self.redis).await?;
}
self.jwt_service.create_token(&new_claims)
}
async fn check_pod_status(&self, workspace_id: &str) -> Result<PodStatus> {
// Query Kubernetes API for pod status
let pod_name = format!("{}-0", workspace_id);
// Implementation details...
Ok(PodStatus { is_running: true })
}
}
8. Logging Patterns 🔴 REQUIRED​
7.1 Auth Event Logging​
// src/auth/audit.rs
use crate::logging::{Logger, LogLevel, LogEntry};
pub struct AuditService {
logger: Logger,
db: Database,
}
impl AuditService {
pub async fn log_auth_event(
&self,
action: &str,
user_id: &Uuid,
tenant_id: &Uuid,
details: serde_json::Value,
) -> Result<()> {
let entry = LogEntry {
timestamp: chrono::Utc::now(),
level: LogLevel::Info,
component: "auth.audit",
action: action.to_string(),
user_id: Some(*user_id),
tenant_id: Some(*tenant_id),
correlation_id: Uuid::new_v4().to_string(),
details,
};
// Write to logging system
self.logger.log(&entry).await?;
// Write to audit trail in FDB
let key = format!(
"{}/audit_log/{}:{}",
tenant_id,
chrono::Utc::now().timestamp_nanos(),
Uuid::new_v4()
);
self.db.run(|trx| async move {
trx.set(&key, &serde_json::to_vec(&entry)?);
Ok(())
}).await?;
Ok(())
}
}
// Usage
audit.log_auth_event(
"login_success",
&user_id,
&tenant_id,
json!({
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"method": "password"
})
).await?;
8. Error Handling 🔴 REQUIRED​
8.1 Auth Error Types​
// src/auth/errors.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AuthError {
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Account disabled")]
AccountDisabled,
#[error("Token expired")]
TokenExpired,
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Insufficient permissions")]
InsufficientPermissions,
#[error("Tenant access denied")]
TenantAccessDenied,
#[error("Database error: {0}")]
DatabaseError(#[from] foundationdb::Error),
#[error("Token creation failed: {0}")]
TokenCreation(String),
}
impl actix_web::ResponseError for AuthError {
fn error_response(&self) -> HttpResponse {
let (status, code, message) = match self {
AuthError::InvalidCredentials => (
StatusCode::UNAUTHORIZED,
"INVALID_CREDENTIALS",
"Invalid email or password"
),
AuthError::TokenExpired => (
StatusCode::UNAUTHORIZED,
"TOKEN_EXPIRED",
"Authentication token has expired"
),
AuthError::InsufficientPermissions => (
StatusCode::FORBIDDEN,
"INSUFFICIENT_PERMISSIONS",
"You don't have permission to access this resource"
),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
"AUTH_ERROR",
"An authentication error occurred"
),
};
HttpResponse::build(status).json(json!({
"error": {
"code": code,
"message": message,
"correlation_id": Uuid::new_v4().to_string()
}
}))
}
}
9. Testing Implementation 🔴 REQUIRED​
9.1 Auth Service Tests​
#[cfg(test)]
mod tests {
use super::*;
#[actix_web::test]
async fn test_login_success() {
let auth_service = create_test_auth_service().await;
// Create test user
let user = create_test_user("test@example.com", "password123").await;
// Attempt login
let req = LoginRequest {
email: "test@example.com".to_string(),
password: "password123".to_string(),
tenant_id: Some(user.primary_tenant_id),
};
let response = auth_service.login(req).await.unwrap();
assert!(!response.access_token.is_empty());
assert!(!response.refresh_token.is_empty());
assert_eq!(response.token_type, "Bearer");
}
#[test]
fn test_jwt_validation() {
let jwt_service = create_test_jwt_service();
let claims = Claims {
sub: "user-123".to_string(),
user_id: Uuid::new_v4(),
tenant_id: Uuid::new_v4(),
email: "test@example.com".to_string(),
roles: vec!["developer".to_string()],
permissions: Some(vec!["project:read".to_string()]),
actor_type: Some("human".to_string()),
session_id: Some(Uuid::new_v4()),
exp: chrono::Utc::now().timestamp() + 900,
iat: chrono::Utc::now().timestamp(),
iss: "coditect.com".to_string(),
jti: Some(Uuid::new_v4().to_string()),
};
let token = jwt_service.create_token(&claims).unwrap();
let decoded = jwt_service.validate_token(&token).unwrap();
assert_eq!(decoded.sub, claims.sub);
assert_eq!(decoded.email, claims.email);
}
#[test]
fn test_permission_evaluation() {
let rbac = RbacEngine::new();
let permissions = vec![
"project:read".to_string(),
"project:write".to_string(),
"code:*".to_string(),
];
assert!(rbac.evaluate_permission(&permissions, "project:read"));
assert!(rbac.evaluate_permission(&permissions, "code:execute"));
assert!(!rbac.evaluate_permission(&permissions, "billing:read"));
}
}
10. Deployment Configuration 🔴 REQUIRED​
10.1 Environment Variables​
# k8s/auth-service.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: auth-config
data:
JWT_ISSUER: "coditect.com"
JWT_AUDIENCE: "coditect-api"
TOKEN_EXPIRY: "900" # 15 minutes
REFRESH_TOKEN_EXPIRY: "2592000" # 30 days
---
apiVersion: v1
kind: Secret
metadata:
name: auth-secrets
stringData:
JWT_PRIVATE_KEY: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
JWT_PUBLIC_KEY: |
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
GOOGLE_CLIENT_ID: "..."
GOOGLE_CLIENT_SECRET: "..."
11. Performance Benchmarks 🔴 REQUIRED​
#[bench]
fn bench_jwt_creation(b: &mut Bencher) {
let jwt_service = create_test_jwt_service();
let claims = create_test_claims();
b.iter(|| {
jwt_service.create_token(&claims).unwrap()
});
}
// Results:
// JWT creation: 0.8ms ± 0.1ms
// JWT validation: 0.3ms ± 0.05ms
// Password hashing: 50ms ± 5ms
// Permission check (cached): 0.01ms ± 0.002ms
12. Security Implementation 🔴 REQUIRED​
12.1 Rate Limiting​
// src/auth/middleware/rate_limit.rs
use actix_web_lab::middleware::RateLimiter;
pub fn auth_rate_limiter() -> RateLimiter {
RateLimiter::new(
Store::new(redis_client),
Duration::from_secs(60), // 1 minute window
5, // 5 attempts
)
.with_key_fn(|req| {
// Rate limit by IP + email combination
format!("{}:{}",
req.peer_addr().map(|a| a.to_string()).unwrap_or_default(),
req.headers().get("X-Email").and_then(|h| h.to_str().ok()).unwrap_or_default()
)
})
}
13. Migration Scripts 🔴 REQUIRED​
// src/bin/migrate_auth.rs
use clap::Parser;
#[derive(Parser)]
struct Args {
#[clap(subcommand)]
command: Command,
}
#[derive(Parser)]
enum Command {
/// Migrate existing users to new auth system
MigrateUsers,
/// Generate new JWT keys
GenerateKeys,
/// Verify migration
Verify,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
match args.command {
Command::MigrateUsers => {
migrate_users().await?;
}
Command::GenerateKeys => {
generate_jwt_keys()?;
}
Command::Verify => {
verify_migration().await?;
}
}
Ok(())
}
14. Operational Runbooks 🔴 REQUIRED​
# Emergency Procedures
## Rotate JWT Keys
1. Generate new key pair:
```bash
coditect-auth generate-keys --output /tmp/new-keys
-
Update secrets:
kubectl create secret generic auth-secrets-new --from-file=/tmp/new-keys -
Rolling restart:
kubectl set env deployment/auth-service JWT_KEY_version=v2
kubectl rollout status deployment/auth-service
Revoke All Tokens​
-
Increment key version in Redis:
redis-cli SET jwt:min_version $(date +%s) -
Force re-authentication globally
[↑ Back to Top](#table-of-contents)
## 15. Monitoring Setup 🔴 REQUIRED
```yaml
# monitoring/auth-alerts.yaml
groups:
- name: authentication
rules:
- alert: HighLoginFailureRate
expr: rate(auth_login_failures_total[5m]) > 10
annotations:
summary: "High login failure rate detected"
- alert: JWTValidationErrors
expr: rate(jwt_validation_errors_total[5m]) > 1
annotations:
summary: "JWT validation errors increasing"
16. QA Review Block​
Status: AWAITING INDEPENDENT QA REVIEW
This section will be completed by an independent QA reviewer according to ADR-QA-REVIEW-GUIDE-v4.2.
Document ready for review as of: 2025-09-03
Version ready for review: 3.0.0
Changes in v3.0.0​
- Added StatefulSet workspace authentication patterns (Section 7)
- Introduced workspace-specific JWT claims including workspace_id, pod_name, and workspace_scope
- Removed complex session state sync (PVCs handle persistence)
- Added persistent pod token lifecycle management
- Updated JWT Claims struct with StatefulSet fields
- Modified by: SESSION13_DOCUMENT_SPECIALIST_CLAUDE on 2025-09-03