use rand_core::CryptoRng;
use time::OffsetDateTime;
use super::{
Client, CodeMaterial, Effect, Refusal, Transition, UNEXPECTED, check_code, complete_sign_in,
failure, start_session, use_code,
};
use crate::Error;
use crate::account::{Account, AccountKey};
use crate::config::Config;
use crate::limits::FailureWindow;
use crate::session::{Access, Session, SignInMethod, Stage};
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum SignIn {
Start,
VerifyPassword {
account: Option<Account>,
key: AccountKey,
},
SecondFactor {
account: Account,
session: Session,
},
SignedIn {
account: Account,
session: Session,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SignInEvent {
Password {
login: String,
account: Option<Account>,
wrong_passwords: FailureWindow,
},
PasswordChecked {
matched: bool,
client: Client,
},
Code {
code: String,
material: CodeMaterial,
client: Client,
},
Passkey {
verified: bool,
client: Client,
},
DiscoverablePasskey {
account: Option<Account>,
verified: bool,
client: Client,
},
}
impl SignIn {
#[must_use]
pub fn second_factor(session: Session, account: Account) -> Self {
SignIn::SecondFactor { account, session }
}
#[allow(clippy::too_many_lines)] pub fn advance<R: CryptoRng + ?Sized>(
self,
config: &Config,
event: SignInEvent,
now: OffsetDateTime,
rng: &mut R,
) -> Result<Transition<SignIn>, Refusal> {
match (self, event) {
(
SignIn::Start,
SignInEvent::Password {
login,
account,
wrong_passwords,
},
) => {
let limits = config.rate_limits();
if let Some(secs) = wrong_passwords.blocked(
limits.wrong_passwords_per_account,
limits.wrong_password_window,
now,
) {
return Err(Error::too_many(secs).into());
}
let account = account.filter(|account| account.active);
if let Some(account) = &account {
account.lockout.check(now)?;
}
Ok(Transition {
state: SignIn::VerifyPassword {
account,
key: AccountKey::from_login(&login),
},
effects: Vec::new(),
})
}
(
SignIn::VerifyPassword { account, key },
SignInEvent::PasswordChecked { matched, client },
) => {
let mut effects = Vec::new();
if !matched {
effects.push(Effect::RecordWrongPassword { key });
}
let Some(account) = account else {
return Err(Refusal::with(Error::SignInFailed, effects));
};
if !matched {
effects.extend(failure(&account, "password", &client, now));
return Err(Refusal::with(Error::SignInFailed, effects));
}
let (session, started) = start_session(
config,
account.id,
Stage::SecondFactor,
Some(SignInMethod::Password),
&client,
None,
now,
rng,
);
effects.extend(started);
Ok(Transition {
state: SignIn::SecondFactor { account, session },
effects,
})
}
(
SignIn::SecondFactor { account, session },
SignInEvent::Code {
code,
material,
client,
},
) => {
second_factor_allowed(config, &account, &session, now)?;
let Some(matched) = check_code(&code, &material, now) else {
let effects = failure(&account, "code", &client, now);
return Err(Refusal::with(Error::SignInFailed, effects));
};
let method = match matched {
super::CodeMatch::Totp(_) => SignInMethod::PasswordTotp,
super::CodeMatch::RecoveryCode(_) => SignInMethod::PasswordRecoveryCode,
};
let mut effects = use_code(&account, matched, &client, now);
let (session, done) = complete_sign_in(
config,
&account,
&method,
&client,
Some(session.token_hash),
now,
rng,
);
effects.extend(done);
Ok(Transition {
state: SignIn::SignedIn { account, session },
effects,
})
}
(
SignIn::SecondFactor { account, session },
SignInEvent::Passkey { verified, client },
) => {
second_factor_allowed(config, &account, &session, now)?;
if !verified {
return Err(Error::SignInFailed.into());
}
let (session, effects) = complete_sign_in(
config,
&account,
&SignInMethod::PasswordPasskey,
&client,
Some(session.token_hash),
now,
rng,
);
Ok(Transition {
state: SignIn::SignedIn { account, session },
effects,
})
}
(
SignIn::Start,
SignInEvent::DiscoverablePasskey {
account,
verified,
client,
},
) => {
let Some(account) = account.filter(|account| account.active && verified) else {
return Err(Error::SignInFailed.into());
};
let (session, effects) = complete_sign_in(
config,
&account,
&SignInMethod::Passkey,
&client,
None,
now,
rng,
);
Ok(Transition {
state: SignIn::SignedIn { account, session },
effects,
})
}
_ => Err(UNEXPECTED.into()),
}
}
}
fn second_factor_allowed(
config: &Config,
account: &Account,
session: &Session,
now: OffsetDateTime,
) -> Result<(), Error> {
if session.user_id != account.id {
return Err(Error::Unauthorized);
}
session.authorize(
account.active,
Access::Stage(Stage::SecondFactor),
config.step_up_window(),
now,
)?;
account.lockout.check(now)
}