Skip to content

allow account linking for existing users when the localpart matches in upstream OAuth 2.0 logins #4193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

mcalinghee
Copy link
Contributor

@mcalinghee mcalinghee commented Mar 11, 2025

Fixes Point 1 and 4 in #2089

  • do nothing, and fail the registration/login (corresponds to allow_existing_users: false in Synapse)
  • import the link without overriding existing links for this user (corresponds to allow_existing_users: true in Synapse)
    import the link if and only if there is no existing link for this user

Introducing new configuration on_conflict in section claims_imports.localpart to set the account linking strategy

upstream_oauth2:
  providers:
    - id: 
      claims_imports:
        localpart:
          action: require 
          on_conflict: add
  • fail (default value): do nothing, and fail the registration/login (corresponds to allow_existing_users: false in Synapse)
  • add : import the link without overriding existing links for this user (corresponds to allow_existing_users: true in Synapse)
  • replace : import the link and replace any existing link for this user => not implemented in this PR
  • set : import the link if and only if there is no existing link for this user => not implemented in this PR

confirmation page when configuration is on_conflict: add :
image

@mcalinghee mcalinghee changed the title Allow existing user wip: allow existing user Mar 11, 2025
@mcalinghee mcalinghee force-pushed the feat/allow_override_user branch 6 times, most recently from 9d35eeb to d50b6c0 Compare March 12, 2025 16:20
@mcalinghee mcalinghee changed the title wip: allow existing user allow importing existing users when the localpart matches in upstream OAuth 2.0 logins Mar 12, 2025
@mcalinghee mcalinghee marked this pull request as ready for review March 12, 2025 16:21
@mcalinghee mcalinghee force-pushed the feat/allow_override_user branch from d50b6c0 to 405abce Compare March 12, 2025 16:47
@mcalinghee mcalinghee marked this pull request as draft March 12, 2025 16:52
@mcalinghee mcalinghee force-pushed the feat/allow_override_user branch from 405abce to 11f17cc Compare March 12, 2025 17:18
@mcalinghee mcalinghee marked this pull request as ready for review March 12, 2025 17:20
@mcalinghee mcalinghee force-pushed the feat/allow_override_user branch 3 times, most recently from 8f7d65b to 0307f3c Compare March 17, 2025 10:32
@mcalinghee mcalinghee force-pushed the feat/allow_override_user branch from 0307f3c to 63f811a Compare March 25, 2025 15:27
@mcalinghee mcalinghee force-pushed the feat/allow_override_user branch 2 times, most recently from fa428d9 to 3d64262 Compare April 11, 2025 06:55
@mcalinghee mcalinghee changed the title allow importing existing users when the localpart matches in upstream OAuth 2.0 logins allow account linking for existing users when the localpart matches in upstream OAuth 2.0 logins Apr 14, 2025
@mcalinghee mcalinghee force-pushed the feat/allow_override_user branch from 3d64262 to f5a3404 Compare April 14, 2025 13:01
@odelcroi odelcroi force-pushed the feat/allow_override_user branch 2 times, most recently from e2b0077 to 4405e08 Compare May 14, 2025 09:29
@odelcroi
Copy link

odelcroi commented May 15, 2025

Wrong configuration of provider

When activating the option allow_existing_users : true, the claim username is used to check if users exist or not. If the attribute mapper username is set to ignore or suggest , the user existence is not checked. Therefore to enable this option the username attribute mapper should be set to require or force.

  • documentation should mention it
  • an error should be raised at startup if the configuration does not respect this condition

@odelcroi

This comment was marked as resolved.

@mcalinghee mcalinghee marked this pull request as draft May 19, 2025 07:43
@mcalinghee mcalinghee force-pushed the feat/allow_override_user branch 5 times, most recently from 37f772c to 22b3d5d Compare June 13, 2025 15:22
@odelcroi odelcroi force-pushed the feat/allow_override_user branch from 22b3d5d to 39716c9 Compare June 16, 2025 08:39
@mcalinghee mcalinghee marked this pull request as ready for review June 16, 2025 10:11
@mcalinghee
Copy link
Contributor Author

@sandhose : are you ok for this renaming?

@sandhose
Copy link
Member

@sandhose : are you ok for this renaming?

Yep, makes sense!

