Skip to content

Commit

Permalink
Tiny big refactoring.
Browse files Browse the repository at this point in the history
  • Loading branch information
idlesign committed Dec 19, 2011
1 parent 9901099 commit 6b1ae95
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 99 deletions.
2 changes: 1 addition & 1 deletion oauthost/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin

from models import Client, RedirectionEndpoint, AuthorizationCode, Scope, Token
from oauthost.models import Client, RedirectionEndpoint, AuthorizationCode, Scope, Token


class AuthorizationCodeInlineAdmin(admin.TabularInline):
Expand Down
101 changes: 101 additions & 0 deletions oauthost/auth_handlers.py
@@ -0,0 +1,101 @@
from datetime import datetime

from django.core.exceptions import ObjectDoesNotExist
from django.template import loader, RequestContext
from django.http import HttpResponse
from django.utils.translation import ugettext_lazy as _


class BearerAuthHandler():
"""Handles Bearer token authentication calls.
SPEC: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer
"""

_token = None
_request = None
_response = None
_error = None
_scope = None

def __init__(self, request, scope):
self._request = request
self._scope = scope

self.fetch_token()
self.validate_token()
self.prepare_response()

def fetch_token(self):
token = None

# Authorization Request Header Field
authorization_method = self._request.META.get('Authorization')
if authorization_method is not None:
auth_method_type, auth_method_value = authorization_method.split(' ', 1)
if auth_method_type == 'Bearer':
token = auth_method_value
else:
# Form-Encoded Body Parameter or URI Query Parameter
token = self._request.REQUEST.get('access_token')

if not token:
self._error = 'invalid_request'
else:
self._token = token

def validate_token(self):

if self._token is None:
self._error = 'invalid_token'
return False

from oauthost.models import Token
try:
token = Token.objects.get(access_token=self._token)
except ObjectDoesNotExist:
self._error = 'invalid_token'
return False

# If token found is granted to all the different token type.
if token.access_token_type != 'bearer':
self._error = 'invalid_token'
return False

# Token has expired.
if token.expires_at <= datetime.now():
self._error = 'invalid_token'
return False

# If target scope is defined, let's verify that the token has access to it.
if self._scope is not None:
if not token.scopes.filter(identifier=self._scope).count():
self._error = 'insufficient_scope'
return False

return True

def prepare_response(self):

if self._error is not None:
from oauthost.config import OAUTHOST_TEMPLATE_RESTRICTED

errors = {
'invalid_request': (400, 'Request is malformed. Check request parameters validity.'),
'invalid_token': (401, 'Given access token is invalid'),
'insufficient_scope': (403, 'Access token grants no access to required scope.')
}

current_error = errors[self._error]
additional_params = {
'error': self._error, 'error_description': current_error[1]
}
additional_params = ',' . join( [ '%s="%s"' % (i[0], i[1]) for i in additional_params.items() ] )
context = RequestContext(self._request)
self._response = HttpResponse(content=loader.render_to_string(OAUTHOST_TEMPLATE_RESTRICTED,
{'oauthost_title': _('Access Restricted')}, context), status=current_error[0])
self._response['WWW-Authenticate'] = 'Bearer %s' % additional_params

def response(self):
return self._response
29 changes: 18 additions & 11 deletions oauthost/auth_views.py
Expand Up @@ -11,9 +11,9 @@
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _

from models import Client, AuthorizationCode, Token
from utils import *
from config import *
from oauthost.models import Client, AuthorizationCode, Token
from oauthost.utils import *
from oauthost.config import *


@login_required
Expand All @@ -34,7 +34,7 @@ def endpoint_authorize(request):
# security mechanism when sending requests to the authorization endpoint.
if not request.is_secure() and not settings.DEBUG:
# Insecure connections are only available in debug mode.
return ep_auth_response_error_page(request, _('OAuth requires secure connection to be established.'))
return ep_auth_response_error_page(request, _('OAuth requires secure connection to be established.'), 403)

if request.POST.get('auth_decision') is None:
# User has made no decision on auth confirmation yet.
Expand All @@ -60,7 +60,7 @@ def endpoint_authorize(request):
LOGGER.error('Invalid client ID supplied. Value "%s" was sent from IP "%s".' % (client_id, get_remote_ip(request)))
return ep_auth_response_error_page(request, _('Invalid client ID is supplied.'))

# TODO There should be at least one redirection URI associated with a client. URI validity should be checked while such association is made.
# TODO There should be at least one redirection URI associated with a client. URI validity should be checked while such an association is made.
registered_uris = [url[0] for url in client.redirectionendpoint_set.values_list('uri')]

# Check redirection URI validity.
Expand Down Expand Up @@ -176,7 +176,7 @@ def endpoint_token(request):
# security mechanism when sending requests to the token endpoint.
if not request.is_secure() and not settings.DEBUG:
# Insecure connections are only available in debug mode.
return ep_auth_response_error_page(request, _('OAuth requires secure connection to be established.'))
return ep_auth_response_error_page(request, _('OAuth requires secure connection to be established.'), 403)

