-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(account): Reauthentication required
- Loading branch information
Showing
17 changed files
with
295 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import time | ||
|
||
from django.contrib.auth import REDIRECT_FIELD_NAME | ||
from django.http import HttpResponseRedirect | ||
from django.urls import resolve, reverse | ||
from django.utils.http import urlencode | ||
|
||
from allauth.account import app_settings | ||
from allauth.account.utils import get_next_redirect_url | ||
from allauth.core.internal.http import deserialize_request, serialize_request | ||
from allauth.utils import import_callable | ||
|
||
|
||
STATE_SESSION_KEY = "account_reauthentication_state" | ||
AUTHENTICATED_AT_SESSION_KEY = "account_authenticated_at" | ||
|
||
|
||
def suspend_request(request, redirect_to): | ||
path = request.get_full_path() | ||
if request.method == "POST": | ||
request.session[STATE_SESSION_KEY] = {"request": serialize_request(request)} | ||
return HttpResponseRedirect( | ||
redirect_to + "?" + urlencode({REDIRECT_FIELD_NAME: path}) | ||
) | ||
|
||
|
||
def resume_request(request): | ||
state = request.session.pop(STATE_SESSION_KEY, None) | ||
if state and "callback" in state: | ||
callback = import_callable(state["callback"]) | ||
return callback(request, state["state"]) | ||
|
||
url = get_next_redirect_url(request, REDIRECT_FIELD_NAME) | ||
if not url: | ||
return None | ||
if state and "request" in state: | ||
suspended_request = deserialize_request(state["request"], request) | ||
if suspended_request.path == url: | ||
resolved = resolve(suspended_request.path) | ||
return resolved.func(suspended_request, *resolved.args, **resolved.kwargs) | ||
return HttpResponseRedirect(url) | ||
|
||
|
||
def record_authentication(request, user): | ||
request.session[AUTHENTICATED_AT_SESSION_KEY] = time.time() | ||
|
||
|
||
def reauthenticate_then_callback(request, serialize_state, callback): | ||
# TODO: Currently, ACCOUNT_REAUTHENTICATION_REQUIRED does not play well with | ||
# XHR. | ||
if did_recently_authenticate(request): | ||
return None | ||
request.session[STATE_SESSION_KEY] = { | ||
"state": serialize_state(request), | ||
"callback": callback, | ||
} | ||
return HttpResponseRedirect(reverse("account_reauthenticate")) | ||
|
||
|
||
def did_recently_authenticate(request): | ||
if request.user.is_anonymous: | ||
return False | ||
if not request.user.has_usable_password(): | ||
# TODO: This user only has social accounts attached. Now, ideally, you | ||
# would want to reauthenticate over at the social account provider. For | ||
# now, this is not implemented. Although definitely suboptimal, this | ||
# method is currently used for reauthentication checks over at MFA, and, | ||
# users that delegate the security of their account to an external | ||
# provider like Google typically use MFA over there anyway. | ||
return True | ||
authenticated_at = request.session.get(AUTHENTICATED_AT_SESSION_KEY) | ||
if not authenticated_at: | ||
return False | ||
return time.time() - authenticated_at < app_settings.REAUTHENTICATION_TIMEOUT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -263,6 +263,22 @@ def test_add(auth_client, user, settings): | |
assertTemplateUsed(resp, "account/messages/email_confirmation_sent.txt") | ||
|
||
|
||
def test_add_with_reauthentication(auth_client, user, user_password, settings): | ||
settings.ACCOUNT_REAUTHENTICATION_REQUIRED = True | ||
resp = auth_client.post( | ||
reverse("account_email"), | ||
{"action_add": "", "email": "[email protected]"}, | ||
) | ||
assert not EmailAddress.objects.filter(email="[email protected]").exists() | ||
assert resp.status_code == 302 | ||
assert resp["location"] == reverse("account_reauthenticate") + "?next=%2Femail%2F" | ||
resp = auth_client.post(resp["location"], {"password": user_password}) | ||
assert EmailAddress.objects.filter(email="[email protected]").exists() | ||
assertTemplateUsed(resp, "account/messages/email_confirmation_sent.txt") | ||
assert resp.status_code == 302 | ||
assert resp["location"] == reverse("account_email") | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"prevent_enumeration", | ||
[ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import json | ||
|
||
from django.http import QueryDict | ||
|
||
|
||
def serialize_request(request): | ||
return json.dumps( | ||
{ | ||
"path": request.path, | ||
"path_info": request.path_info, | ||
"META": {k: v for k, v in request.META.items() if isinstance(v, str)}, | ||
"GET": request.GET.urlencode(), | ||
"POST": request.POST.urlencode(), | ||
"method": request.method, | ||
} | ||
) | ||
|
||
|
||
def deserialize_request(s, request): | ||
data = json.loads(s) | ||
request.GET = QueryDict(data["GET"]) | ||
request.POST = QueryDict(data["POST"]) | ||
request.META = data["META"] | ||
request.path = data["path"] | ||
request.path_info = data["path_info"] | ||
request.method = data["method"] | ||
return request |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.