@mcalinghee mcalinghee requested a review from sandhose June 16, 2025 13:35
@odelcroi odelcroi force-pushed the feat/allow_override_user branch 2 times, most recently from 93a61bd to 3d4d1a6 Compare July 3, 2025 13:46
Copy link
Member

@sandhose sandhose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're almost there! A few nitpicks, and I also wonder whether the confirmation screen is needed in this case or not; we can also change that later

Comment on lines +503 to +512
// new oauth link is allowed
let ctx = UpstreamExistingLinkContext::new(existing_user)
.with_csrf(csrf_token.form_value())
.with_language(locale);

return Ok((
cookie_jar,
Html(templates.render_upstream_oauth2_login_link(&ctx)?)
.into_response(),
));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm now wondering if it makes sense to show this intermediate page or not? For the 'active session' linking I think it does, as it's user-initiated and we don't want it to happen automatically, whereas here the intent really is for HS admins to transition from local passwords to upstream IDPs, the users should not care about this.

I'm fine with leaving it like that for now, and I'll clean that up later, as we also would like to make import dialog to be optional in some cases

@odelcroi odelcroi force-pushed the feat/allow_override_user branch 4 times, most recently from e910c49 to 00d8171 Compare July 17, 2025 09:06
mcalinghee and others added 9 commits July 17, 2025 14:37
Co-authored-by: Quentin Gliech <quenting@element.io>
Co-authored-by: Quentin Gliech <quenting@element.io>
Co-authored-by: Quentin Gliech <quenting@element.io>
Co-authored-by: Quentin Gliech <quenting@element.io>
@odelcroi odelcroi force-pushed the feat/allow_override_user branch from b877b9c to ea8c8d2 Compare July 17, 2025 12:37
@odelcroi
Copy link

@sandhose I'm done with the required modifications, I think we can consider updating the user flow by skipping the screen in a second step. If good with you, we can merge :)

Copy link
Member

@sandhose sandhose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Almost there, just a few minor stylistic things, but I think this should make the cut for the next RC on Tuesday!

Comment on lines +1373 to +1386
#[allow(clippy::disallowed_methods)]
let timestamp = chrono::Utc::now().timestamp_millis();

//suffix timestamp to generate unique test data
let existing_username = format!("{}{}", "john", timestamp);
let existing_email = format!("{}@{}", existing_username, "example.com");

//existing username matches oidc username
let oidc_username = existing_username.clone();

//oidc email is different from existing email
let oidc_email: String = format!("{}{}@{}", "any_email", timestamp, "example.com");

let subject = format!("{}+{}", "subject", timestamp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The databases are isolated between each test, no need to add timestamps there!

Comment on lines +656 to +659
// User already exists, but it is not linked, neither logged in
// Proceed by associating the link and log in the user
// Upstream_session is used to re-render the username as it is the only source
// of truth
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a little reflow and rewording of this comment

Suggested change
// User already exists, but it is not linked, neither logged in
// Proceed by associating the link and log in the user
// Upstream_session is used to re-render the username as it is the only source
// of truth
// There is an existing user with the same username, but no link.
// If the configuration allows it, the user is prompted to link the
// existing account. Note that we cannot trust the user input here,
// which is why we have to re-calculate the localpart, instead of
// passing it through form data.

Comment on lines +696 to +698
let localpart = render_attribute_template(&env, template, &context, true)?;

let maybe_user = repo.user().find_by_username(&localpart.unwrap()).await?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better error out than crash (even if we recover from panics!)

Suggested change
let localpart = render_attribute_template(&env, template, &context, true)?;
let maybe_user = repo.user().find_by_username(&localpart.unwrap()).await?;
let Some(localpart) = render_attribute_template(&env, template, &context, true)? else {
// This should never be the case at this point
return Err(RouteError::InvalidFormAction);
};
let maybe_user = repo.user().find_by_username(&localpart).await?;

&state.clock,
&mut repo,
existing_username.clone(),
existing_email.clone(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this test need an email? I don't think the create_user utility is very helpful here

Comment on lines +426 to +432

impl OnConflict {
#[must_use]
pub fn is_add(&self) -> bool {
matches!(self, Self::Add)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see it used?

Suggested change
impl OnConflict {
#[must_use]
pub fn is_add(&self) -> bool {
matches!(self, Self::Add)
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants