Skip to main content

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 🔴 REQUIRED​

FieldValue
ADR NumberADR-005
TitleAuthentication & Authorization - Technical
StatusUpdated for StatefulSets
Date Created2025-08-31
Last Modified2025-09-03
Version3.0.0
Dependenciesjsonwebtoken 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;

↑ Back to Top

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())
}
}

↑ Back to Top

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)
}
}

↑ Back to Top

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())
}
}

↑ Back to Top

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))
}
}

↑ Back to Top

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 })
}
}

↑ Back to Top

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?;

↑ Back to Top

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()
}
}))
}
}

↑ Back to Top

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"));
}
}

↑ Back to Top

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: "..."

↑ Back to Top

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

↑ Back to Top

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()
)
})
}

↑ Back to Top

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(())
}

↑ Back to Top

14. Operational Runbooks 🔴 REQUIRED​

# Emergency Procedures

## Rotate JWT Keys
1. Generate new key pair:
```bash
coditect-auth generate-keys --output /tmp/new-keys
  1. Update secrets:

    kubectl create secret generic auth-secrets-new --from-file=/tmp/new-keys
  2. Rolling restart:

    kubectl set env deployment/auth-service JWT_KEY_version=v2
    kubectl rollout status deployment/auth-service

Revoke All Tokens​

  1. Increment key version in Redis:

    redis-cli SET jwt:min_version $(date +%s)
  2. 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"

↑ Back to Top

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