what is the best way to generate a reset token in python?
Asked Answered
S

3

17

I'm trying to make a validation process for a password reset, what i've used are two values: the epoch time, and i want to use the users's old password (pbkdf2) as a key,

Since i dont want to get non ASCII characters, i've used SimpleEncode library because it's fast since it's only a BASE64 with a key used, but the problem is that the password is too long (196 chars) so i get a long key!

What i've done is split the result code = simpleencode.encode(key,asci)[::30], but this will not be unique!

To get an idea how it works, i've tried Facebook reset process, but what is given is a number! so how this process works, don't they use a key to make it hard for someone to forge a link to reset someone's password?

Update: how the algorithme will work:

1- get the time using epoche time.time()

2- generate the Base64 of the epoche time (to use for the URL) and the epoch time value + a key, this key is PBKDF2(password).

3- generate the url www.example.com/reset/user/Base64(time.time()) and send this URL + the simpleencode.encode(key,asci)[::30]

4- when the user clicks on the URL, he put the generated code, this generated code, if it matches with the URL, then let him modifiy the password, else, it is a forget URL!

Spancel answered 5/2, 2013 at 17:52 Comment(12)
It doesn't need to be unique if you confirm it's not already in the database and it's long enough that no one will guess it.Harts
what i did is to push this value to the user's document (table), so it's in the database.Spancel
You should not expose (a hash of) the user's password in any form as part of the reset token. That would allow an attacker to learn the hash, which they could then attempt to brute-force.Recognition
@Zack a hacker cant get access to the password, it'sonly it's hash that is used to calculate something in the server side, i've updated the algorithmeSpancel
You are exposing PBKDF2(password). That is reversible by brute force if "password" is weak, which it is likely to be if it is, in fact, the user's password.Recognition
how this is exposed? the user will never know that it is his password that has been used with epoche time value to generate the token.Spancel
@AbdelouahabPp It's still a security hole, and a security hole is only unlikely to be exploited until it has been. Just don't introduce them when they're avoidable. You can always securely hash the password with a salt that's different than the one you use to store the passwords.Leto
@AbdelouahabPp Also, 30 characters of base64-ed random-ish data (i.e. a hash) are definitely "unique enough".Leto
yes but the user will not enter all those 30 characters, users are always lazy :pSpancel
@AbdelouahabPp In that case you can include the user ID in the hashed key. Since the internal user ID is always unique, the user ID+password will be unique, and it's therefore very likely a 320-bit hash thereof will be unique. (That said the random GUID method where you explicitly store reset requests in the database seems simpler.)Leto
@AbdelouahabPp Also, if you can produce PBKDF2(password), that means you're storing passwords in cleartext in your database, which is a bad idea. Don't do this. Your database should contain hashes of passwords, preferrably salted with a user-specific random value. Bonus points for also salting them with a random number from a small range that you don't even store anywhere. (Although this complicates the login algorithm a bit.)Leto
no, the password are stored using pbkdf2, and this algorithme uses salt, so i just tried to use the generated text and not the password itself, but now i think i'll stick with the randomly generated digits ''.join(choice(digits) for i in xrange(4)) , and about the user ID, i'm using their emails as primary key, so it is a little bit complicated :pSpancel
S
34

Not sure it's the best way, but I'd probably just generate a UUID4, which can be used in a URL to reset the password and expire it after 'n' amount of time.

>>> import uuid
>>> uuid.uuid4().hex
'8c05904f0051419283d1024fc5ce1a59'

You could use something like http://redis.io to hold that key, with a value of the appropriate user ID and set its time to live. So, when something comes in from http://example.com/password-reset/8c05904f0051419283d1024fc5ce1a59 it looks to see if it's valid and if so then allows changes to set a new password.

If you did want a "validation pin", then store along with the token, a small random key, eg:

>>> from string import digits
>>> from random import choice
>>> ''.join(choice(digits) for i in xrange(4))
'2545'

And request that be entered on the reset link.

