Discussions#
Tokens design#
django-sesame builds authentication tokens as follows:
Encode the primary key of the user for which they were generated;
If
SESAME_MAX_AGE
is enabled, encode the token generation timestamp;Assemble a revocation key which is used for invalidating tokens;
Add a message authentication code (MAC) to prevent tampering with the token.
Primary keys are stored in clear text. If this is a concern, you can customize primary keys.
The revocation key is derived from:
The password of the user, unless
SESAME_INVALIDATE_ON_PASSWORD_CHANGE
is disabled;The email of the user, if
SESAME_INVALIDATE_ON_EMAIL_CHANGE
is enabled;The last login date of the user, if
SESAME_ONE_TIME
is enabled.
django-sesame provides two token formats:
v1 is the original format; it is still fully supported;
v2 is a better, cleaner, faster design that produces shorter tokens.
SESAME_TOKENS
defaults to ["sesame.tokens_v2", "sesame.tokens_v1"]
.
This means “generate tokens v2, accept tokens v2 and v1”.
Tokens v2#
Tokens v2 contain a primary key, an optional timestamp, and a signature.
The signature covers the primary key, the optional timestamp, and the revocation key. If the revocation key changes, the signature becomes invalid. As a consequence, there’s no need to include the revocation key in tokens.
The signature algorithm is Blake2 in keyed mode. A unique key is derived by
hashing the SECRET_KEY
setting and relevant django-sesame settings.
By default the signature length is 10 bytes. You can adjust it to any value
between 1 and 64 bytes with the SESAME_SIGNATURE_SIZE
setting.
Tokens v1#
Tokens v1 contain a primary key and a revocation key, plus an optional timestamp
and a signature generated by Django’s Signer
or
TimestampSigner
. The signature algorithm is
HMAC-SHA1.
Tokens invalidation#
Once a token is created, you can invalidate it in several ways.
Invalid tokens are simply rejected. You may enable logs to understand the reason.
Expiration#
By default, tokens are valid forever. You can configure expiration to give them a finite lifetime.
When expiration is enabled, tokens store the time when they were created. When authenticating them, django-sesame verifies how old they are.
You can check if an invalid token is expired by
re-authenticating it with a very large max_age
.
If that makes it valid, then it was expired.
Single-use#
By default, tokens can be reused. You can enable single-use tokens to invalidate them when they’re used.
Single-use tokens are tied to the user’s last login date. When authenticating a single-use token successfully, django-sesame updates the user’s last login date, which invalidates the token.
As a consequence of this design:
As soon as a user logs in, via django-sesame or via another login mechanism, all their single-use tokens become invalid.
Authenticating a single-use token updates the user’s last login date, even if the user isn’t logged in permanently.
Finally, single-use tokens can easily get invalidated by accident.
For all these reasons, tokens with a short lifetime are recommended over single-use tokens.
Password change#
By default, tokens are tied to the users’ passwords. Changing the password invalidates the token.
Indeed, when there’s a suspicion that an account may be compromised, changing the password is the first step. Invalidating tokens makes sense in that case.
Invalidation on password change is less needed when tokens expire quickly.
For example, if you rely on short-lived tokens to validate the email address in a sign up process and you don’t know whether validation will occur before or after initializing the password, you need to disable invalidation. That’s fine from a security perspective.
Since Django hashes the password with a random salt, the token is invalidated even if the new password is identical to the old one.
When users log in with django-sesame only, they don’t need a password. In that
case, you should set their passwords to a invalid value with
set_unusable_password()
. You can
invalidate a token at any time by calling
set_unusable_password()
again and saving
the user instance.
You can disable this behavior by setting
SESAME_INVALIDATE_ON_PASSWORD_CHANGE
to False
.
Disabling invalidation on password change makes it impossible to invalidate a single token.
If a token is compromised, your only options are to deactivate the user or to invalidate all tokens for all users.
Email change#
You can invalidate tokens when a user changes their email by setting
SESAME_INVALIDATE_ON_EMAIL_CHANGE
to True
. Then, changing the
email invalidates the token.
Enabling this behavior may improve resilience to compromised email accounts.
Inactive user#
When the is_active
attribute of a
user is set to False
, django-sesame rejects their tokens.
Different settings#
You must generate tokens and authenticate them with the same settings.
There’s a limited exception for SESAME_MAX_AGE
: as long as it isn’t
None
, you can change its value and tokens will remain valid.
If you need to invalidate all tokens, set the SESAME_KEY
setting to a
new value. This invalidates the signatures of all tokens v2.
If you still have non-expired tokens v1, do the same with
SESAME_SALT
.
Custom primary keys#
Alternative keys#
New in version 3.1.
When generating a token for a user, django-sesame stores the user’s primary key in the token.
If you’d like to store an alternative key in the token instead of the primary
key of the user model, set the SESAME_PRIMARY_KEY_FIELD
setting to the
name of the field storing the alternative key. This field must be declared with
unique=True
.
This may be useful if your user model defines a UUID key in addition to Django’s standard integer primary key and you always want to rely on the UUID externally.
Custom packers#
To keep tokens short, django-sesame creates a compact binary representations depending on the type of the primary key.
If you’re using integer or UUID primary keys, you’re fine.
If you’re using another type of primary key, for example a string created by a unique ID generation algorithm, the default representation may be suboptimal.
For example, let’s say primary keys are strings containing 24 hexadecimal characters. The default packer represents them with 25 bytes. You can reduce them to 12 bytes with this custom packer:
from sesame.packers import BasePacker
class Packer(BasePacker):
@staticmethod
def pack_pk(user_pk):
assert len(user_pk) == 24
return bytes.fromhex(user_pk)
@staticmethod
def unpack_pk(data):
return data[:12].hex(), data[12:]
Set the SESAME_PACKER
setting to the dotted Python path to the custom
packer class.
For details, see BasePacker
and look at built-in
packers defined in the sesame.packers
module.
Safari issues#
AuthenticationMiddleware
removes the token from the
URL with an HTTP 302 Redirect after authenticating a user successfully.
Unfortunately, this triggers a false positive of Safari’s Protection Against First Party Bounce Trackers. As a consequence, Safari clears cookies and the user is logged out.
To avoid this problem, django-sesame doesn’t redirect when it detects that the browser is Safari. This relies on the ua-parser package, which is an optional dependency. If ua-parser isn’t installed, django-sesame always redirects.
Stateless authentication#
Theoretically, django-sesame can provide stateless authenticated navigation
without django.contrib.sessions
, provided all internal links include the
authentication token.
When Django’s SessionMiddleware
and
AuthenticationMiddleware
aren’t
configured, django-sesame’s AuthenticationMiddleware
sets request.user
to the logged-in user or
AnonymousUser
.
There is no clear use case for this. Better persist authentication in cookies than in URLs.