input_params = filter_input_params(request.POST)

Expand All @@ -186,6 +186,7 @@ def endpoint_token(request):

token_obj_params = {}
error_out_headers = {}
client = None
client_id = None
client_secret = None

Expand All @@ -210,13 +211,19 @@ def endpoint_token(request):
client_secret = input_params.get('client_secret')

if client_id is not None:
supposed_client = Client.objects.get(identifier=client_id)
try:
client = Client.objects.get(identifier=client_id)
except ObjectDoesNotExist:
client = None

# SPEC: A public client that was not issued a client password MAY use
# the "client_id" request parameter to identify itself when sending requests
# to the token endpoint.
if supposed_client.password.strip() == '' or supposed_client.password == client_secret:
client = supposed_client
else:
if client is not None:
if client.password.strip() != '' and client.password != client_secret:
client = None

if client is None:
return ep_token_reponse_error('invalid_client', 'Unable to authenticate client by its credentials.', 401, error_out_headers)

# Calculate token expiration datetime.
Expand Down Expand Up @@ -317,7 +324,7 @@ def endpoint_token(request):
if grant_type == 'client_credentials':
del(output_params['refresh_token'])

# TODO Some auth methods require additional parameters to be passed.
# TODO Some auth methods require additional parameters to be passed as spec puts it.
additional_params = {}

return ep_token_response(output_params)
6 changes: 4 additions & 2 deletions oauthost/config.py
@@ -1,18 +1,20 @@
import logging

from django.utils.translation import ugettext_lazy as _
from oauthost.auth_handlers import BearerAuthHandler

LOGGER = logging.getLogger('django.oauthost')

REGISTRY_EP_AUTH_RESPONSE_TYPE = ['code', 'token']
REGISTRY_EP_TOKEN_GRANT_TYPE = ['authorization_code', 'password', 'client_credentials', 'refresh_token']

# Someday here might be something more than bare Bearer.
TOKEN_TYPE_BEARER = 'bearer'
REGISTRY_TOKEN_TYPE = {
(TOKEN_TYPE_BEARER, _('Bearer')),
(TOKEN_TYPE_BEARER, 'Bearer', BearerAuthHandler),
}

OAUTHOST_TEMPLATE_AUTHORIZE = 'oauthost/authorize.html'
OAUTHOST_TEMPLATE_AUTHORIZE_ERROR = 'oauthost/authorize_error.html'
OAUTHOST_TEMPLATE_AUTHORIZE_PROCEED = 'oauthost/authorize_proceed.html'
OAUTHOST_TEMPLATE_FORBIDDEN = 'oauthost/forbidden.html'
OAUTHOST_TEMPLATE_RESTRICTED = 'oauthost/restricted.html'
10 changes: 2 additions & 8 deletions oauthost/decorators.py
Expand Up @@ -2,7 +2,7 @@

from django.utils.decorators import available_attrs

from utils import check_token, forbidden_error_response
from oauthost.utils import auth_handler_response


def oauth_required(scope=None, scope_auto=False):
Expand All @@ -25,13 +25,7 @@ def wrapper(request, *args, **kwargs):
'app_name': view_function.__module__.split('.')[0],
'view_name': view_function.__name__}

if not check_token(request, scope=target_scope):
# For now we just use generic error page no matter what
# token type is used and what that's type spec tells us to do.
# We are evil enough, I confirm.
# Yet, it may change some day.
return forbidden_error_response(request)
return view_function(request, *args, **kwargs)
return auth_handler_response(request, scope=target_scope) or view_function(request, *args, **kwargs)
return wrapper

return decorated_view
4 changes: 2 additions & 2 deletions oauthost/models.py
Expand Up @@ -5,7 +5,7 @@
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User

from config import *
from oauthost.config import *


