From a817424bdc9b7552c8f0e2ad7c6122a241f8d45e Mon Sep 17 00:00:00 2001 From: Kurt Vogel Date: Fri, 24 May 2013 17:51:20 -0700 Subject: [PATCH] Django 1.5 support, Admin interface & docs, backwards compat w/1.3. Make django_nose optional, runtime generation of uri in example app request instead of hardcoded port 8000, sqlite directory location set depending on settings.py location, import absolute_http_url_re tries Django 1.5 location first if fails tries 1.3 location, use django's datetime field in models, remove KeyGenerator, use keygen helper and some pep8 formatting. --- .gitignore | 7 + examples/manage.py | 12 + examples/mysite/apps/account/forms.py | 32 ++- examples/mysite/apps/account/urls.py | 13 +- examples/mysite/apps/account/views.py | 36 +-- examples/mysite/apps/api/urls.py | 10 +- examples/mysite/apps/api/views.py | 13 +- examples/mysite/apps/base/urls.py | 6 +- examples/mysite/apps/base/views.py | 5 +- examples/mysite/apps/client/urls.py | 12 +- examples/mysite/apps/client/views.py | 20 +- examples/mysite/apps/oauth2/forms.py | 2 +- examples/mysite/apps/oauth2/urls.py | 10 +- examples/mysite/apps/oauth2/views.py | 26 +- examples/mysite/manage.py | 14 - examples/mysite/settings.py | 20 +- .../mysite/templates/account/clients.html | 4 +- examples/mysite/templates/account/login.html | 2 +- .../mysite/templates/admin/base_site.html | 55 ++++ examples/mysite/templates/base/homepage.html | 265 +++++++++--------- examples/mysite/templates/client/client.html | 8 +- examples/mysite/templates/layout.html | 14 +- .../mysite/templates/oauth2/authorize.html | 6 +- examples/mysite/urls.py | 11 +- oauth2app/admin.py | 35 +++ oauth2app/authenticate.py | 41 +-- oauth2app/authorize.py | 26 +- oauth2app/consts.py | 3 +- oauth2app/decorators.py | 6 +- oauth2app/lib/uri.py | 11 +- oauth2app/models.py | 116 +++----- oauth2app/token.py | 40 ++- 32 files changed, 478 insertions(+), 403 deletions(-) create mode 100644 examples/manage.py delete mode 100644 examples/mysite/manage.py create mode 100644 examples/mysite/templates/admin/base_site.html create mode 100644 oauth2app/admin.py diff --git a/.gitignore b/.gitignore index 2b465bc..feedaf3 100755 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ docs/_build/* .DS_Store oauth2app.egg-info/ *.pyc +build/ +dist/ +# eclipse specific: +.project +.pydevproject +.settings + diff --git a/examples/manage.py b/examples/manage.py new file mode 100644 index 0000000..630fc3b --- /dev/null +++ b/examples/manage.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +import os +import sys + +if __name__ == "__main__": + + os.environ["DJANGO_SETTINGS_MODULE"] = "mysite.settings" + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/examples/mysite/apps/account/forms.py b/examples/mysite/apps/account/forms.py index 30e1f03..e4f30e9 100644 --- a/examples/mysite/apps/account/forms.py +++ b/examples/mysite/apps/account/forms.py @@ -4,19 +4,19 @@ from django.contrib.auth import authenticate from django.contrib.auth.forms import UserCreationForm from uni_form.helpers import FormHelper, Submit, Reset -from oauth2app.models import AccessRange + class CreateClientForm(forms.Form): - + name = forms.CharField(label="Name", max_length=30) - + @property def helper(self): form = CreateClientForm() helper = FormHelper() - reset = Reset('','Reset') + reset = Reset('', 'Reset') helper.add_input(reset) - submit = Submit('','Create Client') + submit = Submit('', 'Create Client') helper.add_input(submit) helper.form_action = '/account/clients' helper.form_method = 'POST' @@ -29,16 +29,16 @@ class ClientRemoveForm(forms.Form): class SignupForm(UserCreationForm): - + email = forms.EmailField(label="Email") - + @property def helper(self): form = SignupForm() helper = FormHelper() - reset = Reset('','Reset') + reset = Reset('', 'Reset') helper.add_input(reset) - submit = Submit('','Sign Up') + submit = Submit('', 'Sign Up') helper.add_input(submit) helper.form_action = '/account/signup' helper.form_method = 'POST' @@ -46,17 +46,17 @@ def helper(self): class LoginForm(forms.Form): - + username = forms.CharField(label="Username", max_length=30) password = forms.CharField(label="Password", widget=forms.PasswordInput) - + @property def helper(self): form = LoginForm() helper = FormHelper() - reset = Reset('','Reset') + reset = Reset('', 'Reset') helper.add_input(reset) - submit = Submit('','Log In') + submit = Submit('', 'Log In') helper.add_input(submit) helper.form_action = '/account/login' helper.form_method = 'POST' @@ -69,7 +69,9 @@ def clean(self): if username and password: self.user_cache = authenticate(username=username, password=password) if self.user_cache is None: - raise forms.ValidationError("Please enter a correct username and password. Note that both fields are case-sensitive.") + raise forms.ValidationError("Please enter a correct username and password. " + "Note that both fields are case-sensitive.") elif not self.user_cache.is_active: raise forms.ValidationError("This account is inactive.") - return self.cleaned_data \ No newline at end of file + return self.cleaned_data + diff --git a/examples/mysite/apps/account/urls.py b/examples/mysite/apps/account/urls.py index 02a8e8f..7064de5 100644 --- a/examples/mysite/apps/account/urls.py +++ b/examples/mysite/apps/account/urls.py @@ -1,9 +1,10 @@ + #-*- coding: utf-8 -*- -from django.conf.urls.defaults import patterns, url +from django.conf.urls.defaults import patterns urlpatterns = patterns('mysite.apps.account.views', - (r'^login/?$', 'login'), - (r'^logout/?$', 'logout'), - (r'^signup/?$', 'signup'), - (r'^clients/?$', 'clients'), -) \ No newline at end of file + (r'^login/?$', 'login'), + (r'^logout/?$', 'logout'), + (r'^signup/?$', 'signup'), + (r'^clients/?$', 'clients'), +) diff --git a/examples/mysite/apps/account/views.py b/examples/mysite/apps/account/views.py index 2ceca81..2c40c04 100644 --- a/examples/mysite/apps/account/views.py +++ b/examples/mysite/apps/account/views.py @@ -7,9 +7,10 @@ from django.contrib import auth from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from oauth2app.models import Client, AccessRange +from oauth2app.models import Client from .forms import SignupForm, LoginForm, CreateClientForm, ClientRemoveForm + @login_required def clients(request): if request.method == "POST": @@ -25,13 +26,8 @@ def clients(request): form = CreateClientForm() else: form = CreateClientForm() - template = { - "form":form, - "clients":Client.objects.filter(user=request.user)} - return render_to_response( - 'account/clients.html', - template, - RequestContext(request)) + template = {"form": form, "clients": Client.objects.filter(user=request.user)} + return render_to_response('account/clients.html', template, RequestContext(request)) def login(request): @@ -45,20 +41,14 @@ def login(request): return HttpResponseRedirect("/") else: form = LoginForm() - template = {"form":form} - return render_to_response( - 'account/login.html', - template, - RequestContext(request)) - - + template = {"form": form} + return render_to_response('account/login.html', template, RequestContext(request)) + + @login_required def logout(request): auth.logout(request) - return render_to_response( - 'account/logout.html', - {}, - RequestContext(request)) + return render_to_response('account/logout.html', {}, RequestContext(request)) def signup(request): @@ -76,8 +66,6 @@ def signup(request): return HttpResponseRedirect("/") else: form = SignupForm() - template = {"form":form} - return render_to_response( - 'account/signup.html', - template, - RequestContext(request)) + context = {"form": form} + return render_to_response('account/signup.html', context, RequestContext(request)) + diff --git a/examples/mysite/apps/api/urls.py b/examples/mysite/apps/api/urls.py index 1c1e38a..012df0b 100644 --- a/examples/mysite/apps/api/urls.py +++ b/examples/mysite/apps/api/urls.py @@ -1,8 +1,8 @@ #-*- coding: utf-8 -*- -from django.conf.urls.defaults import patterns, url +from django.conf.urls.defaults import patterns urlpatterns = patterns('mysite.apps.api.views', - (r'^date_joined/?$', 'date_joined'), - (r'^last_login/?$', 'last_login'), - (r'^email/?$', 'email') -) \ No newline at end of file + (r'^date_joined/?$', 'date_joined'), + (r'^last_login/?$', 'last_login'), + (r'^email/?$', 'email') +) diff --git a/examples/mysite/apps/api/views.py b/examples/mysite/apps/api/views.py index 7de0a34..8e8546e 100644 --- a/examples/mysite/apps/api/views.py +++ b/examples/mysite/apps/api/views.py @@ -4,6 +4,7 @@ from oauth2app.authenticate import JSONAuthenticator, AuthenticationException from oauth2app.models import AccessRange + def date_joined(request): scope = AccessRange.objects.get(key="date_joined") authenticator = JSONAuthenticator(scope=scope) @@ -12,9 +13,9 @@ def date_joined(request): except AuthenticationException: return authenticator.error_response() return authenticator.response({ - "date_joined":str(authenticator.user.date_joined)}) - - + "date_joined": str(authenticator.user.date_joined)}) + + def last_login(request): scope = AccessRange.objects.get(key="last_login") authenticator = JSONAuthenticator(scope=scope) @@ -22,9 +23,9 @@ def last_login(request): authenticator.validate(request) except AuthenticationException: return authenticator.error_response() - data = {"date_joined":str(request.user.date_joined)} + #data = {"date_joined": str(request.user.date_joined)} return authenticator.response({ - "last_login":str(authenticator.user.last_login)}) + "last_login": str(authenticator.user.last_login)}) def email(request): @@ -33,4 +34,4 @@ def email(request): authenticator.validate(request) except AuthenticationException: return authenticator.error_response() - return authenticator.response({"email":authenticator.user.email}) + return authenticator.response({"email": authenticator.user.email}) diff --git a/examples/mysite/apps/base/urls.py b/examples/mysite/apps/base/urls.py index fa92fb8..9c5ac33 100644 --- a/examples/mysite/apps/base/urls.py +++ b/examples/mysite/apps/base/urls.py @@ -1,6 +1,6 @@ #-*- coding: utf-8 -*- -from django.conf.urls.defaults import patterns, url +from django.conf.urls.defaults import patterns urlpatterns = patterns('mysite.apps.base.views', - (r'^/?$', 'homepage'), -) \ No newline at end of file + (r'^/?$', 'homepage'), +) diff --git a/examples/mysite/apps/base/views.py b/examples/mysite/apps/base/views.py index ac40682..b70d415 100644 --- a/examples/mysite/apps/base/views.py +++ b/examples/mysite/apps/base/views.py @@ -14,7 +14,4 @@ def homepage(request): access_tokens = access_tokens.select_related() template["access_tokens"] = access_tokens template["clients"] = clients - return render_to_response( - 'base/homepage.html', - template, - RequestContext(request)) \ No newline at end of file + return render_to_response('base/homepage.html', template, RequestContext(request)) diff --git a/examples/mysite/apps/client/urls.py b/examples/mysite/apps/client/urls.py index a6a6070..a17a3bd 100644 --- a/examples/mysite/apps/client/urls.py +++ b/examples/mysite/apps/client/urls.py @@ -1,12 +1,8 @@ #-*- coding: utf-8 -*- -from django.conf.urls.defaults import patterns, url - -urlpatterns = patterns('',)#-*- coding: utf-8 -*- - - -from django.conf.urls.defaults import patterns, url +from django.conf.urls.defaults import patterns +urlpatterns = patterns('',) urlpatterns = patterns('mysite.apps.client.views', - (r'^(?P\w+)/?$', 'client'), -)# Create your views here. + (r'^(?P\w+)/?$', 'client'), +) # Create your views here. diff --git a/examples/mysite/apps/client/views.py b/examples/mysite/apps/client/views.py index 7c1afe1..1321121 100644 --- a/examples/mysite/apps/client/views.py +++ b/examples/mysite/apps/client/views.py @@ -1,20 +1,18 @@ #-*- coding: utf-8 -*- - from django.shortcuts import render_to_response from django.template import RequestContext from oauth2app.models import Client, AccessToken, Code from base64 import b64encode + def client(request, client_id): client = Client.objects.get(key=client_id) - template = { - "client":client, - "basic_auth":"Basic %s" % b64encode(client.key + ":" + client.secret), - "codes":Code.objects.filter(client=client).select_related(), - "access_tokens":AccessToken.objects.filter(client=client).select_related()} - template["error_description"] = request.GET.get("error_description") - return render_to_response( - 'client/client.html', - template, - RequestContext(request)) \ No newline at end of file + context = { + "client": client, + "basic_auth": "Basic %s" % b64encode(client.key + ":" + client.secret), + "codes": Code.objects.filter(client=client).select_related(), + "access_tokens": AccessToken.objects.filter(client=client).select_related()} + context["error_description"] = request.GET.get("error_description") + return render_to_response('client/client.html', context, RequestContext(request)) + diff --git a/examples/mysite/apps/oauth2/forms.py b/examples/mysite/apps/oauth2/forms.py index c512f9a..b037143 100644 --- a/examples/mysite/apps/oauth2/forms.py +++ b/examples/mysite/apps/oauth2/forms.py @@ -4,4 +4,4 @@ class AuthorizeForm(forms.Form): - pass \ No newline at end of file + pass diff --git a/examples/mysite/apps/oauth2/urls.py b/examples/mysite/apps/oauth2/urls.py index 2abe008..61c76f8 100644 --- a/examples/mysite/apps/oauth2/urls.py +++ b/examples/mysite/apps/oauth2/urls.py @@ -1,8 +1,8 @@ #-*- coding: utf-8 -*- -from django.conf.urls.defaults import patterns, url +from django.conf.urls.defaults import patterns urlpatterns = patterns('', - (r'^missing_redirect_uri/?$', 'mysite.apps.oauth2.views.missing_redirect_uri'), - (r'^authorize/?$', 'mysite.apps.oauth2.views.authorize'), - (r'^token/?$', 'oauth2app.token.handler'), -) \ No newline at end of file + (r'^missing_redirect_uri/?$', 'mysite.apps.oauth2.views.missing_redirect_uri'), + (r'^authorize/?$', 'mysite.apps.oauth2.views.authorize'), + (r'^token/?$', 'oauth2app.token.handler'), +) diff --git a/examples/mysite/apps/oauth2/views.py b/examples/mysite/apps/oauth2/views.py index 2bf7705..e80048f 100644 --- a/examples/mysite/apps/oauth2/views.py +++ b/examples/mysite/apps/oauth2/views.py @@ -4,19 +4,18 @@ from django.shortcuts import render_to_response from django.http import HttpResponseRedirect from django.template import RequestContext -from uni_form.helpers import FormHelper, Submit, Reset from django.contrib.auth.decorators import login_required + +from uni_form.helpers import FormHelper, Submit + from oauth2app.authorize import Authorizer, MissingRedirectURI, AuthorizationException -from oauth2app.authorize import UnvalidatedRequest, UnauthenticatedUser from .forms import AuthorizeForm @login_required def missing_redirect_uri(request): - return render_to_response( - 'oauth2/missing_redirect_uri.html', - {}, - RequestContext(request)) + return render_to_response('oauth2/missing_redirect_uri.html', + {}, RequestContext(request)) @login_required @@ -24,9 +23,9 @@ def authorize(request): authorizer = Authorizer() try: authorizer.validate(request) - except MissingRedirectURI, e: + except MissingRedirectURI: return HttpResponseRedirect("/oauth2/missing_redirect_uri") - except AuthorizationException, e: + except AuthorizationException: # The request is malformed or invalid. Automatically # redirects to the provided redirect URL. return authorizer.error_redirect() @@ -34,21 +33,18 @@ def authorize(request): # Make sure the authorizer has validated before requesting the client # or access_ranges as otherwise they will be None. template = { - "client":authorizer.client, - "access_ranges":authorizer.access_ranges} + "client": authorizer.client, + "access_ranges": authorizer.access_ranges} template["form"] = AuthorizeForm() helper = FormHelper() - no_submit = Submit('connect','No') + no_submit = Submit('connect', 'No') helper.add_input(no_submit) yes_submit = Submit('connect', 'Yes') helper.add_input(yes_submit) helper.form_action = '/oauth2/authorize?%s' % authorizer.query_string helper.form_method = 'POST' template["helper"] = helper - return render_to_response( - 'oauth2/authorize.html', - template, - RequestContext(request)) + return render_to_response('oauth2/authorize.html', template, RequestContext(request)) elif request.method == 'POST': form = AuthorizeForm(request.POST) if form.is_valid(): diff --git a/examples/mysite/manage.py b/examples/mysite/manage.py deleted file mode 100644 index 3e4eedc..0000000 --- a/examples/mysite/manage.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -import imp -try: - imp.find_module('settings') # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) - sys.exit(1) - -import settings - -if __name__ == "__main__": - execute_manager(settings) diff --git a/examples/mysite/settings.py b/examples/mysite/settings.py index 13cf068..c1f01b7 100644 --- a/examples/mysite/settings.py +++ b/examples/mysite/settings.py @@ -2,7 +2,6 @@ import os - DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -13,7 +12,8 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'mysite.sqlite', + # default location: oauth2app/examples/mysite + 'NAME': os.path.join(os.path.dirname(__file__), 'mysite.sqlite'), 'USER': '', 'PASSWORD': '', 'HOST': '', @@ -62,7 +62,11 @@ ) TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.request', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.contrib.messages.context_processors.messages', ) MIDDLEWARE_CLASSES = ( @@ -84,6 +88,8 @@ 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.admin', + 'django.contrib.admindocs', 'mysite.apps.base', 'mysite.apps.client', 'mysite.apps.account', @@ -91,10 +97,15 @@ 'mysite.apps.api', 'uni_form', 'oauth2app', - 'django_nose', ) -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +try: + import django_nose + INSTALLED_APPS += ('django_nose', ) + TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +except ImportError: + pass + LOGGING = { 'version': 1, @@ -113,4 +124,3 @@ }, } } - diff --git a/examples/mysite/templates/account/clients.html b/examples/mysite/templates/account/clients.html index 0f592f0..235b2e4 100644 --- a/examples/mysite/templates/account/clients.html +++ b/examples/mysite/templates/account/clients.html @@ -36,7 +36,7 @@

