Skip to content

Commit

Permalink
@Natim review: store the activation-key in the cache
Browse files Browse the repository at this point in the history
  • Loading branch information
magopian committed Jan 21, 2019
1 parent a403a09 commit ba6102a
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 8 deletions.
2 changes: 1 addition & 1 deletion kinto/plugins/accounts/__init__.py
Expand Up @@ -5,7 +5,7 @@
from pyramid.settings import asbool

from .authentication import AccountsAuthenticationPolicy as AccountsPolicy
from .utils import ACCOUNT_CACHE_KEY, ACCOUNT_POLICY_NAME
from .utils import ACCOUNT_CACHE_KEY, ACCOUNT_POLICY_NAME, ACCOUNT_VALIDATION_CACHE_KEY


__all__ = ["ACCOUNT_CACHE_KEY", "ACCOUNT_POLICY_NAME", "AccountsPolicy"]
Expand Down
1 change: 1 addition & 0 deletions kinto/plugins/accounts/utils.py
Expand Up @@ -3,6 +3,7 @@

ACCOUNT_CACHE_KEY = "accounts:{}:verified"
ACCOUNT_POLICY_NAME = "account"
ACCOUNT_VALIDATION_CACHE_KEY = "accounts:{}:validation-key"


def hash_password(password):
Expand Down
45 changes: 40 additions & 5 deletions kinto/plugins/accounts/views.py
Expand Up @@ -16,7 +16,13 @@
from kinto.core.events import ResourceChanged, ACTIONS
from kinto.core.storage import exceptions as storage_exceptions

from .utils import hash_password, is_validated, ACCOUNT_CACHE_KEY, ACCOUNT_POLICY_NAME
from .utils import (
hash_password,
is_validated,
ACCOUNT_CACHE_KEY,
ACCOUNT_POLICY_NAME,
ACCOUNT_VALIDATION_CACHE_KEY,
)


def _extract_posted_body_id(request):
Expand Down Expand Up @@ -159,15 +165,18 @@ def process_object(self, new, old=None):
raise_invalid(self.request, **error_details)

activation_key = str(uuid.uuid4())
new["activation-key"] = activation_key
extra_data = {"activation-key": activation_key}
new["validated"] = False

# Store the activation key in the cache to be used in the `validate` endpoint.
cache_validation_key(activation_key, new["id"], self.request.registry)

# Send an email to the user with the link to activate their account.
message = Message(
subject=self.email_subject_template.format(**new),
subject=self.email_subject_template.format(**extra_data, **new),
sender=self.email_sender,
recipients=[user_email],
body=self.email_body_template.format(**new),
body=self.email_body_template.format(**extra_data, **new),
)
self.mailer.send(message)

Expand Down Expand Up @@ -211,6 +220,32 @@ def on_account_changed(event):
)


def cache_validation_key(activation_key, username, registry):
"""Store a validation_key in the cache."""
settings = registry.settings
hmac_secret = settings["userid_hmac_secret"]
cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_VALIDATION_CACHE_KEY.format(username))
# Store an activation key for 7 days by default.
cache_ttl = int(settings.get("account_validation.cache_ttl_seconds", 7 * 24 * 60))

cache = registry.cache
cache.set(cache_key, activation_key, ttl=cache_ttl)


def check_validation_key(activation_key, username, registry):
"""Given a username, get the activation-key from the cache."""
hmac_secret = registry.settings["userid_hmac_secret"]
cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_VALIDATION_CACHE_KEY.format(username))

cache = registry.cache
cache_result = cache.get(cache_key)

if cache_result == activation_key:
cache.delete(cache_key) # We're done with the activation key.
return True
return False


@validation.post()
def post_validation(request):
user_id = request.matchdict["user_id"]
Expand All @@ -230,7 +265,7 @@ def post_validation(request):
error_details = {"message": f"Account {user_id} has already been validated"}
raise http_error(httpexceptions.HTTPForbidden(), **error_details)

if user["activation-key"] != activation_key:
if not check_validation_key(activation_key, user_id, request.registry):
error_details = {"message": "Account ID and activation key do not match"}
raise http_error(httpexceptions.HTTPForbidden(), **error_details)

Expand Down
15 changes: 13 additions & 2 deletions tests/plugins/test_accounts.py
Expand Up @@ -6,7 +6,7 @@
from kinto.core.testing import get_user_headers
from pyramid.exceptions import ConfigurationError

from kinto.plugins.accounts import scripts, ACCOUNT_CACHE_KEY
from kinto.plugins.accounts import scripts, ACCOUNT_CACHE_KEY, ACCOUNT_VALIDATION_CACHE_KEY
from kinto.plugins.accounts.utils import hash_password
from .. import support

Expand Down Expand Up @@ -182,6 +182,11 @@ def test_authentication_refresh_the_cache_each_time_we_authenticate(self):


class AccountValidationCreationTest(AccountsValidationWebTest):
def get_account_validation_cache(self, username):
hmac_secret = self.app.app.registry.settings["userid_hmac_secret"]
cache_key = utils.hmac_digest(hmac_secret, ACCOUNT_VALIDATION_CACHE_KEY.format(username))
return self.app.app.registry.cache.get(cache_key)

def test_create_account_fails_if_not_email(self):
activation_form_url = "https://example.com/"
resp = self.app.post_json(
Expand Down Expand Up @@ -219,14 +224,16 @@ def test_create_account_stores_activated_field(self):
status=201,
)
assert resp.json["data"]["activation-form-url"] == activation_form_url
assert resp.json["data"]["activation-key"] == uuid_string
assert "activation-key" not in resp.json["data"]
assert "validated" in resp.json["data"]
assert not resp.json["data"]["validated"]
mailer_call = self.get_mailer().send.call_args_list[0]
assert mailer_call[0][0].sender == "admin@example.com"
assert mailer_call[0][0].subject == "activate your account"
assert mailer_call[0][0].recipients == ["alice@example.com"]
assert mailer_call[0][0].body == f"{activation_form_url}{uuid_string}"
# The activation key is stored in the cache.
assert self.get_account_validation_cache("alice@example.com") == uuid_string

def test_cant_authenticate_with_unactivated_account(self):
self.app.post_json(
Expand Down Expand Up @@ -268,6 +275,8 @@ def test_validation_fail_bad_activation_key(self):
"/accounts/alice@example.com/validate/bad-activation-key", {}, status=403
)
assert "Account ID and activation key do not match" in resp.json["message"]
# The activation key is still in the cache
assert self.get_account_validation_cache("alice@example.com") is not None

def test_validation_validates_user(self):
# On user activation the 'validated' field is set to True.
Expand All @@ -292,6 +301,8 @@ def test_validation_validates_user(self):
# An active user can authenticate.
resp = self.app.get("/", headers=get_user_headers("alice@example.com", "12éé6"))
assert resp.json["user"]["id"] == "account:alice@example.com"
# Once activated, the activation key is removed from the cache.
assert self.get_account_validation_cache("alice@example.com") is None

def test_validation_fail_active_user(self):
uuid_string = "20e81ab7-51c0-444f-b204-f1c4cfe1aa7a"
Expand Down

0 comments on commit ba6102a

Please sign in to comment.