cradmin_generic_token_with_metadata — Secure generic tokens with metadata¶
The purpose of the django_cradmin.apps.cradmin_generic_token_with_metadata
app is to provide
secure and unique tokens with attached metadata. The tokens are suitable for email confirmation
workflows and public share urls.
Each token belongs to a user and an app. Tokens only live for a limited time, and this time can be configured on a per app basis.
Very useful for single-use URLs like password reset, account activation, etc.
How it works¶
Lets say you have an object and want to generate a unique token for that object:
from django_cradmin.apps.cradmin_generic_token_with_metadata.models import GenericTokenWithMetadata
from django.contrib.auth import get_user_model
myuser = get_user_model().get(...) # The user is the object the token is for.
generictoken = GenericTokenWithMetadata.objects.generate(
app='myapp', content_object=myuser,
expiration_datetime=get_expiration_datetime_for_app('myapp'))
# Use generictoken.token
This creates a GenericTokenWithMetadata object with a token
-attribute that
contains a unique token. The app is provided for two reasons:
- Makes it easier to debug/browse the data model because you know what app generated the token.
- Makes it possible to configure different time to live for each app.
- Isolation. Each app has their own “namespace” of tokens.
When you have a token, typically from part of an URL, and want to get the user owning the token, use:
generictoken = GenericTokenWithMetadata.objects.pop(app='myapp', token=token)
# Use generictoken.user and generictoken.metadata
This returns the GenericTokenWithMetadata, and deletes the GenericTokenWithMetadata from the database.
Use case — password reset email¶
Lets say you want to use GenericTokenWithMetadata to generate a password reset email.
First, we want to give the user an URL where they can go to reset the password:
url = 'http://example.com/resetpassword/{}'.format(
GenericTokenWithMetadata.objects.generate(
app='passwordreset',
content_object=self.request.user,
expiration_datetime=get_expiration_datetime_for_app('passwordreset'))
Since we are using Django, we will most likely want the url to be to a view, so this would most likely look more like this:
def start_password_reset_view(request):
url = request.build_absolute_uri(reverse('my-reset-password-accept-view', kwargs={
'token': GenericTokenWithMetadata.objects.generate(
app='passwordreset', content_object=self.request.user,
expiration_datetime=get_expiration_datetime_for_app('passwordreset'))
}
# ... send an email giving the receiver instructions to click the url
In the view that lives at the URL that the user clicks to confirm the password reset request, we do something like the following:
class ResetThePassword(View):
def get(request, token):
try:
token = GenericTokenWithMetadata.objects.get_and_validate(app='passwordreset', token=token)
except GenericTokenWithMetadata.DoesNotExist:
return HttpResponse('Invalid password reset token.')
except GenericTokenExpiredError:
return HttpResponse('Your password reset token has expired.')
else:
# show a password reset form
def post(request, token):
try:
token = GenericTokenWithMetadata.objects.pop(app='passwordreset', token=token)
except GenericTokenWithMetadata.DoesNotExist:
return HttpResponse('Invalid password reset token.')
else:
# reset the password
Configure¶
You can configure the time to live of the generated tokens using the
DJANGO_CRADMIN_SECURE_USER_TOKEN_TIME_TO_LIVE_MINUTES
setting:
DJANGO_CRADMIN_SECURE_USER_TOKEN_TIME_TO_LIVE_MINUTES = {
'default': 1440,
'myapp': 2500
}
It defaults to:
DJANGO_CRADMIN_SECURE_USER_TOKEN_TIME_TO_LIVE_MINUTES = {
'default': 60*24*4
}
Delete expired tokens¶
To delete expired tokens, you can use:
GenericTokenWithMetadata.objects.delete_expired()
or the cradmin_generic_token_with_metadata_delete_expired
management command:
$ python manage.py cradmin_generic_token_with_metadata_delete_expired
Note
You do not need to delete expired tokens very often unless you generate a lot of
tokens. Expired tokens are not available through the GenericTokenWithMetadataBaseManager.pop()
method. So if you use the API as intended, you will never use an expired token.
API¶
-
get_time_to_live_minutes
(app)¶ Get the configured time to live in minutes for tokens for the given
app
.
-
get_expiration_datetime_for_app
(app)¶ Get the expiration datetime of tokens for the given
app
relative tonow
.If the given app is configured to with 60 minutes time to live, this will return a datetime object representing 60 minutes in the future.
-
generate_token
()¶ Generate a token for the
GenericTokenWithMetadata.token
field.Joins an UUID1 (unique uuid) with an UUID4 (random uuid), so the chance of this not beeing unique is very low, and guessing this is very hard.
Returns: A token that is very unlikely to not be unique.
-
exception
GenericTokenExpiredError
¶ Bases:
Exception
Raised by
GenericTokenWithMetadata.get_and_validate()
when the token is found, but has expired.
-
class
GenericTokenWithMetadataQuerySet
(model=None, query=None, using=None, hints=None)¶ Bases:
django.db.models.query.QuerySet
QuerySet for
GenericTokenWithMetadata
.-
unsafe_pop
(app, token)¶ Get the
GenericTokenWithMetadata
matching the given token and app. Removes the GenericTokenWithMetadata from the database, and returns the GenericTokenWithMetadata object.You should normally use
GenericTokenWithMetadataBaseManager.pop()
instead of this.Raises: - GenericTokenWithMetadata.DoesNotExist if no matching token is stored for
- the given app.
-
filter_has_expired
()¶ Return a queryset containing only the expired GenericTokenWithMetadata objects in the current queryset.
-
filter_not_expired
()¶ Return a queryset containing only the un-expired GenericTokenWithMetadata objects in the current queryset.
-
filter_by_content_object
(content_object)¶ Filter by
GenericTokenWithMetadata.content_object
.Examples
Lets say the content_object is a User object, you can find all tokens for that user in the
page_admin_invites
app like this:from django.contrib.auth import get_user_model user = get_user_model() GenericTokenWithMetadata.objects .filter(app='page_admin_invites') .filter_by_content_object(user)
-
filter_usable_by_content_object_in_app
(content_object, app)¶ Filters only non-expired tokens with the given
content_object
andapp
.
-
-
class
GenericTokenWithMetadataBaseManager
¶ Bases:
django.db.models.manager.Manager
Manager for
GenericTokenWithMetadata
.Inherits all methods from
GenericTokenWithMetadataQuerySet
.-
generate
(app, expiration_datetime, content_object, metadata=None)¶ Generate and save a token for the given user and app.
Returns: A GenericTokenWithMetadata
object with a token that is guaranteed to be unique.
-
pop
(app, token)¶ Get the
GenericTokenWithMetadata
matching the given token and app. Removes the GenericTokenWithMetadata from the database, and returns the GenericTokenWithMetadata object.Does not return expired tokens.
Raises: GenericTokenWithMetadata.DoesNotExist
– If no matching token is stored for the given app, or if the token is expired.
-
delete_expired
()¶ Delete all expired tokens.
-
get_and_validate
(app, token)¶ Get the given
token
for the givenapp
.Raises: GenericTokenWithMetadata.DoesNotExist
– If the token does not exist.GenericTokenExpiredError
– If the token has expired.
-
-
class
GenericTokenWithMetadata
(*args, **kwargs)¶ Bases:
django.db.models.base.Model
Provides a secure token with attached metadata suitable for email and sharing workflows like password reset, public share urls, etc.
-
app
¶ The app that generated the token. You should set this to the name of the app the generated the token.
-
token
¶ A unique and random token, set it using
generate_token()
.
-
created_datetime
¶ Datetime when the token was created.
-
expiration_datetime
¶ Datetime when the token expires. This can be None, which means that the token does not expire.
-
single_use
¶ Single use? If this is False, the token can be used an unlimited number of times.
-
metadata_json
¶ JSON encoded metadata
-
content_type
¶ The content-type of the
content_object
. Together withobject_id
this creates a generic foreign key to any Django model.
-
object_id
¶ The object ID of the
content_object
. Together withcontent_type
this creates a generic foreign key to any Django model.
-
content_object
¶ A
django.contrib.contenttypes.fields.GenericForeignKey
to the object this token is for.This generic relationship is used to associate the token with a specific object.
Use cases:
- Password reset: Use the content_object to link to a User object when you create password reset tokens.
- Invites: Use the content_object to link to the object that you are inviting users to. This enables you to filter on the content object to show pending shares.
-
is_expired
()¶ Returns True if
GenericTokenWithMetadata.expiration_datetime
is in the past, and False if it is in the future or now.
-
exception
DoesNotExist
¶ Bases:
django.core.exceptions.ObjectDoesNotExist
-
exception
MultipleObjectsReturned
¶ Bases:
django.core.exceptions.MultipleObjectsReturned
-
metadata
¶ Decode
GenericTokenWithMetadata.metadata_json
and return the result.Return None if metadata_json is empty.
-