Your Clients

Go to the homepage to authorize these clients to request information on your behalf.

{% else %} -

You have no clients. To create one just put a name in the form below - for example, “My Test Client”

+

You have no clients. To create one just put a name in the form below - for example, Test Client

{% endif %} @@ -51,4 +51,4 @@

Your Clients

}); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/examples/mysite/templates/account/login.html b/examples/mysite/templates/account/login.html index f3d5492..2f3b284 100644 --- a/examples/mysite/templates/account/login.html +++ b/examples/mysite/templates/account/login.html @@ -17,4 +17,4 @@

Login

}); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/examples/mysite/templates/admin/base_site.html b/examples/mysite/templates/admin/base_site.html new file mode 100644 index 0000000..2b1cdd7 --- /dev/null +++ b/examples/mysite/templates/admin/base_site.html @@ -0,0 +1,55 @@ +{% extends "admin/base.html" %} +{% load i18n %} + +{% block title %}{{ title }} | {% trans 'MySite admin' %}{% endblock %} + +{% block extrastyle %} + + +{% endblock %} + +{% block branding %} + + +

{% trans 'MySite administration' %}

+{% endblock %} + +{% block nav-global %}{% endblock %} diff --git a/examples/mysite/templates/base/homepage.html b/examples/mysite/templates/base/homepage.html index aeb236e..f80062f 100644 --- a/examples/mysite/templates/base/homepage.html +++ b/examples/mysite/templates/base/homepage.html @@ -7,131 +7,144 @@

OAuth 2.0 Example Site

-

This example Django site implements OAuth 2.0 clients and servers.

- -

Start by signing up then creating a client. Clients are applications that request authorization from users to access data on their behalf.

- - {% if request.user.is_authenticated %} -

The following account data is available to authorized clients at the specified endpoint URI:

- - - - - - - - - - - - - - - - -
NameValueScopeEndpoint URI
Date Joined{{ request.user.date_joined }}date_joined/api/date_joined
Last Login{{ request.user.last_login }}last_login/api/last_login
Email{{ request.user.email }}No scope/api/email
- - {% if clients %} -

Authorize the clients at the various scopes:

- - - - - - - - {% for client in clients %} - - - - - - - {% endfor %} -
Namedate_joinedlast_loginNo scope
{{ client.name }} -
- - - - - -
-
-
- - - - - -
-
-
- - - - -
-
- {% else %} -

You have no clients. Create one to demo authorization.

- {% endif %} - - {% if access_tokens %} -

You have issued the following access tokens. Follow the links to see access your data using their credentials.

- - - - - - - - - {% for access_token in access_tokens %} - - - - - - - - {% endfor %} -
Client nameScopedate_joinedlast_loginNo scope
{{ access_token.client.name }} - {% if access_token.scope.all|length == 0 %} - No scope - {% endif %} - {% for access_range in access_token.scope.all %} - {{ access_range.key }} - {% endfor %} - /api/date_joined/api/last_login/api/email
- {% else %} -

