Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 43 additions & 1 deletion crates/defguard_common/src/db/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ impl User<Id> {
totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \
mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \
from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, ldap_remote_enrollment_completed, enrollment_pending \
FROM \"user\" WHERE email ILIKE $1",
FROM \"user\" WHERE LOWER(email) = LOWER($1)",
email
)
.fetch_optional(executor)
Expand Down Expand Up @@ -1358,6 +1358,48 @@ mod test {
secret::SecretStringWrapper,
};

#[sqlx::test]
async fn test_find_by_email_is_exact_not_a_pattern(
_: PgPoolOptions,
options: PgConnectOptions,
) {
let pool = setup_pool(options).await;

User::new(
"hpotter",
Some("pass123"),
"Potter",
"Harry",
"h.potter@hogwart.edu.uk",
None,
)
.save(&pool)
.await
.unwrap();

// Lookups stay case-insensitive.
assert!(
User::find_by_email(&pool, "H.Potter@Hogwart.Edu.UK")
.await
.unwrap()
.is_some()
);

// The argument is a value, not a `LIKE` pattern: callers pass externally
// supplied addresses, so wildcards must never match another user.
for pattern in [
"%",
"%@%",
"h.potter@hogwart.edu.u_",
"_.potter@hogwart.edu.uk",
] {
assert!(
User::find_by_email(&pool, pattern).await.unwrap().is_none(),
"email lookup treated {pattern:?} as a pattern"
);
}
}

#[sqlx::test]
async fn test_mfa_code(_: PgPoolOptions, options: PgConnectOptions) {
let pool = setup_pool(options).await;
Expand Down
12 changes: 5 additions & 7 deletions crates/defguard_common/src/db/models/wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,13 @@ impl Wizard {
.fetch_one(executor)
.await?;

let active_wizard;

if has_auto_adopt_flags {
active_wizard = ActiveWizard::AutoAdoption;
let active_wizard = if has_auto_adopt_flags {
ActiveWizard::AutoAdoption
} else if is_fresh_instance {
active_wizard = ActiveWizard::Initial;
ActiveWizard::Initial
} else {
active_wizard = ActiveWizard::Migration;
}
ActiveWizard::Migration
};

wizard.active_wizard = active_wizard;

Expand Down
19 changes: 19 additions & 0 deletions crates/defguard_core/src/enterprise/handlers/openid_login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,25 @@ pub async fn user_from_claims(
user
}
None => {
// Only an explicit `false` is rejected. Some providers omit `email_verified`.
match token_claims.email_verified() {
Some(false) => {
warn!(
"OpenID login: provider reported email address {} as unverified, \
refusing to link or create an account",
email.as_str()
);
return Err(WebError::Authorization(
"Provider did not verify the email address".into(),
));
}
None => debug!(
"OpenID login: provider sent no email_verified claim for {}, so the address \
cannot be confirmed as belonging to this identity",
email.as_str()
),
Some(true) => {}
}
if let Some(mut user) = User::find_by_email(pool, email).await? {
if !user.is_active {
debug!("User {} tried to log in, but is disabled", user.username);
Expand Down
10 changes: 5 additions & 5 deletions crates/defguard_proxy_manager/src/servers/enrollment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1113,17 +1113,17 @@ impl EnrollmentServer {
if user.is_enrolled() {
return Err(Status::permission_denied("User is already enrolled"));
}
let mfa_method: MFAMethod;

// enable corresponding MFA
match method {
let mfa_method = match method {
MfaMethod::Email => {
if !user.verify_email_mfa_code(&request.code) {
return Err(Status::invalid_argument("Email code invalid".to_owned()));
}
user.enable_email_mfa(&self.pool)
.await
.map_err(|_| Status::internal("Enabling method failed.".to_owned()))?;
mfa_method = MFAMethod::Email;
MFAMethod::Email
}
MfaMethod::Totp => {
if !user.verify_totp_code(&request.code) {
Expand All @@ -1132,12 +1132,12 @@ impl EnrollmentServer {
user.enable_totp(&self.pool)
.await
.map_err(|_| Status::internal("Enabling method failed.".to_owned()))?;
mfa_method = MFAMethod::OneTimePassword;
MFAMethod::OneTimePassword
}
_ => {
return Err(Status::invalid_argument("Method not supported"));
}
}
};
user.enable_mfa(&self.pool)
.await
.map_err(|_| Status::internal("Enabling MFA on the account failed.".to_owned()))?;
Expand Down
29 changes: 22 additions & 7 deletions crates/defguard_proxy_manager/src/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -762,8 +762,10 @@ struct OidcProviderState {
/// * `POST /token` – exchange authorization code for ID token
///
/// ### Code format
/// The authorization code must be `"{sub}:{email}:{nonce}"`. The `/token`
/// handler parses those three components and embeds them in the signed JWT.
/// The authorization code must be `"{sub}:{email}:{nonce}:{email_verified}"`.
/// The `/token` handler parses those components and embeds them in the signed
/// JWT. `email_verified` is `"true"` or `"false"`; omitting it drops the
/// claim from the token.
pub(crate) struct MockOidcProvider {
/// HTTP base URL of the mock server, e.g. `http://127.0.0.1:45321`.
pub(crate) base_url: String,
Expand Down Expand Up @@ -871,25 +873,35 @@ async fn oidc_jwks(State(state): State<OidcProviderState>) -> Json<serde_json::V
}))
}

/// Parses the authorization code as `"{sub}:{email}:{nonce}"` and returns a
/// signed RS256 ID token JWT.
/// Parses the authorization code as `"{sub}:{email}:{nonce}:{email_verified}"`
/// and returns a signed RS256 ID token JWT. The final segment is optional and
/// may be `"true"` or `"false"`; when it is absent the `email_verified` claim is
/// omitted. The nonce must not contain a colon.
async fn oidc_token(
State(state): State<OidcProviderState>,
Form(params): Form<HashMap<String, String>>,
) -> Json<serde_json::Value> {
let code = params.get("code").cloned().unwrap_or_default();
// code format: "{sub}:{email}:{nonce}"
let mut parts = code.splitn(3, ':');
// code format: "{sub}:{email}:{nonce}:{email_verified}"
let mut parts = code.splitn(4, ':');
let sub = parts.next().unwrap_or("unknown-sub").to_owned();
let email = parts.next().unwrap_or("unknown@example.com").to_owned();
let nonce = parts.next().unwrap_or("").to_owned();
let email_verified = match parts.next() {
Some("true") => Some(true),
Some("false") => Some(false),
None => None,
Some(other) => {
panic!("unexpected email_verified segment in authorization code: {other:?}")
}
};

let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();

let claims = serde_json::json!({
let mut claims = serde_json::json!({
"iss": state.base_url,
"sub": sub,
"aud": state.client_id,
Expand All @@ -901,6 +913,9 @@ async fn oidc_token(
"family_name": "OidcUser",
"name": "Test OidcUser",
});
if let Some(email_verified) = email_verified {
claims["email_verified"] = serde_json::json!(email_verified);
}

let mut header = Header::new(Algorithm::RS256);
header.kid = None;
Expand Down
Loading
Loading