Sahaptin answered 5/2, 2013 at 18:7 Comment(12)
thank you for this tip, and it is url safe, but, what about the method i've used, updated the questionSpancel
@AbdelouahabPp I personally think your method is over-complicating the problem - and there's no reason to use any information about the user to generate the token...Sahaptin
so if i use the UUID method, i must generate the code that the user must input to enter?Spancel
to verify the the URL has not been forgetSpancel
@AbdelouahabPp If you mean "forged" - it's remarkably unlikely (1 in 340,282,366,920,938,463,463,374,607,431,768,211,456 chance) of generating an issued token (let alone while it's still valid). Have added a simple PIN example if that makes you feel more comfortable though...Sahaptin
hihihihihhi i take your solution :pSpancel
I don't recommend this. The output of the random library is not secure and very easy to predict. Use secure random numbers, for example the secrets module (3.6) or os.urandom (<3.6)Huntsville
@Atsch what's that got to do with UUID's which is the key point in this answer?Sahaptin
uuid is fine, since that uses os.urandom, but the pin solution is potentially dangerousHuntsville
@Atsch okay - I do say that the PIN should be stored along with the token - so they're used in combination. It's fairly pointless but entering something sometimes makes people feel like there's some security going on kind of thing.Sahaptin
my apologies, I had skimmed it before and was alarmed to see somebody using random for tokens, but it turns out that's not very accurate. It might be handy to disclaimer it though, before somebody decides to use it without the token.Huntsville
@Atsch it's late here and I'm on mobile so I'll add a little bit when I can to clarify. Thanks for pointing it out.Sahaptin
M
23

Easiest way by far is to use the ItsDangerous library:

You can serialize and sign a user ID for unsubscribing of newsletters into URLs. This way you don’t need to generate one-time tokens and store them in the database. Same thing with any kind of activation link for accounts and similar things.

You can also embed a timestamp, so very easily to set time periods without having to involve databases or queues. It's all cryptographically signed, so you can easily see if it's been tampered with.

>>> from itsdangerous import TimestampSigner
>>> s = TimestampSigner('secret-key')
>>> string = s.sign('foo')
>>> s.unsign(string, max_age=5)
Traceback (most recent call last):
  ...
itsdangerous.SignatureExpired: Signature age 15 > 5 seconds
Mown answered 5/2, 2013 at 22:10 Comment(2)
can anyone tell me how timestampsigner works, I am developing a web application, for that i need to generate time based forget password links and after sometimes the links should be expiredNebraska
If you need to see how it actually works-- you can look at the source code for the library github.com/pallets/itsdangerous/blob/master/src/itsdangerous/…Mown
H
4

Why not just use a jwt as token for this purpose, its also possible to set an expiration time to the link, as its possible to put an expiration date to the token.

  1. Generate token(JWT) encrypted with a secret key
  2. Send mail containg a link with the token as a query paramater(When the user opens the link the page can read the token)
  3. Verify the token before saving the new password

For generating jwt tokens I use pyjwt. The below code snippet shows how it can be done with an expiry time of 24 hours(1 day) and signed with a secret key:

import jwt
from datetime import datetime, timedelta, timezone

secret = "jwt_secret"
payload = {"exp": datetime.now(timezone.utc) + timedelta(days=1), "id": user_id}
token = jwt.encode(payload, secret, algorithm="HS256")
reset_token = token.decode("utf-8")

Below snippet shows how the verification of token and the new password can be set in django. If the token has expired or has been tampered with, it will raise an exception.

secret = "jwt_secret"
claims = jwt.decode(token, secret, options={"require_exp": True})
# Check if the user exists
user = User.objects.get(id=claims.get("id"))
user.set_password(password)
user.save()
Heptagonal answered 5/2, 2021 at 12:50 Comment(2)
Thank you for this additional answer. Are there any disadvantages if the user id (inside jwt) is provided by the client (step 2, verification and store new password)?Undefined
that should be fine, since ur backend code will send the email with the reset link, even if a malicious user enters someone else's id, only the correct user will have access to his email inbox and only he will be able to click on the reset linkHeptagonal

© 2022 - 2024 — McMap. All rights reserved.