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:

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.