-
Notifications
You must be signed in to change notification settings - Fork 1k
Require email confirmation for TOTP-based logins #18689
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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, |
There was a problem hiding this comment.
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
UserUniqueLogin.user_id == userid, | ||
UserUniqueLogin.ip_address == request.remote_addr, |
There was a problem hiding this comment.
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:
warehouse/warehouse/packaging/models.py
Lines 581 to 583 in e9b70d0
__table_args__ = ( | |
Index("release_dependencies_release_kind_idx", "release_id", "kind"), | |
) |
Unless the UniqueConstraint satisfies this already?
user_service.update_user( | ||
userid, last_totp_value=form.totp_value.data | ||
) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
?
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
warehouse/warehouse/email/services.py
Line 157 in f891447
Text: {message.body_text}""" |
There was a problem hiding this comment.
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.
Fixes #18425.
This PR maintains a record of device information across logins for each user: