Skip to content

Commit

Permalink
Merge branch 'master' into fixes/exodoc
Browse files Browse the repository at this point in the history
* master: (29 commits)
  caffeinehit#56 - Uncomment an assertion in scope tests. It was commented out whilst adding support for Django 1.6.
  Fix get_access_token failing on single tracker mode after token refresh (caffeinehit#70).
  Removing django 1.3 from tox and travis-ci matrices since it's no longer officially supported.
  Fixing silly bug in test wrapper's django version detection.
  caffeinehit#55 - Update travis-ci config to include Django 1.6.
  Django 1.6 detection for test wrapper script.
  caffeinehit#55 - Removing another unnecessary import.
  caffeinehit#55 - Removing unnecessary imports.
  caffeinehit#55 Adding Django 1.6 to tox coverage matrix.
  caffeinehit#55 - Fallback to simplejson in older versions of Django.
  Fixes caffeinehit#55 - Handle non-list/non-tuple form field values for scope introduced in Django 1.6
  caffeinehit#51 caffeinehit#53 - Support for model serialization/deserialization. This addresses the issues in Django 1.6 involving new session storage backend behavior.
  Fix caffeinehit#26 by checking for invalid data before attempting access.
  Use `constants` instead of going directly through settings when invalidating tokens and grants. This also replaces 'clean' with 'delete' for the new setting to be more clear as to what's being done under the hood.
  implement OAUTH_CLEAN_EXPIRED, clean as you go
  Adding a License section to README.
  Fixes caffeinehit#32 - Add `token_type` to access token response to conform to section 4.2.2 of the OAuth 2.0 specification.
  caffeinehit#29 - Remove trailing slash that was causing installs to fail on Windows.
  Addressing caffeinehit#28 by replacing the BSD license reference with MIT to match what's in the repo.
  Bumping version to 0.2.7-dev to avoid ambiguity in an established package.
  ...

Conflicts:
	provider/__init__.py
	provider/oauth2/views.py
  • Loading branch information
Yann Savary committed Nov 20, 2014
2 parents c100eac + 6b5bc0d commit 14c846b
Show file tree
Hide file tree
Showing 21 changed files with 574 additions and 58 deletions.
3 changes: 1 addition & 2 deletions .travis.yml
@@ -1,8 +1,8 @@
language: python
env:
- DJANGO="django>=1.3,<1.4"
- DJANGO="django>=1.4,<1.5"
- DJANGO="django>=1.5,<1.6"
- DJANGO="django>=1.6,<1.7"
python:
- "2.6"
- "2.7"
Expand All @@ -12,4 +12,3 @@ install:
- pip install -q $DJANGO --use-mirrors -U
- python setup.py develop
script: ./test.sh

2 changes: 1 addition & 1 deletion MANIFEST.in
Expand Up @@ -2,4 +2,4 @@ include LICENSE
include README.rst
recursive-include provider/templates *.html
recursive-include provider/templates *.txt
recursive-include provider/ *json
recursive-include provider *json
7 changes: 6 additions & 1 deletion README.rst
Expand Up @@ -4,8 +4,13 @@ django-oauth2-provider
.. image:: https://travis-ci.org/caffeinehit/django-oauth2-provider.png?branch=master

*django-oauth2-provider* is a Django application that provides
customizable OAuth2\_ authentication for your Django projects.
customizable OAuth2\-authentication for your Django projects.

`Documentation <http://readthedocs.org/docs/django-oauth2-provider/en/latest/>`_

`Help <https://groups.google.com/d/forum/django-oauth2-provider>`_

License
=======

*django-oauth2-provider* is released under the MIT License. Please see the LICENSE file for details.
14 changes: 11 additions & 3 deletions docs/api.rst
Expand Up @@ -28,17 +28,25 @@

:settings: `OAUTH_EXPIRE_DELTA`
:default: `datetime.timedelta(days=365)`

The time to expiry for access tokens as outlined in :rfc:`4.2.2` and
:rfc:`5.1`.

.. attribute:: EXPIRE_CODE_DELTA

:settings: `OAUTH_EXPIRE_CODE_DELTA`
:default: `datetime.timedelta(seconds=10*60)`

The time to expiry for an authorization code grant as outlined in :rfc:`4.1.2`.


.. attribute:: DELETE_EXPIRED

:settings: `OAUTH_DELETE_EXPIRED`
:default: `False`

To remove expired tokens immediately instead of letting them persist, set
to `True`.

.. attribute:: ENFORCE_SECURE

:settings: `OAUTH_ENFORCE_SECURE`
Expand Down
2 changes: 1 addition & 1 deletion provider/__init__.py
@@ -1,2 +1,2 @@
__version__ = "0.2.6.epyx.4"
__version__ = "0.2.7.epyx.5"

8 changes: 8 additions & 0 deletions provider/constants.py
Expand Up @@ -11,6 +11,8 @@

RESPONSE_TYPE_CHOICES = getattr(settings, 'OAUTH_RESPONSE_TYPE_CHOICES', ("code", "token"))

TOKEN_TYPE = 'Bearer'

READ = 1 << 1
WRITE = 1 << 2
READ_WRITE = READ | WRITE
Expand All @@ -25,8 +27,14 @@

EXPIRE_DELTA = getattr(settings, 'OAUTH_EXPIRE_DELTA', timedelta(days=365))

# Expiry delta for public clients (which typically have shorter lived tokens)
EXPIRE_DELTA_PUBLIC = getattr(settings, 'OAUTH_EXPIRE_DELTA_PUBLIC', timedelta(days=30))

EXPIRE_CODE_DELTA = getattr(settings, 'OAUTH_EXPIRE_CODE_DELTA', timedelta(seconds=10 * 60))

# Remove expired tokens immediately instead of letting them persist.
DELETE_EXPIRED = getattr(settings, 'OAUTH_DELETE_EXPIRED', False)

ENFORCE_SECURE = getattr(settings, 'OAUTH_ENFORCE_SECURE', False)
ENFORCE_CLIENT_SECURE = getattr(settings, 'OAUTH_ENFORCE_CLIENT_SECURE', True)

Expand Down
6 changes: 6 additions & 0 deletions provider/oauth2/__init__.py
@@ -0,0 +1,6 @@
import backends
import forms
import managers
import models
import urls
import views
23 changes: 22 additions & 1 deletion provider/oauth2/backends.py
@@ -1,5 +1,5 @@
from ..utils import now
from .forms import ClientAuthForm
from .forms import ClientAuthForm, PublicPasswordGrantForm
from .models import AccessToken


Expand Down Expand Up @@ -61,6 +61,27 @@ def authenticate(self, request=None):
return None


class PublicPasswordBackend(object):
"""
Backend that tries to authenticate a client using username, password
and client ID. This is only available in specific circumstances:
- grant_type is "password"
- client.client_type is 'public'
"""

def authenticate(self, request=None):
if request is None:
return None

form = PublicPasswordGrantForm(request.REQUEST)

if form.is_valid():
return form.cleaned_data.get('client')

return None


class AccessTokenBackend(object):
"""
Authenticate a user via access token and client object.
Expand Down
34 changes: 33 additions & 1 deletion provider/oauth2/forms.py
Expand Up @@ -56,8 +56,13 @@ def to_python(self, value):
if not value:
return []

# New in Django 1.6: value may come in as a string.
# Instead of raising an `OAuthValidationError`, try to parse and
# ultimately return an empty list if nothing remains -- this will
# eventually raise an `OAuthValidationError` in `validate` where
# it should be anyways.
if not isinstance(value, (list, tuple)):
raise OAuthValidationError({'error': 'invalid_request'})
value = value.split(' ')

# Split values into list
return u' '.join([smart_unicode(val) for val in value]).split(u' ')
Expand Down Expand Up @@ -301,3 +306,30 @@ def clean(self):

data['user'] = user
return data


class PublicPasswordGrantForm(PasswordGrantForm):
client_id = forms.CharField(required=True)
grant_type = forms.CharField(required=True)

def clean_grant_type(self):
grant_type = self.cleaned_data.get('grant_type')

if grant_type != 'password':
raise OAuthValidationError({'error': 'invalid_grant'})

return grant_type

def clean(self):
data = super(PublicPasswordGrantForm, self).clean()

try:
client = Client.objects.get(client_id=data.get('client_id'))
except Client.DoesNotExist:
raise OAuthValidationError({'error': 'invalid_client'})

if client.client_type != 1: # public
raise OAuthValidationError({'error': 'invalid_client'})

data['client'] = client
return data
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models


class Migration(SchemaMigration):

def forwards(self, orm):
# Adding index on 'AccessToken', fields ['token']
db.create_index('oauth2_accesstoken', ['token'])


def backwards(self, orm):
# Removing index on 'AccessToken', fields ['token']
db.delete_index('oauth2_accesstoken', ['token'])


models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'oauth2.accesstoken': {
'Meta': {'object_name': 'AccessToken'},
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 8, 8, 0, 0)'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'scope': ('django.db.models.fields.IntegerField', [], {'default': '2'}),
'token': ('django.db.models.fields.CharField', [], {'default': "'ab8c8bcd91e8750462b631516b60b0b95dffe1f4'", 'max_length': '255', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'oauth2.client': {
'Meta': {'object_name': 'Client'},
'client_id': ('django.db.models.fields.CharField', [], {'default': "'0a8e54e38c024606ba0a'", 'max_length': '255'}),
'client_secret': ('django.db.models.fields.CharField', [], {'default': "'e53ddb9736f9eea65100885a1b20fb5f2bb0fb4d'", 'max_length': '255'}),
'client_type': ('django.db.models.fields.IntegerField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'redirect_uri': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'oauth2_client'", 'null': 'True', 'to': "orm['auth.User']"})
},
'oauth2.grant': {
'Meta': {'object_name': 'Grant'},
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
'code': ('django.db.models.fields.CharField', [], {'default': "'5e0ca84e98678a3b55b8901e85a20f995672aea2'", 'max_length': '255'}),
'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 8, 8, 0, 0)'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'redirect_uri': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'scope': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'oauth2.refreshtoken': {
'Meta': {'object_name': 'RefreshToken'},
'access_token': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'refresh_token'", 'unique': 'True', 'to': "orm['oauth2.AccessToken']"}),
'client': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oauth2.Client']"}),
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'token': ('django.db.models.fields.CharField', [], {'default': "'32e9eb7edda764ba8f752ae49223d42acea7cb88'", 'max_length': '255'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}

complete_apps = ['oauth2']
47 changes: 42 additions & 5 deletions provider/oauth2/models.py
Expand Up @@ -8,9 +8,8 @@
from django.conf import settings
from .. import constants
from ..constants import CLIENT_TYPES
from ..utils import short_token, long_token, get_token_expiry
from ..utils import get_code_expiry
from ..utils import now
from ..utils import now, short_token, long_token, get_code_expiry
from ..utils import get_token_expiry, serialize_instance, deserialize_instance
from .managers import AccessTokenManager

try:
Expand Down Expand Up @@ -49,6 +48,39 @@ class Client(models.Model):
def __unicode__(self):
return self.name

def get_default_token_expiry(self):
public = (self.client_type == 1)
return get_token_expiry(public)

def serialize(self):
return dict(user=serialize_instance(self.user),
name=self.name,
url=self.url,
redirect_uri=self.redirect_uri,
client_id=self.client_id,
client_secret=self.client_secret,
client_type=self.client_type)

@classmethod
def deserialize(cls, data):
if not data:
return None

kwargs = {}

# extract values that we care about
for field in cls._meta.fields:
name = field.name
val = data.get(field.name, None)

# handle relations
if val and field.rel:
val = deserialize_instance(field.rel.to, val)

kwargs[name] = val

return cls(**kwargs)


class Grant(models.Model):
"""
Expand Down Expand Up @@ -98,9 +130,9 @@ class AccessToken(models.Model):
expiry
"""
user = models.ForeignKey(AUTH_USER_MODEL)
token = models.CharField(max_length=255, default=long_token)
token = models.CharField(max_length=255, default=long_token, db_index=True)
client = models.ForeignKey(Client)
expires = models.DateTimeField(default=get_token_expiry)
expires = models.DateTimeField()
scope = models.IntegerField(default=constants.SCOPES[0][0],
choices=constants.SCOPES)

Expand All @@ -109,6 +141,11 @@ class AccessToken(models.Model):
def __unicode__(self):
return self.token

def save(self, *args, **kwargs):
if not self.expires:
self.expires = self.client.get_default_token_expiry()
super(AccessToken, self).save(*args, **kwargs)

def get_expire_delta(self, reference=None):
"""
Return the number of seconds until this token expires.
Expand Down

0 comments on commit 14c846b

Please sign in to comment.