You have no access_tokens. Authorize a client to create one.

- {% endif %} - - - {% endif %} +

This example Django site implements OAuth 2.0 clients and servers.

+ +

Start by signing up then creating a client. + Clients are applications that request authorization from users to access data on their behalf.

+ + {% if request.user.is_authenticated %} +

The following account data is available to authorized clients at the specified endpoint URI:

+ + + + + + + + + + + + + + + + + + + +
NameValueScopeEndpoint URI
Date Joined{{ request.user.date_joined }}date_joined/api/date_joined
Last Login{{ request.user.last_login }}last_login/api/last_login
Email{{ request.user.email }}No scope/api/email
+ + {% if clients %} +

Authorize the clients at the various scopes:

+ + + + + + + + {% for client in clients %} + + + + + + + {% endfor %} +
Namedate_joinedlast_loginNo scope
{{ client.name }} +
+ + + + + +
+
+
+ + + + + +
+
+
+ + + + +
+
+ {% else %} +

You have no clients. Create one to demo authorization.

+ {% endif %} + + {% if access_tokens %} +

You have issued the following access tokens. Follow the links + to see access your data using their credentials.

+ + + + + + + + + {% for access_token in access_tokens %} + + + + + + + + {% endfor %} +
Client nameScopedate_joinedlast_loginNo scope
{{ access_token.client.name }} + {% if access_token.scope.all|length == 0 %} + No scope + {% endif %} + {% for access_range in access_token.scope.all %} + {{ access_range.key }} + {% endfor %} + /api/date_joined/api/last_login/api/email
+ {% else %} +

You have no access_tokens. Authorize a client to create one.

+ {% endif %} + + + {% endif %}
- - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/examples/mysite/templates/client/client.html b/examples/mysite/templates/client/client.html index ec1d0de..4aab714 100644 --- a/examples/mysite/templates/client/client.html +++ b/examples/mysite/templates/client/client.html @@ -13,9 +13,10 @@

