Skip to content

Commit 7c6c4cf

Browse files
authored
2FA basic auth notification email (#13831)
* Update notification email * Move basic auth check, send updated email * Update translations
1 parent 63ca8ed commit 7c6c4cf

File tree

8 files changed

+47
-83
lines changed

8 files changed

+47
-83
lines changed

tests/unit/accounts/test_core.py

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,7 @@
3434
TokenServiceFactory,
3535
database_login_factory,
3636
)
37-
from warehouse.errors import (
38-
BasicAuthBreachedPassword,
39-
BasicAuthFailedPassword,
40-
BasicAuthTwoFactorEnabled,
41-
)
37+
from warehouse.errors import BasicAuthBreachedPassword, BasicAuthFailedPassword
4238
from warehouse.events.tags import EventTag
4339
from warehouse.oidc.models import OIDCPublisher
4440
from warehouse.rate_limiting import IRateLimiter, RateLimit
@@ -80,7 +76,6 @@ def test_with_invalid_password(self, pyramid_request, pyramid_services):
8076
lambda userid, password, tags=None: False
8177
),
8278
is_disabled=pretend.call_recorder(lambda user_id: (False, None)),
83-
has_two_factor=pretend.call_recorder(lambda uid: False),
8479
)
8580
pyramid_services.register_service(service, IUserService, None)
8681
pyramid_services.register_service(
@@ -217,7 +212,6 @@ def test_with_valid_password(self, monkeypatch, pyramid_request, pyramid_service
217212
),
218213
update_user=pretend.call_recorder(lambda userid, last_login: None),
219214
is_disabled=pretend.call_recorder(lambda user_id: (False, None)),
220-
has_two_factor=pretend.call_recorder(lambda uid: False),
221215
)
222216
breach_service = pretend.stub(
223217
check_password=pretend.call_recorder(lambda pw, tags=None: False)
@@ -238,7 +232,6 @@ def test_with_valid_password(self, monkeypatch, pyramid_request, pyramid_service
238232
assert service.find_userid.calls == [pretend.call("myuser")]
239233
assert service.get_user.calls == [pretend.call(2)]
240234
assert service.is_disabled.calls == [pretend.call(2)]
241-
assert service.has_two_factor.calls == [pretend.call(2)]
242235
assert service.check_password.calls == [
243236
pretend.call(
244237
2,
@@ -259,52 +252,6 @@ def test_with_valid_password(self, monkeypatch, pyramid_request, pyramid_service
259252
)
260253
]
261254

262-
def test_via_basic_auth_2fa_enforced(
263-
self, monkeypatch, pyramid_request, pyramid_services
264-
):
265-
send_email = pretend.call_recorder(lambda *a, **kw: None)
266-
267-
user = pretend.stub(id=2, username="multiFactor")
268-
service = pretend.stub(
269-
get_user=pretend.call_recorder(lambda user_id: user),
270-
find_userid=pretend.call_recorder(lambda username: 2),
271-
check_password=pretend.call_recorder(
272-
lambda userid, password, tags=None: True
273-
),
274-
is_disabled=pretend.call_recorder(lambda user_id: (False, None)),
275-
disable_password=pretend.call_recorder(
276-
lambda user_id, request, reason=None: None
277-
),
278-
has_two_factor=pretend.call_recorder(lambda uid: True),
279-
)
280-
breach_service = pretend.stub(
281-
check_password=pretend.call_recorder(lambda pw, tags=None: False),
282-
)
283-
284-
pyramid_services.register_service(service, IUserService, None)
285-
pyramid_services.register_service(
286-
breach_service, IPasswordBreachedService, None
287-
)
288-
289-
pyramid_request.matched_route = pretend.stub(name="forklift.legacy.file_upload")
290-
291-
with pytest.raises(BasicAuthTwoFactorEnabled) as excinfo:
292-
_basic_auth_check("multiFactor", "mypass", pyramid_request)
293-
294-
assert excinfo.value.status == (
295-
"401 User multiFactor has two factor auth enabled, "
296-
"an API Token or Trusted Publisher must be used "
297-
"to upload in place of password."
298-
)
299-
assert service.find_userid.calls == [pretend.call("multiFactor")]
300-
assert service.get_user.calls == [pretend.call(2)]
301-
assert service.is_disabled.calls == [pretend.call(2)]
302-
assert service.has_two_factor.calls == [pretend.call(2)]
303-
assert service.check_password.calls == []
304-
assert breach_service.check_password.calls == []
305-
assert service.disable_password.calls == []
306-
assert send_email.calls == []
307-
308255
def test_via_basic_auth_compromised(
309256
self, monkeypatch, pyramid_request, pyramid_services
310257
):
@@ -324,7 +271,6 @@ def test_via_basic_auth_compromised(
324271
disable_password=pretend.call_recorder(
325272
lambda user_id, request, reason=None: None
326273
),
327-
has_two_factor=pretend.call_recorder(lambda uid: False),
328274
)
329275
breach_service = pretend.stub(
330276
check_password=pretend.call_recorder(lambda pw, tags=None: True),

tests/unit/forklift/test_legacy.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
from warehouse.admin.flags import AdminFlag, AdminFlagValue
3737
from warehouse.classifiers.models import Classifier
38+
from warehouse.errors import BasicAuthTwoFactorEnabled
3839
from warehouse.forklift import legacy
3940
from warehouse.metrics import IMetricsService
4041
from warehouse.packaging.interfaces import IFileStorage, IProjectService
@@ -2556,7 +2557,7 @@ def test_upload_fails_without_oidc_publisher_permission(
25562557
"See /the/help/url/ for more information."
25572558
).format(project.name)
25582559

2559-
def test_upload_succeeds_with_2fa_enabled(
2560+
def test_basic_auth_upload_fails_with_2fa_enabled(
25602561
self, pyramid_config, db_request, metrics, monkeypatch
25612562
):
25622563
user = UserFactory.create(totp_secret=b"secret")
@@ -2593,8 +2594,17 @@ def test_upload_succeeds_with_2fa_enabled(
25932594
IMetricsService: metrics,
25942595
}.get(svc)
25952596

2596-
legacy.file_upload(db_request)
2597+
with pytest.raises(BasicAuthTwoFactorEnabled) as excinfo:
2598+
legacy.file_upload(db_request)
2599+
2600+
resp = excinfo.value
25972601

2602+
assert resp.status_code == 401
2603+
assert resp.status == (
2604+
f"401 User { user.username } has two factor auth enabled, "
2605+
"an API Token or Trusted Publisher must be used to upload "
2606+
"in place of password."
2607+
)
25982608
assert send_email.calls == [
25992609
pretend.call(db_request, user, project_name=project.name)
26002610
]

warehouse/accounts/security_policy.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
BasicAuthAccountFrozen,
3030
BasicAuthBreachedPassword,
3131
BasicAuthFailedPassword,
32-
BasicAuthTwoFactorEnabled,
3332
WarehouseDenied,
3433
)
3534
from warehouse.events.tags import EventTag
@@ -76,15 +75,6 @@ def _basic_auth_check(username, password, request):
7675
raise _format_exc_status(BasicAuthAccountFrozen(), "Account is frozen.")
7776
else:
7877
raise _format_exc_status(HTTPUnauthorized(), "Account is disabled.")
79-
elif login_service.has_two_factor(user.id):
80-
raise _format_exc_status(
81-
BasicAuthTwoFactorEnabled(),
82-
(
83-
f"User {user.username} has two factor auth enabled, "
84-
"an API Token or Trusted Publisher must be used to upload "
85-
"in place of password."
86-
),
87-
)
8878
elif login_service.check_password(
8979
user.id,
9080
password,

warehouse/forklift/legacy.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
send_basic_auth_with_two_factor_email,
5353
send_gpg_signature_uploaded_email,
5454
)
55+
from warehouse.errors import BasicAuthTwoFactorEnabled
5556
from warehouse.events.tags import EventTag
5657
from warehouse.metrics import IMetricsService
5758
from warehouse.packaging.interfaces import IFileStorage, IProjectService
@@ -1017,10 +1018,17 @@ def file_upload(request):
10171018
request.authentication_method == AuthenticationMethod.BASIC_AUTH
10181019
and request.user.has_two_factor
10191020
):
1020-
# Eventually, raise here to disable basic auth with 2FA enabled
10211021
send_basic_auth_with_two_factor_email(
10221022
request, request.user, project_name=project.name
10231023
)
1024+
raise _exc_with_message(
1025+
BasicAuthTwoFactorEnabled,
1026+
(
1027+
f"User { request.user.username } has two factor auth enabled, "
1028+
"an API Token or Trusted Publisher must be used to upload "
1029+
"in place of password."
1030+
),
1031+
)
10241032

10251033
# Update name if it differs but is still equivalent. We don't need to check if
10261034
# they are equivalent when normalized because that's already been done when we

warehouse/locale/messages.pot

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1744,17 +1744,17 @@ msgstr ""
17441744
#, python-format
17451745
msgid ""
17461746
"During your recent upload or upload attempt of %(project_name)s to "
1747-
"%(site)s, we noticed you used basic authentication (username & "
1748-
"password). However, your account has two-factor authentication (2FA) "
1749-
"enabled."
1747+
"%(site)s, we noticed you attempted to use basic authentication (username "
1748+
"& password). However, your account has two-factor authentication "
1749+
"(2FA) enabled."
17501750
msgstr ""
17511751

17521752
#: warehouse/templates/email/basic-auth-with-2fa/body.html:22
17531753
#, python-format
17541754
msgid ""
1755-
"In the near future, %(site)s will begin prohibiting uploads using basic "
1756-
"authentication for accounts with two-factor authentication enabled. "
1757-
"Instead, we will require API tokens to be used."
1755+
"%(site)s now prohibits uploads using basic authentication for accounts "
1756+
"with two-factor authentication enabled. Instead, we require API tokens or"
1757+
" Trusted Publishers to be used instead."
17581758
msgstr ""
17591759

17601760
#: warehouse/templates/email/basic-auth-with-2fa/body.html:25
@@ -1767,12 +1767,17 @@ msgstr ""
17671767
#: warehouse/templates/email/basic-auth-with-2fa/body.html:27
17681768
#, python-format
17691769
msgid ""
1770-
"First, generate an API token for your account or project at "
1771-
"%(new_token_url)s. Then, use this token when publishing instead of your "
1770+
"If using API tokens, generate an API token for your account or project at"
1771+
" %(new_token_url)s. Then, use this token when publishing instead of your "
17721772
"username and password. See %(token_help_url)s for help using API tokens "
17731773
"to publish."
17741774
msgstr ""
17751775

1776+
#: warehouse/templates/email/basic-auth-with-2fa/body.html:30
1777+
#, python-format
1778+
msgid "If using Trusted Publishers (preferred), see %(tp_url)s to get started."
1779+
msgstr ""
1780+
17761781
#: warehouse/templates/email/canceled-as-invited-organization-member/body.html:19
17771782
#, python-format
17781783
msgid ""

warehouse/templates/email/basic-auth-with-2fa/body.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616
{% block content %}
1717
<h3>{% trans %}What?{% endtrans %}</h3>
1818
<p>
19-
{% trans site=request.registry.settings["site.name"] %}During your recent upload or upload attempt of {{ project_name }} to {{ site }}, we noticed you used basic authentication (username &amp; password). However, your account has two-factor authentication (2FA) enabled.{% endtrans %}
19+
{% trans site=request.registry.settings["site.name"] %}During your recent upload or upload attempt of {{ project_name }} to {{ site }}, we noticed you attempted to use basic authentication (username &amp; password). However, your account has two-factor authentication (2FA) enabled.{% endtrans %}
2020
</p>
2121
<p>
22-
{% trans site=request.registry.settings["site.name"] %}In the near future, {{ site }} will begin prohibiting uploads using basic authentication for accounts with two-factor authentication enabled. Instead, we will require API tokens to be used.{% endtrans %}
22+
{% trans site=request.registry.settings["site.name"] %}{{ site }} now prohibits uploads using basic authentication for accounts with two-factor authentication enabled. Instead, we require API tokens or Trusted Publishers to be used instead.{% endtrans %}
2323
</p>
2424

2525
<h3>{% trans %}What should I do?{% endtrans %}</h3>
2626
<p>
27-
{% trans new_token_url=request.route_url('manage.account.token', _host=request.registry.settings.get('warehouse.domain')), token_help_url=request.help_url(_anchor='apitoken') %}First, generate an API token for your account or project at {{ new_token_url }}. Then, use this token when publishing instead of your username and password. See {{ token_help_url }} for help using API tokens to publish.{% endtrans %}
27+
{% trans new_token_url=request.route_url('manage.account.token', _host=request.registry.settings.get('warehouse.domain')), token_help_url=request.help_url(_anchor='apitoken') %}If using API tokens, generate an API token for your account or project at {{ new_token_url }}. Then, use this token when publishing instead of your username and password. See {{ token_help_url }} for help using API tokens to publish.{% endtrans %}
28+
</p>
29+
<p>
30+
{% trans tp_url="https://docs.pypi.org/trusted-publishers/" %}If using Trusted Publishers (preferred), see {{ tp_url }} to get started.{% endtrans %}
2831
</p>
2932
{% endblock %}

warehouse/templates/email/basic-auth-with-2fa/body.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
{% block content %}
1717
# {% trans %}What?{% endtrans %}
1818

19-
{% trans site=request.registry.settings["site.name"] %}During your recent upload or upload attempt of {{ project_name }} to {{ site }}, we noticed you used basic authentication (username & password). However, your account has two-factor authentication (2FA) enabled.{% endtrans %}
19+
{% trans site=request.registry.settings["site.name"] %}During your recent upload or upload attempt of {{ project_name }} to {{ site }}, we noticed you attempted to use basic authentication (username & password). However, your account has two-factor authentication (2FA) enabled.{% endtrans %}
2020

21-
{% trans site=request.registry.settings["site.name"] %}In the near future, {{ site }} will begin prohibiting uploads using basic authentication for accounts with two-factor authentication enabled. Instead, we will require API tokens to be used.{% endtrans %}
21+
{% trans site=request.registry.settings["site.name"] %}{{ site }} now prohibits uploads using basic authentication for accounts with two-factor authentication enabled. Instead, we require API tokens or Trusted Publishers to be used instead.{% endtrans %}
2222

2323
# {% trans %}What should I do?{% endtrans %}
2424

25-
{% trans new_token_url=request.route_url('manage.account.token', _host=request.registry.settings.get('warehouse.domain')), token_help_url=request.help_url(_anchor='apitoken') %}First, generate an API token for your account or project at {{ new_token_url }}. Then, use this token when publishing instead of your username and password. See {{ token_help_url }} for help using API tokens to publish.{% endtrans %}
25+
{% trans new_token_url=request.route_url('manage.account.token', _host=request.registry.settings.get('warehouse.domain')), token_help_url=request.help_url(_anchor='apitoken') %}If using API tokens, generate an API token for your account or project at {{ new_token_url }}. Then, use this token when publishing instead of your username and password. See {{ token_help_url }} for help using API tokens to publish.{% endtrans %}
26+
27+
{% trans tp_url="https://docs.pypi.org/trusted-publishers/" %}If using Trusted Publishers (preferred), see {{ tp_url }} to get started.{% endtrans %}
2628

2729
{% endblock %}

warehouse/templates/email/basic-auth-with-2fa/subject.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414

1515
{% extends "email/_base/subject.txt" %}
1616

17-
{% block subject %}{% trans site=request.registry.settings["site.name"] %}Migrate to API tokens for uploading to {{ site }}{% endtrans %}{% endblock %}
17+
{% block subject %}{% trans site=request.registry.settings["site.name"] %}Migrate authentication for uploading to {{ site }}{% endtrans %}{% endblock %}

0 commit comments

Comments
 (0)