class Scope(models.Model):
Expand Down Expand Up @@ -127,7 +127,7 @@ class Token(models.Model):
expires_at = models.DateTimeField(_('Expires at'), help_text=_('Time when this token expires.'), null=True, blank=True)
access_token = models.CharField(_('Access Token'), max_length=32, help_text=_('Token to be used to access resources.'), unique=True)
refresh_token = models.CharField(_('Refresh Token'), max_length=32, help_text=_('Token to be used to refresh access token.'), unique=True, null=True, blank=True)
access_token_type = models.CharField(_('Type'), max_length=100, help_text=_('Access token type client uses to apply the appropriate authorization method.'), choices=REGISTRY_TOKEN_TYPE, default=TOKEN_TYPE_BEARER)
access_token_type = models.CharField(_('Type'), max_length=100, help_text=_('Access token type client uses to apply the appropriate authorization method.'), choices=[(t[0], t[1]) for t in REGISTRY_TOKEN_TYPE], default=TOKEN_TYPE_BEARER)
user = models.ForeignKey(User, verbose_name=_('User'), help_text=_('The user token is issued for.'), null=True, blank=True)
client = models.ForeignKey(Client, verbose_name=_('Client'), help_text=_('The client application token is issued for.'))
code = models.ForeignKey(AuthorizationCode, verbose_name=_('Code'), help_text=_('Authorization code used to generate this token.'), null=True, blank=True)
Expand Down
2 changes: 1 addition & 1 deletion oauthost/templates/common/base.html
Expand Up @@ -22,7 +22,7 @@
<div id="container">
<div id="dialog">
<div id="heading">
<h1>{{ oauthost_title }}</h1>
<h1>{% block oauthost_title %}{{ oauthost_title }}{% endblock %}</h1>
</div>
<div id="contents">
{% block oauthost_contents %}{% endblock %}
Expand Down
4 changes: 4 additions & 0 deletions oauthost/templates/oauthost/restricted.html
@@ -0,0 +1,4 @@
{% extends 'common/base.html' %}{% load i18n %}
{% block oauthost_contents %}
<p>{% blocktrans %}Access to this resource is restricted. Please provide appropriate credentials with the request to proceed.{% endblocktrans %}</p>
{% endblock %}
23 changes: 18 additions & 5 deletions oauthost/tests.py
Expand Up @@ -2,8 +2,9 @@
from django.utils import simplejson as json
from django.test.client import Client as TestClient
from django.contrib.auth.models import User
from django.conf import settings

from models import AuthorizationCode, Token, Client, RedirectionEndpoint
from oauthost.models import AuthorizationCode, Token, Client, RedirectionEndpoint


URL_TOKEN = '/token/'
Expand Down Expand Up @@ -38,6 +39,12 @@ class EndpointTokenCheck(TestCase):

def test_grant_authorization_code(self):

# Secure connection check
settings.DEBUG = False
resp = self.client.get(URL_TOKEN, {})
self.assertEqual(resp.status_code, 403)
settings.DEBUG = True

resp = self.client.post(URL_TOKEN, {'grant_type': 'a'})
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.content_json['error'], 'unsupported_grant_type')
Expand Down Expand Up @@ -100,7 +107,6 @@ def test_grant_authorization_code(self):
self.assertTrue('access_token' in resp.content_json)
self.assertTrue('refresh_token' in resp.content_json)
self.assertTrue('token_type' in resp.content_json)
self.assertTrue('expires_in' in resp.content_json)

# An additional call for code issues token and code invalidation.
resp = self.client.post(URL_TOKEN, {'grant_type': 'authorization_code', 'code': '1234567',
Expand All @@ -115,6 +121,7 @@ class EndpointAuthorizeCheck(TestCase):
client_class = OAuthostCLient

def test_auth(self):

# User is not logged in.
resp = self.client.get(URL_AUTHORIZE, {'client_id': '100'})
self.assertEqual(resp.status_code, 302)
Expand All @@ -126,6 +133,12 @@ def test_auth(self):
# Logging the user in.
self.client.login(username='Fred', password='12345')

# Secure connection check
settings.DEBUG = False
resp = self.client.get(URL_AUTHORIZE, {})
self.assertEqual(resp.status_code, 403)
settings.DEBUG = True

# Missing client id.
resp = self.client.get(URL_AUTHORIZE, {'response_type': 'code'})
self.assertEqual(resp.status_code, 400)
Expand Down Expand Up @@ -239,7 +252,6 @@ def test_authorization_code_unsafe(self):
self.assertTrue('access_token' in resp.content_json)
self.assertTrue('refresh_token' in resp.content_json)
self.assertTrue('token_type' in resp.content_json)
self.assertTrue('expires_in' in resp.content_json)

def test_authorization_code_http_basic(self):

Expand Down Expand Up @@ -286,7 +298,6 @@ def test_authorization_code_http_basic(self):
self.assertTrue('access_token' in resp.content_json)
self.assertTrue('refresh_token' in resp.content_json)
self.assertTrue('token_type' in resp.content_json)
self.assertTrue('expires_in' in resp.content_json)

def test_password_http_basic(self):

Expand Down Expand Up @@ -342,7 +353,6 @@ def test_password_http_basic(self):
self.assertTrue('access_token' in resp.content_json)
self.assertTrue('refresh_token' not in resp.content_json)
self.assertTrue('token_type' in resp.content_json)
self.assertTrue('expires_in' in resp.content_json)

access_token = resp.content_json['access_token']
token = Token.objects.get(access_token=access_token)
Expand Down Expand Up @@ -408,3 +418,6 @@ def test_refresh_token_http_basic(self):

token_updated = Token.objects.get(access_token=resp.content_json['access_token'])
self.assertNotEqual(date_issued, token_updated.date_issued)


# TODO Add tests for Bearer auth.

0 comments on commit 6b1ae95

Please sign in to comment.