{{ client.name }}

{% endif %} -
+
{% if access_tokens %} -

This client has been issued the following access tokens. Click the refresh token button to make a refresh request.

+

This client has been issued the following access tokens. Click the refresh token + button to make a refresh request.

@@ -51,7 +52,8 @@

{{ client.name }}

{% endif %} {% if codes %} -

This client has been issued the following authorization code. Click the authorize code button to make an authorization request.

+

This client has been issued the following authorization code. Click the + authorize code button to make an authorization request.

Token
diff --git a/examples/mysite/templates/layout.html b/examples/mysite/templates/layout.html index 40cfb13..27f8f06 100644 --- a/examples/mysite/templates/layout.html +++ b/examples/mysite/templates/layout.html @@ -14,13 +14,17 @@
{% block content %}{% endblock %} diff --git a/examples/mysite/templates/oauth2/authorize.html b/examples/mysite/templates/oauth2/authorize.html index 6c61292..4f4bdb4 100644 --- a/examples/mysite/templates/oauth2/authorize.html +++ b/examples/mysite/templates/oauth2/authorize.html @@ -8,7 +8,8 @@

Authorize

-

Grant “{{ client.name }}” access to {% if access_ranges %}the following parts of{% endif %} your account?

+

Grant {{ client.name }} access + to {% if access_ranges %}the following parts of{% endif %} your account?

{% if access_ranges %}
Key
@@ -22,7 +23,8 @@

Authorize

{% endfor %}
-

This allows “{{ client.name }}” to access parts of your sample application user account using authenticated OAuth 2.0 requests.

+

This allows {{ client.name }} to access + parts of your sample application user account using authenticated OAuth 2.0 requests.

