Skip to content

Conversation

di
Copy link
Member

@di di commented Sep 12, 2025

Fixes #18425.

This PR maintains a record of device information across logins for each user:

  • For TOTP logins, confirmation via a link sent to the primary email is required for each new device;
  • For non-TOTP logins, no confirmation is required.

@di di requested a review from a team as a code owner September 12, 2025 17:11
Copy link
Member

@miketheman miketheman left a comment

Choose a reason for hiding this comment

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

Lots of comments inline. Let me know if they need further details.

Aside: I wonder if there's an opportunity to use these kinds of "annoying" interactions to push webauthn more, but I still want that to be a smoother experience.

def send_unrecognized_login_email(request, user, *, ip_address, user_agent, token):
return {
"username": user.username,
"ip_address": ip_address,
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: include geo data as well

Comment on lines +419 to +420
UserUniqueLogin.user_id == userid,
UserUniqueLogin.ip_address == request.remote_addr,
Copy link
Member

Choose a reason for hiding this comment

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

Since these appear to be the "couplet" of conditions used in most lookups, there should probably be a composite index for both columns.
Here's an example of how that's done:

__table_args__ = (
Index("release_dependencies_release_kind_idx", "release_id", "kind"),
)

Unless the UniqueConstraint satisfies this already?

Comment on lines +433 to +435
user_service.update_user(
userid, last_totp_value=form.totp_value.data
)
Copy link
Member

Choose a reason for hiding this comment

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

question: would it make sense to also update a unique_login.last_used or something like that when we see it again?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what that would give us. Do we want to "expire" trusted devices after some period of time?

Comment on lines +1627 to +1635
if unique_login is None and two_factor_method != "totp":
# We haven't seen this login before. Create a new one and mark it as confirmed
# if this is non-TOTP.
unique_login = UserUniqueLogin(
user_id=userid,
ip_address=request.remote_addr,
status=UniqueLoginStatus.CONFIRMED,
)
request.db.add(unique_login)
Copy link
Member

Choose a reason for hiding this comment

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

question: should this logic trigger if the user used recovery codes?

)
user: Mapped[User] = orm.relationship(back_populates="unique_logins")

ip_address: Mapped[str] = mapped_column(String, nullable=False)
Copy link
Member

Choose a reason for hiding this comment

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

question: Should this be its own string, or a relationship to warehouse.events.models.IpAddress?

Comment on lines +932 to +941
last_login=(
datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=1)
),
)
monkeypatch.setattr(
type(user),
"has_recovery_codes",
property(lambda u: has_recovery_codes),
)
user.record_event = pretend.call_recorder(lambda *a, **kw: None)
Copy link
Member

Choose a reason for hiding this comment

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

thought: instead of monkeypatching, we could pass something like:

recovery_codes=[RecoveryCode(code="fake")] if has_recovery_codes else [],

to UserFactory.create() - but since you're following an existing pattern, that's fine to defer.

Copy link
Member

Choose a reason for hiding this comment

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

When running a manual verification, the email sent was only text, not HTML. Any clue as to why?

Copy link
Member Author

Choose a reason for hiding this comment

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

Are you talking about the ConsoleAndSMTPEmailSender in local dev? I don't think it displays HTML, only text:

Text: {message.body_text}"""

Copy link
Member

Choose a reason for hiding this comment

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

I am - one line above it is the note that you can visualize the HTML version in MailDev, which usually displays something richer for other emails.

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.

Email confirmation for TOTP-based logins
2 participants