527 lines
21 KiB
Rust
527 lines
21 KiB
Rust
use axum::{extract::State, Form, Json};
|
|
use chrono::{Duration, Utc};
|
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
|
use serde::{Deserialize, Deserializer, Serialize};
|
|
use serde_json::Value;
|
|
use std::sync::Arc;
|
|
use worker::{query, Env};
|
|
|
|
use crate::{
|
|
auth::Claims,
|
|
crypto::{ct_eq, generate_salt, hash_password_for_storage, validate_totp},
|
|
db,
|
|
error::AppError,
|
|
handlers::{
|
|
allow_totp_drift, server_password_iterations,
|
|
twofactor::{is_twofactor_enabled, list_user_twofactors},
|
|
},
|
|
models::twofactor::{RememberTokenData, TwoFactor, TwoFactorType},
|
|
models::user::User,
|
|
};
|
|
|
|
/// Deserialize an Option<i32> that may have trailing/leading whitespace.
|
|
/// This handles Android clients that send "0 " instead of "0".
|
|
fn deserialize_trimmed_i32<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
use serde::de::Error;
|
|
let opt: Option<String> = Option::deserialize(deserializer)?;
|
|
match opt {
|
|
Some(s) => {
|
|
let trimmed = s.trim();
|
|
if trimmed.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
trimmed
|
|
.parse::<i32>()
|
|
.map(Some)
|
|
.map_err(|_| D::Error::custom(format!("invalid integer: {}", s)))
|
|
}
|
|
}
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct TokenRequest {
|
|
grant_type: String,
|
|
username: Option<String>,
|
|
password: Option<String>, // This is the masterPasswordHash
|
|
refresh_token: Option<String>,
|
|
// 2FA fields
|
|
#[serde(rename = "twoFactorToken")]
|
|
two_factor_token: Option<String>,
|
|
#[serde(
|
|
rename = "twoFactorProvider",
|
|
default,
|
|
deserialize_with = "deserialize_trimmed_i32"
|
|
)]
|
|
two_factor_provider: Option<i32>,
|
|
#[serde(
|
|
rename = "twoFactorRemember",
|
|
default,
|
|
deserialize_with = "deserialize_trimmed_i32"
|
|
)]
|
|
two_factor_remember: Option<i32>,
|
|
#[serde(rename = "deviceIdentifier")]
|
|
device_identifier: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub struct TokenResponse {
|
|
#[serde(rename = "access_token")]
|
|
access_token: String,
|
|
#[serde(rename = "expires_in")]
|
|
expires_in: i64,
|
|
#[serde(rename = "token_type")]
|
|
token_type: String,
|
|
#[serde(rename = "refresh_token")]
|
|
refresh_token: String,
|
|
#[serde(rename = "Key")]
|
|
key: String,
|
|
#[serde(rename = "PrivateKey")]
|
|
private_key: String,
|
|
#[serde(rename = "Kdf")]
|
|
kdf: i32,
|
|
#[serde(rename = "KdfIterations")]
|
|
kdf_iterations: i32,
|
|
#[serde(rename = "KdfMemory", skip_serializing_if = "Option::is_none")]
|
|
kdf_memory: Option<i32>,
|
|
#[serde(rename = "KdfParallelism", skip_serializing_if = "Option::is_none")]
|
|
kdf_parallelism: Option<i32>,
|
|
#[serde(rename = "ResetMasterPassword")]
|
|
reset_master_password: bool,
|
|
#[serde(rename = "ForcePasswordReset")]
|
|
force_password_reset: bool,
|
|
#[serde(rename = "UserDecryptionOptions")]
|
|
user_decryption_options: UserDecryptionOptions,
|
|
#[serde(rename = "AccountKeys")]
|
|
account_keys: serde_json::Value,
|
|
#[serde(rename = "TwoFactorToken", skip_serializing_if = "Option::is_none")]
|
|
two_factor_token: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub struct UserDecryptionOptions {
|
|
pub has_master_password: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub master_password_unlock: Option<serde_json::Value>,
|
|
pub object: String,
|
|
}
|
|
|
|
fn generate_tokens_and_response(
|
|
user: User,
|
|
env: &Arc<Env>,
|
|
two_factor_token: Option<String>,
|
|
) -> Result<Json<TokenResponse>, AppError> {
|
|
let now = Utc::now();
|
|
let expires_in = Duration::hours(1);
|
|
let exp = (now + expires_in).timestamp() as usize;
|
|
|
|
let access_claims = Claims {
|
|
sub: user.id.clone(),
|
|
exp,
|
|
nbf: now.timestamp() as usize,
|
|
premium: true,
|
|
name: user.name.clone().unwrap_or_else(|| "User".to_string()),
|
|
email: user.email.clone(),
|
|
email_verified: true,
|
|
amr: vec!["Application".into()],
|
|
};
|
|
|
|
let jwt_secret = env.secret("JWT_SECRET")?.to_string();
|
|
let access_token = encode(
|
|
&Header::default(),
|
|
&access_claims,
|
|
&EncodingKey::from_secret(jwt_secret.as_ref()),
|
|
)?;
|
|
|
|
let refresh_expires_in = Duration::days(30);
|
|
let refresh_exp = (now + refresh_expires_in).timestamp() as usize;
|
|
let refresh_claims = Claims {
|
|
sub: user.id.clone(),
|
|
exp: refresh_exp,
|
|
nbf: now.timestamp() as usize,
|
|
premium: true,
|
|
name: user.name.unwrap_or_else(|| "User".to_string()),
|
|
email: user.email.clone(),
|
|
email_verified: true,
|
|
amr: vec!["Application".into()],
|
|
};
|
|
let jwt_refresh_secret = env.secret("JWT_REFRESH_SECRET")?.to_string();
|
|
let refresh_token = encode(
|
|
&Header::default(),
|
|
&refresh_claims,
|
|
&EncodingKey::from_secret(jwt_refresh_secret.as_ref()),
|
|
)?;
|
|
|
|
let has_master_password = !user.master_password_hash.is_empty();
|
|
let master_password_unlock = if has_master_password {
|
|
Some(serde_json::json!({
|
|
"Kdf": {
|
|
"KdfType": user.kdf_type,
|
|
"Iterations": user.kdf_iterations,
|
|
"Memory": user.kdf_memory,
|
|
"Parallelism": user.kdf_parallelism
|
|
},
|
|
// This field is named inconsistently and will be removed and replaced by the "wrapped" variant in the apps.
|
|
// https://github.com/bitwarden/android/blob/release/2025.12-rc41/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt#L22-L26
|
|
"MasterKeyEncryptedUserKey": user.key,
|
|
"MasterKeyWrappedUserKey": user.key,
|
|
"Salt": user.email
|
|
}))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let account_keys = serde_json::json!({
|
|
"publicKeyEncryptionKeyPair": {
|
|
"wrappedPrivateKey": user.private_key,
|
|
"publicKey": user.public_key,
|
|
"Object": "publicKeyEncryptionKeyPair"
|
|
},
|
|
"Object": "privateKeys"
|
|
});
|
|
|
|
Ok(Json(TokenResponse {
|
|
access_token,
|
|
expires_in: expires_in.num_seconds(),
|
|
token_type: "Bearer".to_string(),
|
|
refresh_token,
|
|
key: user.key,
|
|
private_key: user.private_key,
|
|
kdf: user.kdf_type,
|
|
kdf_iterations: user.kdf_iterations,
|
|
kdf_memory: user.kdf_memory,
|
|
kdf_parallelism: user.kdf_parallelism,
|
|
force_password_reset: false,
|
|
reset_master_password: false,
|
|
user_decryption_options: UserDecryptionOptions {
|
|
has_master_password,
|
|
master_password_unlock,
|
|
object: "userDecryptionOptions".to_string(),
|
|
},
|
|
account_keys,
|
|
two_factor_token,
|
|
}))
|
|
}
|
|
|
|
#[worker::send]
|
|
pub async fn token(
|
|
State(env): State<Arc<Env>>,
|
|
Form(payload): Form<TokenRequest>,
|
|
) -> Result<Json<TokenResponse>, AppError> {
|
|
let db = db::get_db(&env)?;
|
|
match payload.grant_type.as_str() {
|
|
"password" => {
|
|
let username = payload
|
|
.username
|
|
.ok_or_else(|| AppError::BadRequest("Missing username".to_string()))?;
|
|
let password_hash = payload
|
|
.password
|
|
.ok_or_else(|| AppError::BadRequest("Missing password".to_string()))?;
|
|
|
|
// Check rate limit using email as key to prevent brute force attacks
|
|
// This limits login attempts per email address, not per IP
|
|
if let Ok(rate_limiter) = env.rate_limiter("LOGIN_RATE_LIMITER") {
|
|
let rate_limit_key = format!("login:{}", username.to_lowercase());
|
|
if let Ok(outcome) = rate_limiter.limit(rate_limit_key).await {
|
|
if !outcome.success {
|
|
return Err(AppError::TooManyRequests(
|
|
"Too many login attempts. Please try again later.".to_string(),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
let user_value: Value = db
|
|
.prepare("SELECT * FROM users WHERE email = ?1")
|
|
.bind(&[username.to_lowercase().into()])?
|
|
.first(None)
|
|
.await
|
|
.map_err(|_| AppError::Unauthorized("Invalid credentials".to_string()))?
|
|
.ok_or_else(|| AppError::Unauthorized("Invalid credentials".to_string()))?;
|
|
let user: User = serde_json::from_value(user_value).map_err(|_| AppError::Internal)?;
|
|
|
|
let verification = user.verify_master_password(&password_hash).await?;
|
|
|
|
if !verification.is_valid() {
|
|
return Err(AppError::Unauthorized("Invalid credentials".to_string()));
|
|
}
|
|
|
|
// Check for 2FA (TOTP) for this user.
|
|
let twofactors: Vec<TwoFactor> = list_user_twofactors(&db, &user.id).await?;
|
|
|
|
let mut two_factor_remember_token: Option<String> = None;
|
|
|
|
if is_twofactor_enabled(&twofactors) {
|
|
// Only advertise Authenticator (TOTP) as the real provider for now.
|
|
let twofactor_ids: Vec<i32> = vec![TwoFactorType::Authenticator as i32];
|
|
let selected_id = payload.two_factor_provider.unwrap_or(twofactor_ids[0]);
|
|
|
|
let twofactor_code = match &payload.two_factor_token {
|
|
Some(code) => code,
|
|
None => {
|
|
// Return 2FA required error
|
|
return Err(AppError::TwoFactorRequired(json_err_twofactor(
|
|
&twofactor_ids,
|
|
)));
|
|
}
|
|
};
|
|
|
|
match TwoFactorType::from_i32(selected_id) {
|
|
Some(TwoFactorType::Authenticator) => {
|
|
let tf = twofactors
|
|
.iter()
|
|
.find(|tf| {
|
|
tf.enabled && tf.atype == TwoFactorType::Authenticator as i32
|
|
})
|
|
.ok_or_else(|| {
|
|
AppError::BadRequest("TOTP not configured".to_string())
|
|
})?;
|
|
|
|
// Validate TOTP code
|
|
let allow_drift = allow_totp_drift(&env);
|
|
let new_last_used =
|
|
validate_totp(twofactor_code, &tf.data, tf.last_used, allow_drift)
|
|
.await?;
|
|
|
|
// Update last_used
|
|
query!(
|
|
&db,
|
|
"UPDATE twofactor SET last_used = ?1 WHERE uuid = ?2",
|
|
new_last_used,
|
|
&tf.uuid
|
|
)
|
|
.map_err(|_| AppError::Database)?
|
|
.run()
|
|
.await
|
|
.map_err(|_| AppError::Database)?;
|
|
}
|
|
Some(TwoFactorType::Remember) => {
|
|
// Remember is handled separately - client sends remember token from previous login
|
|
// Check remember token against stored value for this device
|
|
if let Some(ref device_id) = payload.device_identifier {
|
|
let remember_tf = twofactors.iter().find(|tf| {
|
|
tf.enabled && tf.atype == TwoFactorType::Remember as i32
|
|
});
|
|
|
|
if let Some(tf) = remember_tf {
|
|
// Parse stored remember tokens as JSON
|
|
let mut token_data = RememberTokenData::from_json(&tf.data);
|
|
|
|
// Remove expired tokens first
|
|
token_data.remove_expired();
|
|
|
|
// Validate the provided token
|
|
if !token_data.validate(device_id, twofactor_code) {
|
|
return Err(AppError::TwoFactorRequired(json_err_twofactor(
|
|
&twofactor_ids,
|
|
)));
|
|
}
|
|
|
|
// Update database with cleaned tokens (remove expired)
|
|
let updated_data = token_data.to_json();
|
|
query!(
|
|
&db,
|
|
"UPDATE twofactor SET data = ?1 WHERE uuid = ?2",
|
|
&updated_data,
|
|
&tf.uuid
|
|
)
|
|
.map_err(|_| AppError::Database)?
|
|
.run()
|
|
.await
|
|
.map_err(|_| AppError::Database)?;
|
|
|
|
// Remember token valid, proceed with login
|
|
} else {
|
|
return Err(AppError::TwoFactorRequired(json_err_twofactor(
|
|
&twofactor_ids,
|
|
)));
|
|
}
|
|
} else {
|
|
return Err(AppError::TwoFactorRequired(json_err_twofactor(
|
|
&twofactor_ids,
|
|
)));
|
|
}
|
|
}
|
|
Some(TwoFactorType::RecoveryCode) => {
|
|
// Check recovery code
|
|
if let Some(ref stored_code) = user.totp_recover {
|
|
if !ct_eq(&stored_code.to_uppercase(), &twofactor_code.to_uppercase()) {
|
|
return Err(AppError::BadRequest(
|
|
"Recovery code is incorrect".to_string(),
|
|
));
|
|
}
|
|
|
|
// Delete all 2FA and clear recovery code
|
|
query!(&db, "DELETE FROM twofactor WHERE user_uuid = ?1", &user.id)
|
|
.map_err(|_| AppError::Database)?
|
|
.run()
|
|
.await
|
|
.map_err(|_| AppError::Database)?;
|
|
|
|
query!(
|
|
&db,
|
|
"UPDATE users SET totp_recover = NULL WHERE id = ?1",
|
|
&user.id
|
|
)
|
|
.map_err(|_| AppError::Database)?
|
|
.run()
|
|
.await
|
|
.map_err(|_| AppError::Database)?;
|
|
} else {
|
|
return Err(AppError::BadRequest(
|
|
"Recovery code is incorrect".to_string(),
|
|
));
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(AppError::BadRequest(
|
|
"Invalid two factor provider".to_string(),
|
|
));
|
|
}
|
|
}
|
|
|
|
// Generate remember token if requested
|
|
if payload.two_factor_remember == Some(1) {
|
|
if let Some(ref device_id) = payload.device_identifier {
|
|
let remember_token = uuid::Uuid::new_v4().to_string();
|
|
|
|
// Load existing remember tokens or create new
|
|
let remember_tf = twofactors
|
|
.iter()
|
|
.find(|tf| tf.atype == TwoFactorType::Remember as i32);
|
|
|
|
let mut token_data = remember_tf
|
|
.map(|tf| RememberTokenData::from_json(&tf.data))
|
|
.unwrap_or_default();
|
|
|
|
// Remove expired tokens first
|
|
token_data.remove_expired();
|
|
|
|
// Add/update token for this device
|
|
token_data.upsert(device_id.clone(), remember_token.clone());
|
|
|
|
let json_data = token_data.to_json();
|
|
|
|
// Store or update remember token
|
|
query!(
|
|
&db,
|
|
"INSERT INTO twofactor (uuid, user_uuid, atype, enabled, data, last_used)
|
|
VALUES (?1, ?2, ?3, 1, ?4, 0)
|
|
ON CONFLICT(user_uuid, atype) DO UPDATE SET data = ?4",
|
|
uuid::Uuid::new_v4().to_string(),
|
|
&user.id,
|
|
TwoFactorType::Remember as i32,
|
|
&json_data
|
|
)
|
|
.map_err(|_| AppError::Database)?
|
|
.run()
|
|
.await
|
|
.map_err(|_| AppError::Database)?;
|
|
|
|
two_factor_remember_token = Some(remember_token);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Migrate/upgrade server-side password hashing parameters on successful verification.
|
|
//
|
|
// - Legacy users (no salt) are upgraded to server-side PBKDF2.
|
|
// - Existing users are upgraded if their per-user iteration count is below the configured minimum.
|
|
let desired_iterations = server_password_iterations(&env) as i32;
|
|
let needs_upgrade =
|
|
verification.needs_migration() || user.password_iterations < desired_iterations;
|
|
|
|
let user = if needs_upgrade {
|
|
// Generate new salt and hash the password using the desired iterations.
|
|
let new_salt = generate_salt()?;
|
|
let new_hash =
|
|
hash_password_for_storage(&password_hash, &new_salt, desired_iterations as u32)
|
|
.await?;
|
|
let now = Utc::now().to_rfc3339();
|
|
|
|
// Update user in database
|
|
query!(
|
|
&db,
|
|
"UPDATE users SET master_password_hash = ?1, password_salt = ?2, password_iterations = ?3, updated_at = ?4 WHERE id = ?5",
|
|
&new_hash,
|
|
&new_salt,
|
|
desired_iterations,
|
|
&now,
|
|
&user.id
|
|
)
|
|
.map_err(|_| AppError::Database)?
|
|
.run()
|
|
.await
|
|
.map_err(|_| AppError::Database)?;
|
|
|
|
// Return updated user
|
|
User {
|
|
master_password_hash: new_hash,
|
|
password_salt: Some(new_salt),
|
|
password_iterations: desired_iterations,
|
|
updated_at: now,
|
|
..user
|
|
}
|
|
} else {
|
|
user
|
|
};
|
|
|
|
generate_tokens_and_response(user, &env, two_factor_remember_token)
|
|
}
|
|
"refresh_token" => {
|
|
let refresh_token = payload
|
|
.refresh_token
|
|
.ok_or_else(|| AppError::BadRequest("Missing refresh_token".to_string()))?;
|
|
|
|
let jwt_refresh_secret = env.secret("JWT_REFRESH_SECRET")?.to_string();
|
|
let token_data = decode::<Claims>(
|
|
&refresh_token,
|
|
&DecodingKey::from_secret(jwt_refresh_secret.as_ref()),
|
|
&Validation::default(),
|
|
)
|
|
.map_err(|_| AppError::Unauthorized("Invalid refresh token".to_string()))?;
|
|
|
|
let user_id = token_data.claims.sub;
|
|
let user: Value = db
|
|
.prepare("SELECT * FROM users WHERE id = ?1")
|
|
.bind(&[user_id.into()])?
|
|
.first(None)
|
|
.await
|
|
.map_err(|_| AppError::Unauthorized("Invalid user".to_string()))?
|
|
.ok_or_else(|| AppError::Unauthorized("Invalid user".to_string()))?;
|
|
let user: User = serde_json::from_value(user).map_err(|_| AppError::Internal)?;
|
|
|
|
generate_tokens_and_response(user, &env, None)
|
|
}
|
|
_ => Err(AppError::BadRequest("Unsupported grant_type".to_string())),
|
|
}
|
|
}
|
|
|
|
/// Generates the JSON error response for 2FA required
|
|
fn json_err_twofactor(providers: &[i32]) -> Value {
|
|
let mut result = serde_json::json!({
|
|
"error": "invalid_grant",
|
|
"error_description": "Two factor required.",
|
|
"TwoFactorProviders": providers.iter().map(|p| p.to_string()).collect::<Vec<String>>(),
|
|
"TwoFactorProviders2": {},
|
|
"MasterPasswordPolicy": {
|
|
"Object": "masterPasswordPolicy"
|
|
}
|
|
});
|
|
|
|
// Add provider-specific info
|
|
for provider in providers {
|
|
result["TwoFactorProviders2"][provider.to_string()] = Value::Null;
|
|
|
|
// TOTP doesn't need any additional info
|
|
// Other providers like Email, WebAuthn etc. would add their info here
|
|
}
|
|
|
|
result
|
|
}
|