{% endif %}
diff --git a/examples/mysite/urls.py b/examples/mysite/urls.py index 91a4efa..1b8a393 100644 --- a/examples/mysite/urls.py +++ b/examples/mysite/urls.py @@ -1,10 +1,19 @@ + from django.conf.urls.defaults import patterns, include, url -from django.conf import settings + +from django.contrib import admin + +admin.autodiscover() urlpatterns = patterns('', + # enable admin documentation: + url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + # enable admin interface + url(r'^admin/', include(admin.site.urls)), (r'^', include('mysite.apps.base.urls')), (r'^account/', include('mysite.apps.account.urls')), (r'^client/', include('mysite.apps.client.urls')), (r'^oauth2/', include('mysite.apps.oauth2.urls')), (r'^api/', include('mysite.apps.api.urls')), ) + diff --git a/oauth2app/admin.py b/oauth2app/admin.py new file mode 100644 index 0000000..c374e32 --- /dev/null +++ b/oauth2app/admin.py @@ -0,0 +1,35 @@ + +from django.contrib import admin + +from oauth2app.models import Client, AccessRange, AccessToken, Code, MACNonce + + +class ClientAdmin(admin.options.ModelAdmin): + list_display = ('name', 'user', 'key', 'secret', 'redirect_uri') + + +class AccessRangeAdmin(admin.options.ModelAdmin): + list_display = ('key', 'description') + + +class AccessTokenAdmin(admin.options.ModelAdmin): + list_display = ('client', 'user', 'token', 'refresh_token', + 'mac_key', 'issue', 'expire', 'refreshable') + filter_horizontal = ['scope'] + + +class CodeAdmin(admin.options.ModelAdmin): + list_display = ('client', 'user', 'key', 'issue', 'expire', + 'redirect_uri') + filter_horizontal = ['scope'] + + +class MACNonceAdmin(admin.options.ModelAdmin): + list_display = ('access_token', 'nonce') + + +admin.site.register(Client, ClientAdmin) +admin.site.register(AccessRange, AccessRangeAdmin) +admin.site.register(AccessToken, AccessTokenAdmin) +admin.site.register(Code, CodeAdmin) +admin.site.register(MACNonce, MACNonceAdmin) diff --git a/oauth2app/authenticate.py b/oauth2app/authenticate.py index d75ae5d..8de31a9 100644 --- a/oauth2app/authenticate.py +++ b/oauth2app/authenticate.py @@ -4,16 +4,20 @@ """OAuth 2.0 Authentication""" +import datetime from hashlib import sha256 from urlparse import parse_qsl -try: import simplejson as json -except ImportError: import json +try: + import simplejson as json +except ImportError: + import json from django.conf import settings from django.http import HttpResponse from .exceptions import OAuth2Exception -from .models import AccessToken, AccessRange, TimestampGenerator +from .models import AccessToken, AccessRange from .consts import REALM, AUTHENTICATION_METHOD, MAC, BEARER + class AuthenticationException(OAuth2Exception): """Authentication exception base class.""" pass @@ -38,10 +42,12 @@ class InsufficientScope(AuthenticationException): access token.""" error = 'insufficient_scope' + class UnvalidatedRequest(OAuth2Exception): """The method requested requires a validated request to continue.""" pass + class Authenticator(object): """Django HttpRequest authenticator. Checks a request for valid credentials and scope. @@ -65,10 +71,7 @@ class Authenticator(object): error = None attempted_validation = False - def __init__( - self, - scope=None, - authentication_method=AUTHENTICATION_METHOD): + def __init__(self, scope=None, authentication_method=AUTHENTICATION_METHOD): if authentication_method not in [BEARER, MAC, BEARER | MAC]: raise OAuth2Exception("Possible values for authentication_method" " are oauth2app.consts.MAC, oauth2app.consts.BEARER, " @@ -130,8 +133,7 @@ def _validate(self): if len(new_scope) > 0: raise InsufficientScope(("Access token has insufficient " "scope: %s") % ','.join(self.authorized_scope)) - now = TimestampGenerator()() - if self.access_token.expire < now: + if self.access_token.expire < datetime.datetime.now(): raise InvalidToken("Token is expired") def _validate_bearer(self, token): @@ -147,7 +149,7 @@ def _validate_mac(self, mac_header): """Validate MAC authentication. Not implemented.""" if self.authentication_method & MAC == 0: raise InvalidToken("MAC authentication is not supported.") - mac_header = parse_qsl(mac_header.replace(",","&").replace('"', '')) + mac_header = parse_qsl(mac_header.replace(",", "&").replace('"', '')) mac_header = dict([(x[0].strip(), x[1].strip()) for x in mac_header]) for parameter in ["id", "nonce", "mac"]: if "parameter" not in mac_header: @@ -167,11 +169,11 @@ def _validate_mac(self, mac_header): raise InvalidRequest("Request does not contain a port.") nonce_timestamp, nonce_string = mac_header["nonce"].split(":") mac = sha256("\n".join([ - mac_header["nonce"], # The nonce value generated for the request - self.request.method.upper(), # The HTTP request method - "XXX", # The HTTP request-URI - self.request_hostname, # The hostname included in the HTTP request - self.request_port, # The port as included in the HTTP request + mac_header["nonce"], # The nonce value generated for the request + self.request.method.upper(), # The HTTP request method + "XXX", # The HTTP request-URI + self.request_hostname, # The hostname included in the HTTP request + self.request_port, # The port as included in the HTTP request bodyhash, ext])).hexdigest() raise NotImplementedError() @@ -188,7 +190,6 @@ def _validate_mac(self, mac_header): # define). # 3. Verify the scope and validity of the MAC credentials. - def _get_user(self): """The user associated with the valid access token. @@ -310,12 +311,12 @@ def error_response(self): """Returns a HttpResponse object of JSON error data.""" if self.error is not None: content = json.dumps({ - "error":getattr(self.error, "error", "invalid_request"), - "error_description":self.error.message}) + "error": getattr(self.error, "error", "invalid_request"), + "error_description": self.error.message}) else: content = ({ - "error":"invalid_request", - "error_description":"Invalid Request."}) + "error": "invalid_request", + "error_description": "Invalid Request."}) if self.callback is not None: content = "%s(%s);" % (self.callback, content) response = Authenticator.error_response( diff --git a/oauth2app/authorize.py b/oauth2app/authorize.py index 9c3fb15..255164b 100644 --- a/oauth2app/authorize.py +++ b/oauth2app/authorize.py @@ -3,15 +3,19 @@ """OAuth 2.0 Authorization""" +from django.http import HttpResponseRedirect +try: + from django.http.request import absolute_http_url_re +except ImportError: # try old location, Django 1.3 was defined here: + from django.http import absolute_http_url_re -from django.http import absolute_http_url_re, HttpResponse, HttpResponseRedirect, HttpResponseBadRequest from urllib import urlencode from .consts import ACCESS_TOKEN_EXPIRATION, REFRESHABLE from .consts import CODE, TOKEN, CODE_AND_TOKEN from .consts import AUTHENTICATION_METHOD, MAC, BEARER, MAC_KEY_LENGTH from .exceptions import OAuth2Exception from .lib.uri import add_parameters, add_fragments, normalize -from .models import Client, AccessRange, Code, AccessToken, KeyGenerator +from .models import Client, AccessRange, Code, AccessToken, key_gen class AuthorizationException(OAuth2Exception): @@ -73,8 +77,8 @@ class InvalidScope(AuthorizationException): RESPONSE_TYPES = { - "code":CODE, - "token":TOKEN} + "code": CODE, + "token": TOKEN} class Authorizer(object): @@ -98,12 +102,8 @@ class Authorizer(object): valid = False error = None - def __init__( - self, - scope=None, - authentication_method=AUTHENTICATION_METHOD, - refreshable=REFRESHABLE, - response_type=CODE): + def __init__(self, scope=None, authentication_method=AUTHENTICATION_METHOD, + refreshable=REFRESHABLE, response_type=CODE): if response_type not in [CODE, TOKEN, CODE_AND_TOKEN]: raise OAuth2Exception("Possible values for response_type" " are oauth2app.consts.CODE, oauth2app.consts.TOKEN, " @@ -251,8 +251,8 @@ def _query_string(self): raise UnvalidatedRequest("This request is invalid or has not" "been validated.") parameters = { - "response_type":self.response_type, - "client_id":self.client_id} + "response_type": self.response_type, + "client_id": self.client_id} if self.redirect_uri is not None: parameters["redirect_uri"] = self.redirect_uri if self.state is not None: @@ -301,7 +301,7 @@ def grant_redirect(self): if self.scope is not None: fragments['scope'] = ' '.join(self.scope) if self.authentication_method == MAC: - access_token.mac_key = KeyGenerator(MAC_KEY_LENGTH)() + access_token.mac_key = key_gen(MAC_KEY_LENGTH) fragments["mac_key"] = access_token.mac_key fragments["mac_algorithm"] = "hmac-sha-256" fragments["token_type"] = "mac" diff --git a/oauth2app/consts.py b/oauth2app/consts.py index 1004271..5c4798e 100644 --- a/oauth2app/consts.py +++ b/oauth2app/consts.py @@ -51,4 +51,5 @@ # Grants code style parameters. CODE = 2 # Grants both style parameters. -CODE_AND_TOKEN = CODE | TOKEN \ No newline at end of file +CODE_AND_TOKEN = CODE | TOKEN + diff --git a/oauth2app/decorators.py b/oauth2app/decorators.py index 2a7c9f0..fe4fe10 100644 --- a/oauth2app/decorators.py +++ b/oauth2app/decorators.py @@ -1,6 +1,8 @@ #-*- coding: utf-8 -*- -import functools, inspect + +import functools +import inspect def decorator(func): @@ -15,7 +17,7 @@ def isFuncArg(*args, **kw): if isinstance(func, type): def class_wrapper(*args, **kw): if isFuncArg(*args, **kw): - return func()(*args, **kw) # create class before usage + return func()(*args, **kw) # create class before usage return func(*args, **kw) class_wrapper.__name__ = func.__name__ class_wrapper.__module__ = func.__module__ diff --git a/oauth2app/lib/uri.py b/oauth2app/lib/uri.py index 9308cb5..1f2d747 100644 --- a/oauth2app/lib/uri.py +++ b/oauth2app/lib/uri.py @@ -7,8 +7,8 @@ from urlparse import urlparse, urlunparse, parse_qsl from urllib import urlencode from url_normalize import url_normalize - - + + def add_parameters(url, parameters): """Parses URL and appends parameters. @@ -21,7 +21,7 @@ def add_parameters(url, parameters): parts = list(urlparse(url)) parts[4] = urlencode(parse_qsl(parts[4]) + parameters.items()) return urlunparse(parts) - + def add_fragments(url, fragments): """Parses URL and appends fragments. @@ -35,8 +35,8 @@ def add_fragments(url, fragments): parts = list(urlparse(url)) parts[5] = urlencode(parse_qsl(parts[5]) + fragments.items()) return urlunparse(parts) - - + + def normalize(url): """Normalizes URL. @@ -46,4 +46,3 @@ def normalize(url): *Returns str*""" return url_normalize(url) - \ No newline at end of file diff --git a/oauth2app/models.py b/oauth2app/models.py index 978d832..bdaed2b 100644 --- a/oauth2app/models.py +++ b/oauth2app/models.py @@ -1,51 +1,21 @@ #-*- coding: utf-8 -*- - """OAuth 2.0 Django Models""" - -import time +import datetime from hashlib import sha512 from uuid import uuid4 + from django.db import models from django.contrib.auth.models import User + from .consts import CLIENT_KEY_LENGTH, CLIENT_SECRET_LENGTH from .consts import ACCESS_TOKEN_LENGTH, REFRESH_TOKEN_LENGTH from .consts import ACCESS_TOKEN_EXPIRATION, MAC_KEY_LENGTH, REFRESHABLE from .consts import CODE_KEY_LENGTH, CODE_EXPIRATION - -class TimestampGenerator(object): - """Callable Timestamp Generator that returns a UNIX time integer. - - **Kwargs:** - - * *seconds:* A integer indicating how many seconds in the future the - timestamp should be. *Default 0* - - *Returns int* - """ - def __init__(self, seconds=0): - self.seconds = seconds - - def __call__(self): - return int(time.time()) + self.seconds - - -class KeyGenerator(object): - """Callable Key Generator that returns a random keystring. - - **Args:** - - * *length:* A integer indicating how long the key should be. - - *Returns str* - """ - def __init__(self, length): - self.length = length - - def __call__(self): - return sha512(uuid4().hex).hexdigest()[0:self.length] +# helper to generate 512 bit sha key +key_gen = lambda length: sha512(uuid4().hex).hexdigest()[0:length] class Client(models.Model): @@ -71,18 +41,18 @@ class Client(models.Model): """ name = models.CharField(max_length=256) user = models.ForeignKey(User) + description = models.TextField(null=True, blank=True) - key = models.CharField( - unique=True, - max_length=CLIENT_KEY_LENGTH, - default=KeyGenerator(CLIENT_KEY_LENGTH), - db_index=True) - secret = models.CharField( - unique=True, - max_length=CLIENT_SECRET_LENGTH, - default=KeyGenerator(CLIENT_SECRET_LENGTH)) + # 30 character random string, default pass in callable + key = models.CharField(unique=True, max_length=CLIENT_KEY_LENGTH, + default=lambda: key_gen(CLIENT_KEY_LENGTH), db_index=True) + # 30 character random string, default pass in callable + secret = models.CharField(unique=True, max_length=CLIENT_SECRET_LENGTH, + default=lambda: key_gen(CLIENT_SECRET_LENGTH)) redirect_uri = models.URLField(null=True) + __unicode__ = lambda self: "%s" % self.name + class AccessRange(models.Model): """Stores access range data, also known as scope. @@ -101,6 +71,8 @@ class AccessRange(models.Model): key = models.CharField(unique=True, max_length=255, db_index=True) description = models.TextField(blank=True) + __unicode__ = lambda self: "%s" % self.key + class AccessToken(models.Model): """Stores access token data. @@ -126,32 +98,24 @@ class AccessToken(models.Model): """ client = models.ForeignKey(Client) user = models.ForeignKey(User) - token = models.CharField( - unique=True, - max_length=ACCESS_TOKEN_LENGTH, - default=KeyGenerator(ACCESS_TOKEN_LENGTH), - db_index=True) - refresh_token = models.CharField( - unique=True, - blank=True, - null=True, - max_length=REFRESH_TOKEN_LENGTH, - default=KeyGenerator(REFRESH_TOKEN_LENGTH), - db_index=True) - mac_key = models.CharField( - unique=True, - blank=True, - null=True, - max_length=MAC_KEY_LENGTH, - default=None) - issue = models.PositiveIntegerField( - editable=False, - default=TimestampGenerator()) - expire = models.PositiveIntegerField( - default=TimestampGenerator(ACCESS_TOKEN_EXPIRATION)) + # random string representing access key token + token = models.CharField(unique=True, max_length=ACCESS_TOKEN_LENGTH, + default=lambda: key_gen(ACCESS_TOKEN_LENGTH), db_index=True) + refresh_token = models.CharField(unique=True, blank=True, null=True, db_index=True, + max_length=REFRESH_TOKEN_LENGTH, default=lambda: key_gen(REFRESH_TOKEN_LENGTH)) + mac_key = models.CharField(unique=True, blank=True, null=True, + max_length=MAC_KEY_LENGTH, default=None) + # auto_now_add defaults to now() when created + issue = models.DateTimeField(auto_now_add=True, editable=False) + # default now + ACCESS_TOKEN_EXPIRATION, need to pass callable function + expire = models.DateTimeField(editable=False, default=lambda: + datetime.datetime.now() + datetime.timedelta(seconds=ACCESS_TOKEN_EXPIRATION)) + scope = models.ManyToManyField(AccessRange) refreshable = models.BooleanField(default=REFRESHABLE) + __unicode__ = lambda self: "%s (%s)" % (self.client, self.user) + class Code(models.Model): """Stores authorization code data. @@ -174,16 +138,14 @@ class Code(models.Model): """ client = models.ForeignKey(Client) user = models.ForeignKey(User) - key = models.CharField( - unique=True, - max_length=CODE_KEY_LENGTH, - default=KeyGenerator(CODE_KEY_LENGTH), - db_index=True) - issue = models.PositiveIntegerField( - editable=False, - default=TimestampGenerator()) - expire = models.PositiveIntegerField( - default=TimestampGenerator(CODE_EXPIRATION)) + key = models.CharField(unique=True, max_length=CODE_KEY_LENGTH, + default=lambda: key_gen(CODE_KEY_LENGTH), db_index=True) + # auto_now_add defaults to now() when created + issue = models.DateTimeField(auto_now_add=True, editable=False) + # default now + ACCESS_TOKEN_EXPIRATION, need to pass callable function + expire = models.DateTimeField(editable=False, default=lambda: + datetime.datetime.now() + datetime.timedelta(seconds=CODE_EXPIRATION)) + redirect_uri = models.URLField(null=True) scope = models.ManyToManyField(AccessRange) diff --git a/oauth2app/token.py b/oauth2app/token.py index dc34079..9559130 100644 --- a/oauth2app/token.py +++ b/oauth2app/token.py @@ -3,20 +3,23 @@ """OAuth 2.0 Token Generation""" - +from datetime import datetime, timedelta from base64 import b64encode + from django.http import HttpResponse from django.contrib.auth import authenticate from django.views.decorators.csrf import csrf_exempt -try: import simplejson as json -except ImportError: import json +try: + import simplejson as json +except ImportError: + import json from .exceptions import OAuth2Exception from .consts import ACCESS_TOKEN_EXPIRATION, REFRESH_TOKEN_LENGTH, ACCESS_TOKEN_LENGTH from .consts import AUTHENTICATION_METHOD, MAC, BEARER, MAC_KEY_LENGTH from .consts import REFRESHABLE from .lib.uri import normalize -from .models import Client, AccessRange, Code, AccessToken, TimestampGenerator -from .models import KeyGenerator +from .models import Client, AccessRange, Code, AccessToken +from .models import key_gen class AccessTokenException(OAuth2Exception): @@ -109,11 +112,7 @@ class TokenGenerator(object): error = None request = None - def __init__( - self, - scope=None, - authentication_method=AUTHENTICATION_METHOD, - refreshable=REFRESHABLE): + def __init__(self, scope=None, authentication_method=AUTHENTICATION_METHOD, refreshable=REFRESHABLE): self.refreshable = refreshable if authentication_method not in [BEARER, MAC]: raise OAuth2Exception("Possible values for authentication_method" @@ -235,8 +234,7 @@ def _validate_authorization_code(self): self.code = Code.objects.get(key=self.code_key) except Code.DoesNotExist: raise InvalidRequest('No such code: %s' % self.code_key) - now = TimestampGenerator()() - if self.code.expire < now: + if self.code.expire < datetime.now(): raise InvalidGrant("Provided code is expired") self.scope = set([x.key for x in self.code.scope.all()]) if self.redirect_uri is None: @@ -372,7 +370,7 @@ def _get_authorization_code_token(self): client=self.client, refreshable=self.refreshable) if self.authentication_method == MAC: - access_token.mac_key = KeyGenerator(MAC_KEY_LENGTH)() + access_token.mac_key = key_gen(MAC_KEY_LENGTH) access_ranges = AccessRange.objects.filter(key__in=self.scope) if self.scope else [] access_token.scope = access_ranges access_token.save() @@ -386,7 +384,7 @@ def _get_password_token(self): client=self.client, refreshable=self.refreshable) if self.authentication_method == MAC: - access_token.mac_key = KeyGenerator(MAC_KEY_LENGTH)() + access_token.mac_key = key_gen(MAC_KEY_LENGTH) access_ranges = AccessRange.objects.filter(key__in=self.scope) if self.scope else [] access_token.scope = access_ranges access_token.save() @@ -394,9 +392,9 @@ def _get_password_token(self): def _get_refresh_token(self): """Generate an access token after refresh authorization.""" - self.access_token.token = KeyGenerator(ACCESS_TOKEN_LENGTH)() - self.access_token.refresh_token = KeyGenerator(REFRESH_TOKEN_LENGTH)() - self.access_token.expire = TimestampGenerator(ACCESS_TOKEN_EXPIRATION)() + self.access_token.token = key_gen(ACCESS_TOKEN_LENGTH) + self.access_token.refresh_token = key_gen(REFRESH_TOKEN_LENGTH) + self.access_token.expire = datetime.now() + timedelta(seconds=ACCESS_TOKEN_EXPIRATION) access_ranges = AccessRange.objects.filter(key__in=self.scope) if self.scope else [] self.access_token.scope = access_ranges self.access_token.save() @@ -404,12 +402,10 @@ def _get_refresh_token(self): def _get_client_credentials_token(self): """Generate an access token after client_credentials authorization.""" - access_token = AccessToken.objects.create( - user=self.client.user, - client=self.client, - refreshable=self.refreshable) + access_token = AccessToken.objects.create(user=self.client.user, + client=self.client, refreshable=self.refreshable) if self.authentication_method == MAC: - access_token.mac_key = KeyGenerator(MAC_KEY_LENGTH)() + access_token.mac_key = key_gen(MAC_KEY_LENGTH) access_ranges = AccessRange.objects.filter(key__in=self.scope) if self.scope else [] self.access_token.scope = access_ranges self.access_token.save()