on our Django site we need to create an admin action that will send a mail to the selected user to ask them to update their address. To do so I took a look at django's built in password reset tool, which works using tokens.
I copied and changed a bit their token thing:
class AddressEmailTokenGenerator(object):
"""
Strategy object used to generate and check tokens for the password
reset mechanism.
"""
def make_token(self, address):
"""
Returns a token that can be used once to do a password reset
for the given user.
"""
return self._make_token_with_timestamp(address, self._num_days(self._today()))
def check_token(self, address, token):
"""
Check that a password reset token is correct for a given user.
"""
# Parse the token
try:
ts_b36, hash = token.split("-")
except ValueError:
return False
try:
ts = base36_to_int(ts_b36)
except ValueError:
return False
# Check that the timestamp/uid has not been tampered with
if not constant_time_compare(self._make_token_with_timestamp(address, ts), token):
return False
# Check the timestamp is within limit
if (self._num_days(self._today()) - ts) > settings.PASSWORD_RESET_TIMEOUT_DAYS:
return False
return True
def _make_token_with_timestamp(self, address, timestamp):
# timestamp is number of days since 2001-1-1. Converted to
# base 36, this gives us a 3 digit string until about 2121
ts_b36 = int_to_base36(timestamp)
# By hashing on the internal state of the user and using state
# that is sure to change (the password salt will change as soon as
# the password is set, at least for current Django auth, and
# last_login will also change), we produce a hash that will be
# invalid as soon as it is used.
# We limit the hash to 20 chars to keep URL short
key_salt = "django.contrib.auth.tokens.PasswordResetTokenGenerator"
# Ensure results are consistent across DB backends
# login_timestamp = user.last_login.replace(microsecond=0, tzinfo=None)
value = (six.text_type(address.pk) + six.text_type(timestamp))
hash = salted_hmac(key_salt, value).hexdigest()[::2]
return "%s-%s" % (ts_b36, hash)
def _num_days(self, dt):
return (dt - date(2001, 1, 1)).days
def _today(self):
# Used for mocking in tests
return date.today()
I have my admin action:
def send_update_request(modeladmin, request, queryset):
test_mail = "test#test.ch"
for address in queryset:
token_generator = AddressEmailTokenGenerator()
token = token_generator.make_token(address)
print token
form_url = reverse("update_address", kwargs={"token":token})
send_mail("Address", form_url, "test#test.ch", [test_mail], fail_silently=False)
And now to the question: How can I get the pk of the corresponding address from the token in the url?
My view:
class UpdateAddressView(CreateView):
model = UpdateAddress
def get_context_data(self, **kwargs):
context = super(UpdateAddressView, self).get_context_data(**kwargs)
token = self.kwargs['token']
print token
#The token here is something like "3zi-ebf1da92b5e8bd3aaf86"
return context
You don't, that's not how it works. The Django reset password view has two components: a base-64 encoded string of the user ID, and the token to check validity. Decoding the ID is as simple as calling base64.urlsafe_b64decode, but you can't decode the token at all: it's hashed and salted, the only thing you can do is create a new token with the user object and check that they match.
Related
I'm trying to add this email verification process in to my flask app.
Within app/models.py I have this:
def get_confirm_account_token(self, expires_in=600):
return jwt.encode(
{'confirm_account': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'], algorithm='HS256')
#staticmethod
def verify_confirm_account_token(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['confirm_account']
except:
return
return User.query.get(id)
In my route.py I call send_account_verify_email(user) after the user registers which in turn generates a token:
token = user.get_confirm_account_token()
In my route.py I then have this:
#bp.route('/confirm/<token>')
#login_required
def confirm_email(token):
if current_user.is_confirmed:
flash('Account already confirmed')
return redirect(url_for('main.index'))
if not user:
return redirect(url_for('main.index'))
user = User.verify_confirm_account_token(token)
# HOW DO I CHECK TOKEN VALIDITY BEFORE SETTING CONFIRMED?
user.is_confirmed = True
user.confirmed_on = datetime.now()
db.session.add(user)
db.session.commit()
flash('You have confirmed your account')
else:
flash('The confirmation link is invalid or has expired')
return redirect(url_for('main.index'))
The part I'm struggling with is how to check if the token the user entered is correct - i.e what is stopping them from entering any old token - before I then mark them as confirmed?
The jwt.decode() method will raise an ExpiredSignatureError if your token is expired.
This article explains it pretty good:
https://auth0.com/blog/how-to-handle-jwt-in-python/
I'm using Django-rest-auth for authentication (https://django-rest-auth.readthedocs.io).
but when I register a new account, the api sent back to me a Token who never change after.
For more security, how can I do to have a new token every time I login ?
If you are working with an API, please structure your code in such a way.
class LoginAPIView(GenericAPIView):
serializer_class = LoginFormSerializer
#csrf_exempt
def post(self, request):
serializer = LoginFormSerializer(data=request.data)
if not serializer.is_valid():
return error_message(message=MessageKey.ERROR_MISSING_USERNAME_OR_PASSWORD.value)
username = serializer.data['username']
password = serializer.data['password']
try:
user = authenticate(username=username, password=password)
if not user:
return error_message(message=MessageKey.ERROR_INVALID_USERNAME_OR_PASSWORD.value)
if user.record_status == RecordStatus.ACTIVE.name:
# Create a new auth token using your token service
auth_token = create_auth_token(user)
user_serializer = UserSerializer(user)
user_data = user_serializer.data
user_data['auth_token'] = str(auth_token[0])
data = generate_user_account_profile(language, user, user_data)
return success_message(data=data)
else:
return error_message(MessageKey.ERROR_INVALID_USERNAME_OR_PASSWORD.value)
except Exception as ex:
traceback.print_exc()
return error_message(MessageKey.ERROR_DEFAULT_ERROR_MESSAGE.value)
The function create_auth_token(user) can be structured as folloows;
def create_auth_token(user):
"""
This is used to create or update an auth token
:param user:
:return:
"""
try:
token = Token.objects.filter(user=user)
if not token:
token = Token.objects.get_or_create(user=user)
else:
token = Token.objects.filter(user=user)
new_key = token[0].generate_key()
# Encrypt random string using SHA1
sha1_algorithm = hashlib.sha1()
sha1_algorithm.update(new_key.encode('utf-8'))
first_level_value = sha1_algorithm.hexdigest()
# Encrypt random string using MD5
md5_algorithm = hashlib.md5()
md5_algorithm.update(first_level_value.encode('utf-8'))
second_level_value = md5_algorithm.hexdigest()
token.update(key=second_level_value)
return token
except Exception as ex:
logging.error(msg=f'Failed to create auth token {ex}', stacklevel=logging.CRITICAL)
pass
Note: This can be adjusted to fit in any place of your choice i.e you can adjust it to work in your view.py file, services, etc
I tried to Subclass my PasswordResetConfirmView for me to create an error message if token is invalid. After some trial and errors, this is what I have come up with based on this Github file:
class MyPasswordResetConfirmView(PasswordResetConfirmView):
#method_decorator(sensitive_post_parameters())
#method_decorator(never_cache)
def dispatch(self, *args, **kwargs):
# I declared these myself since the variable itself is from other class and they only have these strings as value
INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'
# I declared these myself since the variable itself is from other class and they only have these strings as value
assert 'uidb64' in kwargs and 'token' in kwargs
self.validlink = False
self.user = self.get_user(kwargs['uidb64'])
if self.user is not None:
token = kwargs['token']
if token == self.reset_url_token:
session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
if self.token_generator.check_token(self.user, session_token):
# If the token is valid, display the password reset form.
self.validlink = True
return super().dispatch(*args, **kwargs)
else:
if self.token_generator.check_token(self.user, token):
# Store the token in the session and redirect to the
# password reset form at a URL without the token. That
# avoids the possibility of leaking the token in the
# HTTP Referer header.
self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
redirect_url = self.request.path.replace(token, self.reset_url_token)
return HttpResponseRedirect(redirect_url)
# Display the "Password reset unsuccessful" page.
if self.validlink:
return self.render_to_response(self.get_context_data())
else:
return redirect("invalid_link_url")
The function works but my token is changed when I open the URL.
From http://localhost:8000/reset/xxx/aj3w7r-8445df53461aeb74dfde3e06357bb6cf/ to http://localhost:8000/reset/xxx/set-password when I open.
Note: the self.reset_url_token's value is set-password. If I do:
self.reset_url_token = token, the page wont load. No error message.
I ran into this problem. It turned out that this was happening because I was reusing the same p/w reset link (inclusive of the one-time use token) more than once.
If you look at this block (and this line from the original code), you'll see that the request is redirected to the same path, having set the token value in the request.session, for the reasons that are indicated in the code comment.
else:
if self.token_generator.check_token(self.user, token):
# Store the token in the session and redirect to the
# password reset form at a URL without the token. That
# avoids the possibility of leaking the token in the
# HTTP Referer header.
self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
redirect_url = self.request.path.replace(token, self.reset_url_token)
return HttpResponseRedirect(redirect_url)
Ultimately, this dispatch method runs twice, the first time setting the token in the request.session, and the second time running the code block after if token == self.reset_url_token.
I'm assuming you're running into this while doing local development. The solution is to go through the reset password flow, as you'll need to trigger a unique token each time.
Perhaps I am missing the logic but I want to add two unique records to a model then run my tests on an API. But my API complains .get() has returned 2 records, raising the error. I notice the error happens on my .post() check, while the get methods work well. Is it a bug?
only enough if I change self.client.post with .get(), it doesn't complain.
I have tried setUpTestData() since my database supports Transactions to no avail. Essentially, I want to try the get method and the post methods. All the get operations pass but on the post test, it fails.
class ResetPwdTest(APITestCase):
""" Test module for resetting forgotten password """
#classmethod
def setUpClass(cls):
super().setUpClass()
cls.valid_payload = '1234567890ABCDEF'
cls.expired_payload = '1234567ACDES'
cls.invalid_payload = 'invalid0000'
user_info = {'first_name': 'John', 'email': 'kmsium#gmail.com','password' : 'kigali11', 'status': 1}
user = User.objects.create_user(**user_info)
PasswordReset.objects.all().delete() # delete any possible we have but having no effect so far
# create valid password token
password2 = PasswordReset(profile_id=user.id, unique_code=cls.valid_payload)
password2.save()
# create an expired passwod token
password = PasswordReset(profile_id=user.id, unique_code=cls.expired_payload)
password.save()
password.createdon = '2018-01-12 21:11:38.997207'
password.save()
def test_valid_token(self):
"""
GET Ensure a valid forgotten password reset token returns 200
WORKING
"""
response = self.client.get(reverse('new-pwd', kwargs= {'token' : self.valid_payload}))
self.assertEqual(response.status_code, 200)
def test_expired_token(self):
"""
GET Ensure an expired token to reset a forgotten password returns 400
WORKING
"""
response = self.client.get(reverse('new-pwd', kwargs= {'token' : self.expired_payload}))
self.assertEqual(response.status_code, 400)
def test_invalid_token(self):
"""
GET Ensure an invali token to reset a forgotten password returns 400
WORKING
"""
response = self.client.get(reverse('new-pwd', kwargs= {'token' : self.invalid_payload}))
self.assertEqual(response.status_code, 400)
def test_valid_token_change_pass(self):
"""
POST Ensure a valid forgotten password with accepted passwords pass reset token returns 200
FAILING BECAUSE TOKEN is not unique
"""
passwords = {'pwd': 'letmein11', 'pwd_confirm': 'letmein11'}
response = self.client.post(reverse('new-pwd', kwargs= {'token' : self.valid_payload}), passwords, format='json')
self.assertEqual(response.status_code, 200)
the View:
class ResetPwd(APIView):
permission_classes = []
def get(self,request,token,format=None):
status = 400
reply = {}
check_token = PasswordReset.objects.values('createdon','profile_id',).get(unique_code=token)
# exists but is it valid or not? It cant be
createdon = check_token["createdon"]
if createdon > timezone.now() - timedelta(hours=USER_VALUES['PWD_RESET_LIFESPAN']):
status = 200
reply['detail'] = True
else:
reply['detail'] = _('errorPwdResetLinkExpired')
return JsonResponse(reply,status=status)
def post(self,request,token,format=None):
'''
#input pwd: password
#input pwd_confirm : confirmation password
'''
status = 400
reply = {}
k = PasswordReset.objects.filter(unique_code= token).count()
print('total in db ', k) # shows 1
check_token = PasswordReset.objects.values('createdon','profile_id',).get(unique_code=token)
# error: returning 2!
#exists but is it valid or not? It cant be
createdon = check_token['createdon']
if createdon > timezone.now() - timedelta(hours=USER_VALUES['PWD_RESET_LIFESPAN']):
status = 200
else:
reply['detail'] = _('errorPwdResetLinkExpired')
'''
except:
reply['detail'] = _('errorBadPwdResetLink')
'''
return JsonResponse(reply,status=status)
I expect all the tests to pass.
I'm using Django 1.4 with Python 2.7 and Ubunutu 12.04.
I have a form that will update a user's profile. The last item in the form is the password. I pre-populate the form with the existing user's data. The password field does not get pre-populated - and that's fine.
The problem is that when I "save" the data it overwrites the password to be a null or empty field (I can't tell which). Bad.
What can I do to prevent this?
I've tried to make it a required field (forms.py):
password = forms.CharField(widget = forms.PasswordInput(), required = True)
Didn't work.
I've tried to check that the password is not None before updating it (views.py):
if (request.POST.get('password') is not None):
user.set_password(request.POST.get('password'))
Didn't work.
Does an empty form value come back as None? If not, what does it come back as and how can I check if it's empty?
EDIT 1:
I updated my one of my views to check for validation - maybe I did this wrong?
#login_required
def profile(request):
"""
.. function:: profile()
Provide the profile page, where it can be updated
:param request: Django Request object
"""
if request.user.is_authenticated():
user = User.objects.get(username = request.user.username)
user_dict = createUserProfileDict(user)
form = ProfileForm(initial = user_dict);
data = { 'user' : request.user }
data.update({ 'form' : form })
data.update(csrf(request))
if form.is_valid():
return render_to_response("profile.html", data)
Now I receive the following error:
The view rsb.views.profile didn't return an HttpResponse object.
So, it appears my form is not valid? How can I find out why?
Here is the update_profile view:
#login_required
def update_profile(request):
"""
.. function:: profile()
provide the profile page
:param request: Django Request object
"""
if request.user.is_authenticated():
user = User.objects.get(username = request.user)
user.first_name = request.POST.get('first_name')
user.last_name = request.POST.get('last_name')
user.email = request.POST.get('email')
if (request.POST.get('password') is not None):
user.set_password(request.POST.get('password'))
user.save()
# Update the additional user information tied to the user
user_info = UserProfile.objects.get(user_id = user.id)
user_info.company_name = request.POST.get('company_name')
user_info.client_type = request.POST.get('client_type')
user_info.address1 = request.POST.get('address1')
user_info.address2 = request.POST.get('address2')
user_info.city = request.POST.get('city')
user_info.state = request.POST.get('state')
user_info.country = request.POST.get('country')
user_info.zip_code = request.POST.get('zip_code')
user_info.phone_number = request.POST.get('phone_number')
user_info.save()
return profile(request)
First of all, remember to control if your form "is_valid()"
To theck if your form has been submitted with empty values or not, use
MyForm.has_changed()
too bad this is not a documented functionality :(
If you want a default password, i suggest you check if the field is valid then use something like
''.join([choice(string.letters + string.digits) for i in range(7)])
to generate a new password for the user (range(7) is the length you want). Then use an opt-in method (see: send a user an email with his temporary password)
edit based on new context:
from the django docs:
If a Field has required=False and you pass clean() an empty value,
then clean() will return a normalized empty value
rather than raising ValidationError.
For CharField, this will be a Unicode empty string.
For other Field classes, it might be None. (This varies from field to field.)
That's it, your password field should have required=False, so you can treat that as an empty string
Then in your view you could do:
if input_password != '' and input_password != saved_password:
saved_password = input_password
It's just pseudocode, but it should give you a clear idea