use crate::errors::{AuthError, Result};
use crate::server::oidc::oidc_enhanced_ciba::EnhancedCibaManager;
use crate::server::oidc::oidc_session_management::SessionManager;
use chrono::{DateTime, Duration, Timelike, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct StepUpConfig {
pub supported_levels: Vec<AuthenticationLevel>,
pub default_token_lifetime: Duration,
pub max_authentication_level: AuthenticationLevel,
pub evaluation_rules: Vec<StepUpRule>,
pub level_methods: HashMap<AuthenticationLevel, Vec<AuthenticationMethod>>,
pub enable_risk_based_stepup: bool,
pub enable_location_based_stepup: bool,
pub enable_time_based_stepup: bool,
pub step_up_grace_period: Duration,
}
impl Default for StepUpConfig {
fn default() -> Self {
let mut level_methods = HashMap::new();
level_methods.insert(
AuthenticationLevel::Basic,
vec![AuthenticationMethod::Password, AuthenticationMethod::OAuth],
);
level_methods.insert(
AuthenticationLevel::Enhanced,
vec![
AuthenticationMethod::Password,
AuthenticationMethod::OAuth,
AuthenticationMethod::TwoFactor,
],
);
level_methods.insert(
AuthenticationLevel::High,
vec![
AuthenticationMethod::TwoFactor,
AuthenticationMethod::Biometric,
AuthenticationMethod::HardwareToken,
],
);
level_methods.insert(
AuthenticationLevel::Maximum,
vec![
AuthenticationMethod::Biometric,
AuthenticationMethod::HardwareToken,
AuthenticationMethod::CertificateBased,
],
);
Self {
supported_levels: vec![
AuthenticationLevel::Basic,
AuthenticationLevel::Enhanced,
AuthenticationLevel::High,
AuthenticationLevel::Maximum,
],
default_token_lifetime: Duration::minutes(30),
max_authentication_level: AuthenticationLevel::Maximum,
evaluation_rules: vec![
StepUpRule::new(
"high_value_transaction",
StepUpTrigger::ResourceSensitivity("high".to_string()),
AuthenticationLevel::High,
),
StepUpRule::new(
"admin_operations",
StepUpTrigger::ResourceType("admin".to_string()),
AuthenticationLevel::Maximum,
),
StepUpRule::new(
"suspicious_location",
StepUpTrigger::RiskScore(0.7),
AuthenticationLevel::Enhanced,
),
],
level_methods,
enable_risk_based_stepup: true,
enable_location_based_stepup: true,
enable_time_based_stepup: true,
step_up_grace_period: Duration::minutes(5),
}
}
}
impl StepUpConfig {
pub fn builder() -> StepUpConfigBuilder {
StepUpConfigBuilder {
inner: Self::default(),
}
}
}
pub struct StepUpConfigBuilder {
inner: StepUpConfig,
}
impl StepUpConfigBuilder {
pub fn token_lifetime(mut self, lifetime: Duration) -> Self {
self.inner.default_token_lifetime = lifetime;
self
}
pub fn grace_period(mut self, period: Duration) -> Self {
self.inner.step_up_grace_period = period;
self
}
pub fn max_level(mut self, level: AuthenticationLevel) -> Self {
self.inner.max_authentication_level = level;
self
}
pub fn add_rule(mut self, rule: StepUpRule) -> Self {
self.inner.evaluation_rules.push(rule);
self
}
pub fn rules(mut self, rules: Vec<StepUpRule>) -> Self {
self.inner.evaluation_rules = rules;
self
}
pub fn level_methods(mut self, level: AuthenticationLevel, methods: Vec<AuthenticationMethod>) -> Self {
self.inner.level_methods.insert(level, methods);
self
}
pub fn enable_risk_stepup(mut self) -> Self {
self.inner.enable_risk_based_stepup = true;
self
}
pub fn disable_risk_stepup(mut self) -> Self {
self.inner.enable_risk_based_stepup = false;
self
}
pub fn enable_location_stepup(mut self) -> Self {
self.inner.enable_location_based_stepup = true;
self
}
pub fn disable_location_stepup(mut self) -> Self {
self.inner.enable_location_based_stepup = false;
self
}
pub fn enable_time_stepup(mut self) -> Self {
self.inner.enable_time_based_stepup = true;
self
}
pub fn disable_time_stepup(mut self) -> Self {
self.inner.enable_time_based_stepup = false;
self
}
pub fn build(self) -> StepUpConfig {
self.inner
}
}
impl StepUpContext {
pub fn new(
user_id: impl Into<String>,
resource: impl Into<String>,
session_id: impl Into<String>,
current_auth_level: AuthenticationLevel,
) -> Self {
Self {
user_id: user_id.into(),
resource: resource.into(),
resource_metadata: HashMap::new(),
risk_score: None,
location: None,
session_id: session_id.into(),
current_auth_level,
auth_time: Utc::now(),
custom_attributes: HashMap::new(),
}
}
pub fn with_risk_score(mut self, score: f64) -> Self {
self.risk_score = Some(score);
self
}
pub fn with_location(mut self, location: LocationInfo) -> Self {
self.location = Some(location);
self
}
pub fn with_auth_time(mut self, auth_time: DateTime<Utc>) -> Self {
self.auth_time = auth_time;
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.resource_metadata.insert(key.into(), value);
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.custom_attributes.insert(key.into(), value);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum AuthenticationLevel {
Basic = 1,
Enhanced = 2,
High = 3,
Maximum = 4,
}
impl AuthenticationLevel {
pub fn meets_requirement(&self, required: AuthenticationLevel) -> bool {
*self >= required
}
pub fn next_level(&self) -> Option<AuthenticationLevel> {
match self {
AuthenticationLevel::Basic => Some(AuthenticationLevel::Enhanced),
AuthenticationLevel::Enhanced => Some(AuthenticationLevel::High),
AuthenticationLevel::High => Some(AuthenticationLevel::Maximum),
AuthenticationLevel::Maximum => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AuthenticationMethod {
Password,
OAuth,
TwoFactor,
Biometric,
HardwareToken,
CertificateBased,
Fido2,
}
#[derive(Debug, Clone)]
pub struct StepUpRule {
pub rule_id: String,
pub trigger: StepUpTrigger,
pub required_level: AuthenticationLevel,
pub priority: u32,
pub active: bool,
}
impl StepUpRule {
pub fn new(rule_id: &str, trigger: StepUpTrigger, required_level: AuthenticationLevel) -> Self {
Self {
rule_id: rule_id.to_string(),
trigger,
required_level,
priority: 100,
active: true,
}
}
}
#[derive(Debug, Clone)]
pub enum StepUpTrigger {
ResourceSensitivity(String),
ResourceType(String),
RiskScore(f64),
LocationChange,
TimeBasedAccess,
Custom(String, serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepUpContext {
pub user_id: String,
pub resource: String,
pub resource_metadata: HashMap<String, serde_json::Value>,
pub risk_score: Option<f64>,
pub location: Option<LocationInfo>,
pub session_id: String,
pub current_auth_level: AuthenticationLevel,
pub auth_time: DateTime<Utc>,
pub custom_attributes: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationInfo {
pub ip_address: String,
pub geolocation: Option<GeoLocation>,
pub location_risk: Option<f64>,
pub is_known_location: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoLocation {
pub latitude: f64,
pub longitude: f64,
pub country: Option<String>,
pub city: Option<String>,
}
#[derive(Debug, Clone)]
pub struct StepUpEvaluationResult {
pub required: bool,
pub target_level: AuthenticationLevel,
pub allowed_methods: Vec<AuthenticationMethod>,
pub matching_rules: Vec<String>,
pub reason: String,
pub grace_period_expires: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepUpRequest {
pub request_id: String,
pub user_id: String,
pub current_level: AuthenticationLevel,
pub target_level: AuthenticationLevel,
pub allowed_methods: Vec<AuthenticationMethod>,
pub reason: String,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub status: StepUpStatus,
pub resource: String,
pub challenge_data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepUpResponse {
pub request_id: String,
pub success: bool,
pub achieved_level: AuthenticationLevel,
pub method_used: Option<AuthenticationMethod>,
pub session_token: Option<String>,
pub expires_at: Option<DateTime<Utc>>,
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum StepUpStatus {
Pending,
InProgress,
Completed,
Failed,
Expired,
Cancelled,
}
#[derive(Debug)]
pub struct SteppedUpAuthManager {
config: StepUpConfig,
active_requests: Arc<RwLock<HashMap<String, StepUpRequest>>>,
session_manager: Arc<SessionManager>,
ciba_manager: Option<Arc<EnhancedCibaManager>>,
}
impl SteppedUpAuthManager {
pub fn new(config: StepUpConfig) -> Self {
Self {
config,
active_requests: Arc::new(RwLock::new(HashMap::new())),
session_manager: Arc::new(SessionManager::new(Default::default())),
ciba_manager: None,
}
}
pub fn with_ciba(config: StepUpConfig, ciba_manager: Arc<EnhancedCibaManager>) -> Self {
Self {
config,
active_requests: Arc::new(RwLock::new(HashMap::new())),
session_manager: Arc::new(SessionManager::new(Default::default())),
ciba_manager: Some(ciba_manager),
}
}
pub async fn evaluate_step_up_requirement(
&self,
context: &StepUpContext,
) -> Result<StepUpEvaluationResult> {
let session_auth_level = self.get_session_auth_level(&context.session_id).await?;
let current_auth_level = if session_auth_level > context.current_auth_level {
session_auth_level
} else {
context.current_auth_level
};
let mut matching_rules = Vec::new();
let mut highest_required_level = current_auth_level;
let mut reason_parts = Vec::new();
for rule in &self.config.evaluation_rules {
if !rule.active {
continue;
}
let rule_matches = match &rule.trigger {
StepUpTrigger::ResourceSensitivity(level) => context
.resource_metadata
.get("sensitivity")
.and_then(|v| v.as_str())
.map(|s| s == level)
.unwrap_or(false),
StepUpTrigger::ResourceType(resource_type) => context
.resource_metadata
.get("type")
.and_then(|v| v.as_str())
.map(|s| s == resource_type)
.unwrap_or(false),
StepUpTrigger::RiskScore(threshold) => context
.risk_score
.map(|score| score >= *threshold)
.unwrap_or(false),
StepUpTrigger::LocationChange => context
.location
.as_ref()
.map(|loc| !loc.is_known_location)
.unwrap_or(false),
StepUpTrigger::TimeBasedAccess => {
let now = Utc::now();
let hour = now.hour();
!(9..=17).contains(&hour)
}
StepUpTrigger::Custom(key, expected_value) => context
.custom_attributes
.get(key)
.map(|value| value == expected_value)
.unwrap_or(false),
};
if rule_matches {
matching_rules.push(rule.rule_id.clone());
if rule.required_level > highest_required_level {
highest_required_level = rule.required_level;
}
reason_parts.push(format!("Rule '{}' triggered", rule.rule_id));
}
}
let required = highest_required_level > current_auth_level;
let allowed_methods = self
.config
.level_methods
.get(&highest_required_level)
.cloned()
.unwrap_or_default();
let reason = if reason_parts.is_empty() {
"No step-up required".to_string()
} else {
reason_parts.join("; ")
};
let grace_period_expires = if required {
let grace_expiry = Utc::now() + self.config.step_up_grace_period;
if let Ok(session_expiry) = self.get_session_expiry(&context.session_id).await {
Some(grace_expiry.min(session_expiry))
} else {
Some(grace_expiry)
}
} else {
None
};
Ok(StepUpEvaluationResult {
required,
target_level: highest_required_level,
allowed_methods,
matching_rules,
reason,
grace_period_expires,
})
}
pub async fn initiate_step_up(
&self,
user_id: &str,
current_level: AuthenticationLevel,
target_level: AuthenticationLevel,
resource: &str,
reason: &str,
) -> Result<StepUpRequest> {
if target_level <= current_level {
return Err(AuthError::validation(
"Target authentication level must be higher than current level".to_string(),
));
}
if !self.config.supported_levels.contains(&target_level) {
return Err(AuthError::validation(format!(
"Unsupported target authentication level: {:?}",
target_level
)));
}
let allowed_methods = self
.config
.level_methods
.get(&target_level)
.cloned()
.unwrap_or_default();
let request_id = Uuid::new_v4().to_string();
let now = Utc::now();
let expires_at = now + self.config.default_token_lifetime;
let step_up_request = StepUpRequest {
request_id: request_id.clone(),
user_id: user_id.to_string(),
current_level,
target_level,
allowed_methods,
reason: reason.to_string(),
expires_at,
created_at: now,
status: StepUpStatus::Pending,
resource: resource.to_string(),
challenge_data: None,
};
{
let mut requests = self.active_requests.write().await;
requests.insert(request_id.clone(), step_up_request.clone());
}
Ok(step_up_request)
}
pub async fn initiate_backchannel_step_up(
&self,
user_id: &str,
current_level: AuthenticationLevel,
target_level: AuthenticationLevel,
resource: &str,
reason: &str,
binding_message: Option<String>,
) -> Result<StepUpRequest> {
if target_level <= current_level {
return Err(AuthError::validation(
"Target authentication level must be higher than current level".to_string(),
));
}
let ciba_manager = self.ciba_manager.as_ref().ok_or_else(|| {
AuthError::auth_method(
"step_up",
"CIBA manager not available for backchannel step-up",
)
})?;
let allowed_methods = self
.config
.level_methods
.get(&target_level)
.cloned()
.unwrap_or_default();
let request_id = Uuid::new_v4().to_string();
let now = Utc::now();
let expires_at = now + self.config.default_token_lifetime;
use crate::server::oidc::oidc_enhanced_ciba::{
AuthenticationContext, AuthenticationMode, BackchannelAuthParams, UserIdentifierHint,
};
let auth_params = BackchannelAuthParams {
client_id: &format!("stepup_{}", request_id),
user_hint: UserIdentifierHint::LoginHint(user_id.to_string()),
binding_message: binding_message.clone(),
auth_context: Some(AuthenticationContext {
transaction_amount: None,
transaction_currency: None,
merchant_info: None,
risk_score: None,
location: None,
device_info: None,
custom_attributes: {
let mut attrs = HashMap::new();
attrs.insert("step_up_reason".to_string(), serde_json::json!(reason));
attrs.insert(
"step_up_target_level".to_string(),
serde_json::json!(target_level),
);
attrs.insert("step_up_resource".to_string(), serde_json::json!(resource));
attrs
},
}),
scopes: vec!["step_up".to_string()],
mode: AuthenticationMode::Poll, client_notification_endpoint: None,
client_notification_token: None, };
let ciba_request = ciba_manager.initiate_backchannel_auth(auth_params).await?;
let step_up_request = StepUpRequest {
request_id: request_id.clone(),
user_id: user_id.to_string(),
current_level,
target_level,
allowed_methods,
reason: reason.to_string(),
expires_at,
created_at: now,
status: StepUpStatus::Pending,
resource: resource.to_string(),
challenge_data: Some(serde_json::json!({
"ciba_auth_req_id": ciba_request.auth_req_id,
"binding_message": binding_message,
"auth_mode": "backchannel"
})),
};
{
let mut requests = self.active_requests.write().await;
requests.insert(request_id.clone(), step_up_request.clone());
}
Ok(step_up_request)
}
pub async fn complete_step_up(
&self,
request_id: &str,
method_used: AuthenticationMethod,
success: bool,
) -> Result<StepUpResponse> {
let mut requests = self.active_requests.write().await;
let request = requests
.get_mut(request_id)
.ok_or_else(|| AuthError::auth_method("step_up", "Request not found"))?;
if request.status != StepUpStatus::Pending && request.status != StepUpStatus::InProgress {
return Err(AuthError::auth_method(
"step_up",
format!("Request is not in progress: {:?}", request.status),
));
}
if Utc::now() > request.expires_at {
request.status = StepUpStatus::Expired;
return Err(AuthError::auth_method("step_up", "Request expired"));
}
let achieved_level = if success {
request.status = StepUpStatus::Completed;
if let Err(e) = self
.update_session_auth_level(&request.user_id, request.target_level)
.await
{
tracing::warn!("Failed to update session auth level: {}", e);
}
request.target_level
} else {
request.status = StepUpStatus::Failed;
request.current_level
};
let session_token = if success {
self.create_elevated_session_token(&request.user_id, achieved_level)
.await
.ok()
} else {
None
};
Ok(StepUpResponse {
request_id: request_id.to_string(),
success,
achieved_level,
method_used: Some(method_used),
session_token,
expires_at: if success {
Some(request.expires_at)
} else {
None
},
error: if success {
None
} else {
Some("Authentication failed".to_string())
},
})
}
pub async fn get_step_up_request(&self, request_id: &str) -> Result<StepUpRequest> {
let requests = self.active_requests.read().await;
requests
.get(request_id)
.cloned()
.ok_or_else(|| AuthError::auth_method("step_up", "Request not found"))
}
pub async fn cancel_step_up(&self, request_id: &str) -> Result<()> {
let mut requests = self.active_requests.write().await;
if let Some(request) = requests.get_mut(request_id) {
request.status = StepUpStatus::Cancelled;
}
Ok(())
}
pub async fn cleanup_expired_requests(&self) -> Result<usize> {
let mut requests = self.active_requests.write().await;
let now = Utc::now();
let initial_count = requests.len();
requests.retain(|_, request| request.expires_at > now);
Ok(initial_count - requests.len())
}
pub fn config(&self) -> &StepUpConfig {
&self.config
}
async fn get_session_auth_level(&self, session_id: &str) -> Result<AuthenticationLevel> {
if session_id.is_empty() {
return Ok(AuthenticationLevel::Basic);
}
if let Some(session) = self.session_manager.get_session(session_id) {
if let Some(auth_level_str) = session.metadata.get("auth_level") {
match auth_level_str.as_str() {
"Enhanced" => return Ok(AuthenticationLevel::Enhanced),
"High" => return Ok(AuthenticationLevel::High),
"Maximum" => return Ok(AuthenticationLevel::Maximum),
_ => return Ok(AuthenticationLevel::Basic),
}
}
}
Ok(AuthenticationLevel::Basic)
}
async fn get_session_expiry(&self, session_id: &str) -> Result<DateTime<Utc>> {
if session_id.is_empty() {
return Err(AuthError::auth_method("session", "Invalid session ID"));
}
match self.session_manager.get_session(session_id) {
Some(session) => {
let expiry_timestamp = session.last_activity + 3600; let expiry_datetime = DateTime::<Utc>::from_timestamp(expiry_timestamp as i64, 0)
.unwrap_or_else(|| Utc::now() + Duration::hours(1));
Ok(expiry_datetime)
}
None => Err(AuthError::auth_method("session", "Session not found")),
}
}
async fn update_session_auth_level(
&self,
user_id: &str,
auth_level: AuthenticationLevel,
) -> Result<()> {
if user_id.is_empty() {
return Err(AuthError::validation("User ID cannot be empty".to_string()));
}
let user_sessions = self.session_manager.get_sessions_for_subject(user_id);
for session in user_sessions {
let mut updated_metadata = session.metadata.clone();
updated_metadata.insert("auth_level".to_string(), format!("{:?}", auth_level));
updated_metadata.insert(
"auth_level_updated_at".to_string(),
Utc::now().timestamp().to_string(),
);
tracing::info!(
"Updated auth level for user {} session {} to {:?}",
user_id,
session.session_id,
auth_level
);
}
Ok(())
}
async fn create_elevated_session_token(
&self,
user_id: &str,
auth_level: AuthenticationLevel,
) -> Result<String> {
if user_id.is_empty() {
return Err(AuthError::validation("User ID cannot be empty".to_string()));
}
let token = format!("elevated_{}_{:?}_{}", user_id, auth_level, Uuid::new_v4());
Ok(token)
}
pub fn has_ciba_support(&self) -> bool {
self.ciba_manager.is_some()
}
pub async fn get_ciba_step_up_status(
&self,
request_id: &str,
) -> Result<Option<serde_json::Value>> {
let requests = self.active_requests.read().await;
if let Some(request) = requests.get(request_id)
&& let Some(challenge_data) = &request.challenge_data
&& let Some(ciba_auth_req_id) = challenge_data.get("ciba_auth_req_id")
&& let Some(ciba_manager) = &self.ciba_manager
{
let auth_req_id_str = ciba_auth_req_id.as_str().unwrap_or("");
if !auth_req_id_str.is_empty() {
match ciba_manager.get_auth_request(auth_req_id_str).await {
Ok(ciba_request) => {
let status = match ciba_request.consent.as_ref() {
Some(consent) => format!("{:?}", consent.status),
None => "pending".to_string(),
};
tracing::info!(
"CIBA authentication status for {}: {}",
auth_req_id_str,
status
);
return Ok(Some(serde_json::json!({
"ciba_auth_req_id": auth_req_id_str,
"status": status,
"mode": format!("{:?}", ciba_request.mode),
"expires_at": request.expires_at
})));
}
Err(e) => {
tracing::warn!("Failed to get CIBA request for {}: {}", auth_req_id_str, e);
return Ok(Some(serde_json::json!({
"ciba_auth_req_id": auth_req_id_str,
"status": "error",
"error": format!("Request check failed: {}", e),
"expires_at": request.expires_at
})));
}
}
}
}
Ok(None)
}
pub async fn validate_session_for_step_up(&self, session_id: &str) -> Result<bool> {
if session_id.is_empty() {
return Ok(false);
}
match self.session_manager.get_session(session_id) {
Some(session) => {
let now = chrono::Utc::now().timestamp() as u64;
let is_valid = matches!(
session.state,
crate::server::oidc::oidc_session_management::OidcSessionState::Authenticated
) && now - session.last_activity < 3600; Ok(is_valid)
}
None => Ok(false),
}
}
pub async fn get_user_sessions(&self, user_id: &str) -> Result<Vec<String>> {
if user_id.is_empty() {
return Ok(Vec::new());
}
let user_sessions = self.session_manager.get_sessions_for_subject(user_id);
let session_ids: Vec<String> = user_sessions
.iter()
.map(|session| session.session_id.clone())
.collect();
Ok(session_ids)
}
pub async fn cleanup_expired_requests_with_sessions(&self) -> Result<usize> {
let mut requests = self.active_requests.write().await;
let now = Utc::now();
let initial_count = requests.len();
let expired_requests: Vec<_> = requests
.iter()
.filter(|(_, request)| request.expires_at <= now)
.map(|(id, _)| id.clone())
.collect();
for request_id in &expired_requests {
if let Some(request) = requests.get(request_id) {
if let Some(ref challenge_data) = request.challenge_data
&& let Some(ciba_auth_req_id) = challenge_data.get("ciba_auth_req_id")
&& let Some(ref ciba_manager) = self.ciba_manager
{
if let Some(auth_req_id_str) = ciba_auth_req_id.as_str() {
match ciba_manager.cancel_auth_request(auth_req_id_str).await {
Ok(()) => {
tracing::info!(
"Successfully cancelled expired CIBA request: {}",
auth_req_id_str
);
}
Err(e) => {
tracing::warn!(
"Failed to cancel expired CIBA request {}: {}",
auth_req_id_str,
e
);
}
}
}
}
}
}
requests.retain(|_, request| request.expires_at > now);
Ok(initial_count - requests.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_step_up_evaluation_basic() {
let config = StepUpConfig::default();
let manager = SteppedUpAuthManager::new(config);
let context = StepUpContext::new("test_user", "basic-resource", "session123", AuthenticationLevel::Basic);
let result = manager
.evaluate_step_up_requirement(&context)
.await
.unwrap();
assert!(!result.required);
assert_eq!(result.target_level, AuthenticationLevel::Basic);
}
#[tokio::test]
async fn test_step_up_evaluation_high_risk() {
let config = StepUpConfig::default();
let manager = SteppedUpAuthManager::new(config);
let context = StepUpContext::new("test_user", "sensitive-resource", "session123", AuthenticationLevel::Basic)
.with_risk_score(0.8);
let result = manager
.evaluate_step_up_requirement(&context)
.await
.unwrap();
assert!(result.required);
assert_eq!(result.target_level, AuthenticationLevel::Enhanced);
assert!(
result
.matching_rules
.contains(&"suspicious_location".to_string())
);
}
#[tokio::test]
async fn test_step_up_initiation() {
let config = StepUpConfig::default();
let manager = SteppedUpAuthManager::new(config);
let request = manager
.initiate_step_up(
"test_user",
AuthenticationLevel::Basic,
AuthenticationLevel::Enhanced,
"sensitive-resource",
"High risk score detected",
)
.await
.unwrap();
assert_eq!(request.user_id, "test_user");
assert_eq!(request.current_level, AuthenticationLevel::Basic);
assert_eq!(request.target_level, AuthenticationLevel::Enhanced);
assert_eq!(request.status, StepUpStatus::Pending);
assert!(!request.allowed_methods.is_empty());
}
#[tokio::test]
async fn test_step_up_completion() {
let config = StepUpConfig::default();
let manager = SteppedUpAuthManager::new(config);
let request = manager
.initiate_step_up(
"test_user",
AuthenticationLevel::Basic,
AuthenticationLevel::Enhanced,
"sensitive-resource",
"Test step-up",
)
.await
.unwrap();
let response = manager
.complete_step_up(&request.request_id, AuthenticationMethod::TwoFactor, true)
.await
.unwrap();
assert!(response.success);
assert_eq!(response.achieved_level, AuthenticationLevel::Enhanced);
assert!(response.session_token.is_some());
}
#[test]
fn test_step_up_config_builder() {
let config = StepUpConfig::builder()
.grace_period(Duration::minutes(10))
.token_lifetime(Duration::hours(1))
.disable_location_stepup()
.max_level(AuthenticationLevel::High)
.build();
assert_eq!(config.step_up_grace_period, Duration::minutes(10));
assert_eq!(config.default_token_lifetime, Duration::hours(1));
assert!(!config.enable_location_based_stepup);
assert!(config.enable_risk_based_stepup); assert_eq!(config.max_authentication_level, AuthenticationLevel::High);
}
#[test]
fn test_step_up_context_new() {
let ctx = StepUpContext::new("user1", "resource1", "sess1", AuthenticationLevel::Basic)
.with_risk_score(0.5);
assert_eq!(ctx.user_id, "user1");
assert_eq!(ctx.resource, "resource1");
assert_eq!(ctx.session_id, "sess1");
assert_eq!(ctx.current_auth_level, AuthenticationLevel::Basic);
assert_eq!(ctx.risk_score, Some(0.5));
assert!(ctx.location.is_none());
assert!(ctx.resource_metadata.is_empty());
}
}