[RFC PATCH] REST: enable token authentication
Philippe Pepiot
phil at philpep.org
Sat May 27 09:32:05 AEST 2017
On 05/25/2017 10:47 AM, Andrew Donnellan wrote:
> Token authentication is generally viewed as a more secure option for API
> authentication than storing a username and password.
>
> Django REST Framework gives us a TokenAuthentication class and an authtoken
> app that we can use to generate random tokens and authenticate to API
> endpoints.
>
> Enable DRF's token support and add options to the user profile view to view
> or regenerate tokens.
>
> Signed-off-by: Andrew Donnellan <andrew.donnellan at au1.ibm.com>
>
> ---
>
> This is an RFC; I haven't written any tests or documentation, UI's a bit
> ugly, need to split patches.
> ---
Hi Andrew,
Thanks for adding this ! Aside my comments below, I tested a bit and it
worked fine.
> patchwork/settings/base.py | 6 ++++++
> patchwork/signals.py | 10 ++++++++++
> patchwork/templates/patchwork/profile.html | 23 +++++++++++++++++++++--
> patchwork/urls.py | 4 ++++
> patchwork/views/bundle.py | 12 ++++++++----
> patchwork/views/user.py | 19 +++++++++++++++++++
> 6 files changed, 68 insertions(+), 6 deletions(-)
>
> diff --git a/patchwork/settings/base.py b/patchwork/settings/base.py
> index 26c75c9..6fd98a7 100644
> --- a/patchwork/settings/base.py
> +++ b/patchwork/settings/base.py
> @@ -143,6 +143,7 @@ try:
>
> INSTALLED_APPS += [
> 'rest_framework',
> + 'rest_framework.authtoken',
> 'django_filters',
> ]
> except ImportError:
> @@ -158,6 +159,11 @@ REST_FRAMEWORK = {
> 'rest_framework.filters.SearchFilter',
> 'rest_framework.filters.OrderingFilter',
> ),
> + 'DEFAULT_AUTHENTICATION_CLASSES': (
> + 'rest_framework.authentication.SessionAuthentication',
> + 'rest_framework.authentication.BasicAuthentication',
> + 'rest_framework.authentication.TokenAuthentication',
> + ),
> 'SEARCH_PARAM': 'q',
> 'ORDERING_PARAM': 'order',
> }
> diff --git a/patchwork/signals.py b/patchwork/signals.py
> index 208685c..f335525 100644
> --- a/patchwork/signals.py
> +++ b/patchwork/signals.py
> @@ -19,6 +19,7 @@
>
> from datetime import datetime as dt
>
> +from django.conf import settings
> from django.db.models.signals import post_save
> from django.db.models.signals import pre_save
> from django.dispatch import receiver
> @@ -239,3 +240,12 @@ def create_series_completed_event(sender, instance, created, **kwargs):
>
> if instance.series.received_all:
> create_event(instance.series)
> +
> +
> +if settings.ENABLE_REST_API:
> + from rest_framework.authtoken.models import Token
> + @receiver(post_save, sender=settings.AUTH_USER_MODEL)
> + def create_user_created_event(sender, instance=None, created=False,
> + **kwargs):
> + if created:
> + Token.objects.create(user=instance)
> diff --git a/patchwork/templates/patchwork/profile.html b/patchwork/templates/patchwork/profile.html
> index f976195..c7be044 100644
> --- a/patchwork/templates/patchwork/profile.html
> +++ b/patchwork/templates/patchwork/profile.html
> @@ -133,8 +133,27 @@ address.</p>
> </div>
>
> <div class="box">
> -<h2>Authentication</h2>
> -<a href="{% url 'password_change' %}">Change password</a>
> + <h2>Authentication</h2>
> + <form method="post" action="{%url 'generate_token' %}">
> + {% csrf_token %}
> + <table>
> + <tr>
> + <th>Password:</th>
> + <td><a href="{% url 'password_change' %}">Change password</a>
> + </tr>
> + <tr>
> + <th>API Token:</th>
> + <td>
> + {% if api_token %}
> + {{ api_token }}
> + <input type="submit" value="Regenerate token" />
> + {% else %}
> + <input type="submit" value="Generate token" />
> + {% endif %}
> + </td>
> + </tr>
> + </table>
> + </form>
> </div>
>
> </div>
> diff --git a/patchwork/urls.py b/patchwork/urls.py
> index 1871c9a..aa49b4d 100644
> --- a/patchwork/urls.py
> +++ b/patchwork/urls.py
> @@ -101,6 +101,10 @@ urlpatterns = [
> auth_views.password_reset_complete,
> name='password_reset_complete'),
>
> + # token change
> + url(r'^user/generate-token/$', user_views.generate_token,
> + name='generate_token'),
> +
> # login/logout
> url(r'^user/login/$', auth_views.login,
> {'template_name': 'patchwork/login.html'},
> diff --git a/patchwork/views/bundle.py b/patchwork/views/bundle.py
> index 387b7c6..bb331f4 100644
> --- a/patchwork/views/bundle.py
> +++ b/patchwork/views/bundle.py
> @@ -36,17 +36,21 @@ from patchwork.views import generic_list
> from patchwork.views.utils import bundle_to_mbox
>
> if settings.ENABLE_REST_API:
> - from rest_framework.authentication import BasicAuthentication # noqa
> + from rest_framework.authentication import SessionAuthentication
> from rest_framework.exceptions import AuthenticationFailed
> + from rest_framework.settings import api_settings
>
>
> def rest_auth(request):
> if not settings.ENABLE_REST_API:
> return request.user
> try:
> - auth_result = BasicAuthentication().authenticate(request)
> - if auth_result:
> - return auth_result[0]
> + for auth in api_settings.DEFAULT_AUTHENTICATION_CLASSES:
> + if auth == SessionAuthentication:
> + continue
> + auth_result = auth().authenticate(request)
> + if auth_result:
> + return auth_result[0]
> except AuthenticationFailed:
> pass
I think the try/except block should be inside the loop when calling
authenticate().
> return request.user
> diff --git a/patchwork/views/user.py b/patchwork/views/user.py
> index 375d3d9..53e2ea8 100644
> --- a/patchwork/views/user.py
> +++ b/patchwork/views/user.py
> @@ -42,6 +42,8 @@ from patchwork.models import Project
> from patchwork.models import State
> from patchwork.views import generic_list
>
> +if settings.ENABLE_REST_API:
> + from rest_framework.authtoken.models import Token
>
> def register(request):
> context = {}
> @@ -126,6 +128,11 @@ def profile(request):
> .extra(select={'is_optout': optout_query})
> context['linked_emails'] = people
> context['linkform'] = EmailForm()
> + if settings.ENABLE_REST_API:
> + try:
> + context['api_token'] = Token.objects.get(user=request.user)
> + except Token.DoesNotExist:
> + pass
>
> return render(request, 'patchwork/profile.html', context)
>
> @@ -232,3 +239,15 @@ def todo_list(request, project_id):
> context['action_required_states'] = \
> State.objects.filter(action_required=True).all()
> return render(request, 'patchwork/todo-list.html', context)
> +
> + at login_required
> +def generate_token(request):
> + if not settings.ENABLE_REST_API:
> + raise RuntimeError('REST API not enabled')
What about disabling the url in patchwork/urls.py instead ?
> + try:
> + t = Token.objects.get(user=request.user)
> + t.delete()
> + except Token.DoesNotExist:
> + pass
This block could be replaced by:
Token.objects.filter(user=request.user).delete()
> + Token.objects.create(user=request.user)
> + return HttpResponseRedirect(reverse('user-profile'))
>
More information about the Patchwork
mailing list