Merge pull request #6 from juanifioren/v0.3.x

Fetch lates version
This commit is contained in:
Wojciech Bartosiak 2016-09-06 12:20:57 +02:00 committed by GitHub
commit 6f2204f78f
30 changed files with 653 additions and 227 deletions

2
.gitignore vendored
View file

@ -10,3 +10,5 @@ src/
.venv .venv
.idea .idea
docs/_build/ docs/_build/
.eggs/
.python-version

View file

@ -7,12 +7,14 @@ env:
- DJANGO=1.7 - DJANGO=1.7
- DJANGO=1.8 - DJANGO=1.8
- DJANGO=1.9 - DJANGO=1.9
- DJANGO=1.10
matrix: matrix:
exclude: exclude:
- python: "3.5" - python: "3.5"
env: DJANGO=1.7 env: DJANGO=1.7
install: install:
- pip install -q django==$DJANGO - pip install tox coveralls
- pip install -e .
script: script:
- PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test oidc_provider --settings=oidc_provider.tests.app.settings - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-django${DJANGO//[.]/}
after_success:
- coveralls

View file

@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
### [Unreleased] ### [Unreleased]
### [0.3.7] - 2016-08-31
##### Added
- Support for Django 1.10.
- Initial translation files (ES, FR).
- Support for at_hash parameter.
##### Fixed
- Empty address dict in userinfo response.
### [0.3.6] - 2016-07-07 ### [0.3.6] - 2016-07-07
##### Changed ##### Changed

View file

@ -1,4 +1,4 @@
include LICENSE include LICENSE
include README.rst include README.md
recursive-include oidc_provider/templates * recursive-include oidc_provider/templates *
recursive-include oidc_provider/tests/templates * recursive-include oidc_provider/tests/templates *

View file

@ -2,7 +2,7 @@
[![Python Versions](https://img.shields.io/pypi/pyversions/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider) [![Python Versions](https://img.shields.io/pypi/pyversions/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider)
[![PyPI Versions](https://img.shields.io/pypi/v/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider) [![PyPI Versions](https://img.shields.io/pypi/v/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider)
[![Travis](https://travis-ci.org/juanifioren/django-oidc-provider.svg?branch=master)](https://travis-ci.org/juanifioren/django-oidc-provider) [![Travis](https://travis-ci.org/juanifioren/django-oidc-provider.svg?branch=develop)](https://travis-ci.org/juanifioren/django-oidc-provider)
[![PyPI Downloads](https://img.shields.io/pypi/dm/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider) [![PyPI Downloads](https://img.shields.io/pypi/dm/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider)
## About OpenID ## About OpenID
@ -24,4 +24,4 @@ We love contributions, so please feel free to fix bugs, improve things, provide
* Fork the project. * Fork the project.
* Make your feature addition or bug fix. * Make your feature addition or bug fix.
* Add tests for it inside `oidc_provider/tests`. Then run all and ensure everything is OK (read docs for how to test in all envs). * Add tests for it inside `oidc_provider/tests`. Then run all and ensure everything is OK (read docs for how to test in all envs).
* Send pull request to the specific version branch. * Send pull request to the `develop` branch.

View file

@ -106,7 +106,7 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
html_theme = 'sphinx_rtd_theme' html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the

View file

@ -18,12 +18,11 @@ Use `tox <https://pypi.python.org/pypi/tox>`_ for running tests in each of the e
# Run all tests. # Run all tests.
$ tox $ tox
# Run a particular test file with Python 2.7 and Django 1.9. # Run with Python 2.7 and Django 1.9.
$ tox -e py27-django19 oidc_provider.tests.test_authorize_endpoint $ tox -e py27-django19
If you have a Django project properly configured with the package. Then just run tests as normal:: # Run single test file.
$ python runtests.py oidc_provider.tests.test_authorize_endpoint
$ python manage.py test --settings oidc_provider.tests.app.settings oidc_provider
Also tests run on every commit to the project, we use `travis <https://travis-ci.org/juanifioren/django-oidc-provider/>`_ for this. Also tests run on every commit to the project, we use `travis <https://travis-ci.org/juanifioren/django-oidc-provider/>`_ for this.

View file

@ -7,7 +7,7 @@ Requirements
============ ============
* Python: ``2.7`` ``3.4`` ``3.5`` * Python: ``2.7`` ``3.4`` ``3.5``
* Django: ``1.7`` ``1.8`` ``1.9`` * Django: ``1.7`` ``1.8`` ``1.9`` ``1.10``
Quick Installation Quick Installation
================== ==================

View file

@ -60,8 +60,11 @@ class ScopeClaims(object):
if value is None or value == '': if value is None or value == '':
del aux_dic[key] del aux_dic[key]
elif type(value) is dict: elif type(value) is dict:
aux_dic[key] = self._clean_dic(value) cleaned_dict = self._clean_dic(value)
if not cleaned_dict:
del aux_dic[key]
continue
aux_dic[key] = cleaned_dict
return aux_dic return aux_dic
@classmethod @classmethod

View file

@ -1,3 +1,4 @@
from datetime import timedelta
import logging import logging
try: try:
from urllib import urlencode from urllib import urlencode
@ -8,10 +9,22 @@ except ImportError:
from django.utils import timezone from django.utils import timezone
from oidc_provider.lib.claims import StandardScopeClaims from oidc_provider.lib.claims import StandardScopeClaims
from oidc_provider.lib.errors import * from oidc_provider.lib.errors import (
from oidc_provider.lib.utils.params import * AuthorizeError,
from oidc_provider.lib.utils.token import * ClientIdError,
from oidc_provider.models import * RedirectUriError,
)
from oidc_provider.lib.utils.params import Params
from oidc_provider.lib.utils.token import (
create_code,
create_id_token,
create_token,
encode_id_token,
)
from oidc_provider.models import (
Client,
UserConsent,
)
from oidc_provider import settings from oidc_provider import settings
@ -121,35 +134,41 @@ class AuthorizeEndpoint(object):
query_params['state'] = self.params.state if self.params.state else '' query_params['state'] = self.params.state if self.params.state else ''
elif self.grant_type == 'implicit': elif self.grant_type == 'implicit':
# We don't need id_token if it's an OAuth2 request.
if self.is_authentication:
id_token_dic = create_id_token(
user=self.request.user,
aud=self.client.client_id,
nonce=self.params.nonce,
request=self.request)
query_fragment['id_token'] = encode_id_token(id_token_dic, self.client)
else:
id_token_dic = {}
token = create_token( token = create_token(
user=self.request.user, user=self.request.user,
client=self.client, client=self.client,
id_token_dic=id_token_dic,
scope=self.params.scope) scope=self.params.scope)
# Store the token.
token.save()
query_fragment['token_type'] = 'bearer'
# TODO: Create setting 'OIDC_TOKEN_EXPIRE'.
query_fragment['expires_in'] = 60 * 10
# Check if response_type is an OpenID request with value 'id_token token' # Check if response_type is an OpenID request with value 'id_token token'
# or it's an OAuth2 Implicit Flow request. # or it's an OAuth2 Implicit Flow request.
if self.params.response_type in ['id_token token', 'token']: if self.params.response_type in ['id_token token', 'token']:
query_fragment['access_token'] = token.access_token query_fragment['access_token'] = token.access_token
# We don't need id_token if it's an OAuth2 request.
if self.is_authentication:
kwargs = {
"user": self.request.user,
"aud": self.client.client_id,
"nonce": self.params.nonce,
"request": self.request
}
# Include at_hash when access_token is being returned.
if 'access_token' in query_fragment:
kwargs['at_hash'] = token.at_hash
id_token_dic = create_id_token(**kwargs)
query_fragment['id_token'] = encode_id_token(id_token_dic, self.client)
token.id_token = id_token_dic
else:
id_token_dic = {}
# Store the token.
token.id_token = id_token_dic
token.save()
query_fragment['token_type'] = 'bearer'
# TODO: Create setting 'OIDC_TOKEN_EXPIRE'.
query_fragment['expires_in'] = 60 * 10
query_fragment['state'] = self.params.state if self.params.state else '' query_fragment['state'] = self.params.state if self.params.state else ''
except Exception as error: except Exception as error:

View file

@ -9,10 +9,20 @@ except ImportError:
from django.http import JsonResponse from django.http import JsonResponse
from oidc_provider.lib.errors import * from oidc_provider.lib.errors import (
from oidc_provider.lib.utils.params import * TokenError,
from oidc_provider.lib.utils.token import * )
from oidc_provider.models import * from oidc_provider.lib.utils.params import Params
from oidc_provider.lib.utils.token import (
create_id_token,
create_token,
encode_id_token,
)
from oidc_provider.models import (
Client,
Code,
Token,
)
from oidc_provider import settings from oidc_provider import settings
@ -131,21 +141,22 @@ class TokenEndpoint(object):
return self.create_refresh_response_dic() return self.create_refresh_response_dic()
def create_code_response_dic(self): def create_code_response_dic(self):
token = create_token(
user=self.code.user,
client=self.code.client,
scope=self.code.scope)
if self.code.is_authentication: if self.code.is_authentication:
id_token_dic = create_id_token( id_token_dic = create_id_token(
user=self.code.user, user=self.code.user,
aud=self.client.client_id, aud=self.client.client_id,
nonce=self.code.nonce, nonce=self.code.nonce,
at_hash=token.at_hash,
request=self.request, request=self.request,
) )
else: else:
id_token_dic = {} id_token_dic = {}
token.id_token = id_token_dic
token = create_token(
user=self.code.user,
client=self.code.client,
id_token_dic=id_token_dic,
scope=self.code.scope)
# Store the token. # Store the token.
token.save() token.save()
@ -164,22 +175,23 @@ class TokenEndpoint(object):
return dic return dic
def create_refresh_response_dic(self): def create_refresh_response_dic(self):
token = create_token(
user=self.token.user,
client=self.token.client,
scope=self.token.scope)
# If the Token has an id_token it's an Authentication request. # If the Token has an id_token it's an Authentication request.
if self.token.id_token: if self.token.id_token:
id_token_dic = create_id_token( id_token_dic = create_id_token(
user=self.token.user, user=self.token.user,
aud=self.client.client_id, aud=self.client.client_id,
nonce=None, nonce=None,
at_hash=token.at_hash,
request=self.request, request=self.request,
) )
else: else:
id_token_dic = {} id_token_dic = {}
token.id_token = id_token_dic
token = create_token(
user=self.token.user,
client=self.token.client,
id_token_dic=id_token_dic,
scope=self.token.scope)
# Store the token. # Store the token.
token.save() token.save()

View file

@ -9,11 +9,15 @@ from jwkest.jwk import SYMKey
from jwkest.jws import JWS from jwkest.jws import JWS
from oidc_provider.lib.utils.common import get_issuer from oidc_provider.lib.utils.common import get_issuer
from oidc_provider.models import * from oidc_provider.models import (
Code,
RSAKey,
Token,
)
from oidc_provider import settings from oidc_provider import settings
def create_id_token(user, aud, nonce, request=None): def create_id_token(user, aud, nonce, at_hash=None, request=None):
""" """
Receives a user object and aud (audience). Receives a user object and aud (audience).
Then creates the id_token dictionary. Then creates the id_token dictionary.
@ -44,6 +48,9 @@ def create_id_token(user, aud, nonce, request=None):
if nonce: if nonce:
dic['nonce'] = str(nonce) dic['nonce'] = str(nonce)
if at_hash:
dic['at_hash'] = at_hash
processing_hook = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK') processing_hook = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK')
if isinstance(processing_hook, (list, tuple)): if isinstance(processing_hook, (list, tuple)):
@ -79,7 +86,7 @@ def encode_id_token(payload, client):
return _jws.sign_compact(keys) return _jws.sign_compact(keys)
def create_token(user, client, id_token_dic, scope): def create_token(user, client, scope, id_token_dic=None):
""" """
Create and populate a Token object. Create and populate a Token object.
@ -90,6 +97,7 @@ def create_token(user, client, id_token_dic, scope):
token.client = client token.client = client
token.access_token = uuid.uuid4().hex token.access_token = uuid.uuid4().hex
if id_token_dic is not None:
token.id_token = id_token_dic token.id_token = id_token_dic
token.refresh_token = uuid.uuid4().hex token.refresh_token = uuid.uuid4().hex

View file

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-08-11 19:54
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('oidc_provider', '0016_userconsent_and_verbosenames'),
]
operations = [
migrations.AlterField(
model_name='client',
name='_redirect_uris',
field=models.TextField(default='', help_text='Enter each URI on a new line.', verbose_name='Redirect URIs'),
),
migrations.AlterField(
model_name='client',
name='client_secret',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='Client SECRET'),
),
migrations.AlterField(
model_name='client',
name='client_type',
field=models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], default='confidential', help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.', max_length=30, verbose_name='Client Type'),
),
migrations.AlterField(
model_name='client',
name='name',
field=models.CharField(default='', max_length=100, verbose_name='Name'),
),
migrations.AlterField(
model_name='client',
name='response_type',
field=models.CharField(choices=[('code', 'code (Authorization Code Flow)'), ('id_token', 'id_token (Implicit Flow)'), ('id_token token', 'id_token token (Implicit Flow)')], max_length=30, verbose_name='Response Type'),
),
migrations.AlterField(
model_name='code',
name='_scope',
field=models.TextField(default='', verbose_name='Scopes'),
),
migrations.AlterField(
model_name='code',
name='nonce',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='Nonce'),
),
migrations.AlterField(
model_name='token',
name='_scope',
field=models.TextField(default='', verbose_name='Scopes'),
),
migrations.AlterField(
model_name='userconsent',
name='_scope',
field=models.TextField(default='', verbose_name='Scopes'),
),
]

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from hashlib import md5 import base64
import binascii
from hashlib import md5, sha256
import json import json
from django.db import models from django.db import models
@ -24,6 +26,7 @@ JWT_ALGS = [
('RS256', 'RS256'), ('RS256', 'RS256'),
] ]
class Client(models.Model): class Client(models.Model):
name = models.CharField(max_length=100, default='', verbose_name=_(u'Name')) name = models.CharField(max_length=100, default='', verbose_name=_(u'Name'))
@ -49,8 +52,10 @@ class Client(models.Model):
def redirect_uris(): def redirect_uris():
def fget(self): def fget(self):
return self._redirect_uris.splitlines() return self._redirect_uris.splitlines()
def fset(self, value): def fset(self, value):
self._redirect_uris = '\n'.join(value) self._redirect_uris = '\n'.join(value)
return locals() return locals()
redirect_uris = property(**redirect_uris()) redirect_uris = property(**redirect_uris())
@ -69,8 +74,10 @@ class BaseCodeTokenModel(models.Model):
def scope(): def scope():
def fget(self): def fget(self):
return self._scope.split() return self._scope.split()
def fset(self, value): def fset(self, value):
self._scope = ' '.join(value) self._scope = ' '.join(value)
return locals() return locals()
scope = property(**scope()) scope = property(**scope())
@ -105,11 +112,15 @@ class Token(BaseCodeTokenModel):
access_token = models.CharField(max_length=255, unique=True, verbose_name=_(u'Access Token')) access_token = models.CharField(max_length=255, unique=True, verbose_name=_(u'Access Token'))
refresh_token = models.CharField(max_length=255, unique=True, null=True, verbose_name=_(u'Refresh Token')) refresh_token = models.CharField(max_length=255, unique=True, null=True, verbose_name=_(u'Refresh Token'))
_id_token = models.TextField(verbose_name=_(u'ID Token')) _id_token = models.TextField(verbose_name=_(u'ID Token'))
def id_token(): def id_token():
def fget(self): def fget(self):
return json.loads(self._id_token) return json.loads(self._id_token)
def fset(self, value): def fset(self, value):
self._id_token = json.dumps(value) self._id_token = json.dumps(value)
return locals() return locals()
id_token = property(**id_token()) id_token = property(**id_token())
@ -117,6 +128,18 @@ class Token(BaseCodeTokenModel):
verbose_name = _(u'Token') verbose_name = _(u'Token')
verbose_name_plural = _(u'Tokens') verbose_name_plural = _(u'Tokens')
@property
def at_hash(self):
# @@@ d-o-p only supports 256 bits (change this if that changes)
hashed_access_token = sha256(
self.access_token.encode('ascii')
).hexdigest().encode('ascii')
return base64.urlsafe_b64encode(
binascii.unhexlify(
hashed_access_token[:len(hashed_access_token) // 2]
)
).rstrip(b'=').decode('ascii')
class UserConsent(BaseCodeTokenModel): class UserConsent(BaseCodeTokenModel):

View file

@ -1,61 +0,0 @@
import os
from datetime import timedelta
DEBUG = False
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
SITE_ID = 1
MIDDLEWARE_CLASSES = [
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
]
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'oidc_provider': {
'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
},
},
}
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
'oidc_provider',
]
SECRET_KEY = 'this-should-be-top-secret'
ROOT_URLCONF = 'oidc_provider.tests.app.urls'
TEMPLATE_DIRS = [
'oidc_provider/tests/templates',
]
USE_TZ = True
# OIDC Provider settings.
SITE_URL = 'http://localhost:8000'
OIDC_USERINFO = 'oidc_provider.tests.app.utils.userinfo'

View file

@ -1,10 +1,10 @@
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.conf.urls import patterns, include, url from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.views.generic import TemplateView from django.views.generic import TemplateView
urlpatterns = patterns('', urlpatterns = [
url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'), url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'),
url(r'^accounts/login/$', auth_views.login, {'template_name': 'accounts/login.html'}, name='login'), url(r'^accounts/login/$', auth_views.login, {'template_name': 'accounts/login.html'}, name='login'),
url(r'^accounts/logout/$', auth_views.logout, {'template_name': 'accounts/logout.html'}, name='logout'), url(r'^accounts/logout/$', auth_views.logout, {'template_name': 'accounts/logout.html'}, name='logout'),
@ -12,4 +12,4 @@ urlpatterns = patterns('',
url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
) ]

View file

@ -1,4 +1,3 @@
import os
import random import random
import string import string
try: try:
@ -8,7 +7,10 @@ except ImportError:
from django.contrib.auth.models import User from django.contrib.auth.models import User
from oidc_provider.models import * from oidc_provider.models import (
Client,
Code,
)
FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d' FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d'

View file

@ -1,25 +1,33 @@
try: try:
from urllib.parse import unquote, urlencode from urllib.parse import urlencode
except ImportError: except ImportError:
from urllib import unquote, urlencode from urllib import urlencode
try:
from urllib.parse import parse_qs, urlsplit
except ImportError:
from urlparse import parse_qs, urlsplit
import uuid import uuid
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.management import call_command from django.core.management import call_command
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import RequestFactory from django.test import RequestFactory
from django.test import TestCase from django.test import TestCase
from jwkest.jwt import JWT
from oidc_provider import settings from oidc_provider import settings
from oidc_provider.models import * from oidc_provider.tests.app.utils import (
from oidc_provider.tests.app.utils import * create_fake_user,
from oidc_provider.views import * create_fake_client,
FAKE_CODE_CHALLENGE,
is_code_valid,
)
from oidc_provider.views import AuthorizeView
class AuthorizationCodeFlowTestCase(TestCase): class AuthorizationCodeFlowTestCase(TestCase):
""" """
Test cases for Authorize Endpoint using Authorization Code Flow. Test cases for Authorize Endpoint using Code Flow.
""" """
def setUp(self): def setUp(self):
@ -28,7 +36,6 @@ class AuthorizationCodeFlowTestCase(TestCase):
self.user = create_fake_user() self.user = create_fake_user()
self.client = create_fake_client(response_type='code') self.client = create_fake_client(response_type='code')
self.client_public = create_fake_client(response_type='code', is_public=True) self.client_public = create_fake_client(response_type='code', is_public=True)
self.client_implicit = create_fake_client(response_type='id_token token')
self.state = uuid.uuid4().hex self.state = uuid.uuid4().hex
self.nonce = uuid.uuid4().hex self.nonce = uuid.uuid4().hex
@ -52,7 +59,6 @@ class AuthorizationCodeFlowTestCase(TestCase):
return response return response
def test_missing_parameters(self): def test_missing_parameters(self):
""" """
If the request fails due to a missing, invalid, or mismatching If the request fails due to a missing, invalid, or mismatching
@ -148,7 +154,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
for key, value in iter(to_check.items()): for key, value in iter(to_check.items()):
is_input_ok = input_html.format(key, value) in response.content.decode('utf-8') is_input_ok = input_html.format(key, value) in response.content.decode('utf-8')
self.assertEqual(is_input_ok, True, self.assertEqual(is_input_ok, True,
msg='Hidden input for "'+key+'" fails.') msg='Hidden input for "' + key + '" fails.')
def test_user_consent_response(self): def test_user_consent_response(self):
""" """
@ -270,43 +276,6 @@ class AuthorizationCodeFlowTestCase(TestCase):
self.assertEqual('Request for Permission' in response.content.decode('utf-8'), True) self.assertEqual('Request for Permission' in response.content.decode('utf-8'), True)
def test_implicit_missing_nonce(self):
"""
The `nonce` parameter is REQUIRED if you use the Implicit Flow.
"""
data = {
'client_id': self.client_implicit.client_id,
'response_type': self.client_implicit.response_type,
'redirect_uri': self.client_implicit.default_redirect_uri,
'scope': 'openid email',
'state': self.state,
}
response = self._auth_request('get', data, is_user_authenticated=True)
self.assertEqual('#error=invalid_request' in response['Location'], True)
def test_implicit_access_token_response(self):
"""
Unlike the Authorization Code flow, in which the client makes
separate requests for authorization and for an access token, the client
receives the access token as the result of the authorization request.
"""
data = {
'client_id': self.client_implicit.client_id,
'redirect_uri': self.client_implicit.default_redirect_uri,
'response_type': self.client_implicit.response_type,
'scope': 'openid email',
'state': self.state,
'nonce': self.nonce,
'allow': 'Accept',
}
response = self._auth_request('post', data, is_user_authenticated=True)
self.assertEqual('access_token' in response['Location'], True)
def test_prompt_parameter(self): def test_prompt_parameter(self):
""" """
Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
@ -331,3 +300,168 @@ class AuthorizationCodeFlowTestCase(TestCase):
# An error is returned if the Client does not have pre-configured consent for the requested Claims. # An error is returned if the Client does not have pre-configured consent for the requested Claims.
self.assertEqual('interaction_required' in response['Location'], True) self.assertEqual('interaction_required' in response['Location'], True)
class AuthorizationImplicitFlowTestCase(TestCase):
"""
Test cases for Authorization Endpoint using Implicit Flow.
"""
def setUp(self):
call_command('creatersakey')
self.factory = RequestFactory()
self.user = create_fake_user()
self.client = create_fake_client(response_type='id_token token')
self.client_public = create_fake_client(response_type='id_token token', is_public=True)
self.client_no_access = create_fake_client(response_type='id_token')
self.client_public_no_access = create_fake_client(response_type='id_token', is_public=True)
self.state = uuid.uuid4().hex
self.nonce = uuid.uuid4().hex
def _auth_request(self, method, data={}, is_user_authenticated=False):
url = reverse('oidc_provider:authorize')
if method.lower() == 'get':
query_str = urlencode(data).replace('+', '%20')
if query_str:
url += '?' + query_str
request = self.factory.get(url)
elif method.lower() == 'post':
request = self.factory.post(url, data=data)
else:
raise Exception('Method unsupported for an Authorization Request.')
# Simulate that the user is logged.
request.user = self.user if is_user_authenticated else AnonymousUser()
response = AuthorizeView.as_view()(request)
return response
def test_missing_nonce(self):
"""
The `nonce` parameter is REQUIRED if you use the Implicit Flow.
"""
data = {
'client_id': self.client.client_id,
'response_type': self.client.response_type,
'redirect_uri': self.client.default_redirect_uri,
'scope': 'openid email',
'state': self.state,
}
response = self._auth_request('get', data, is_user_authenticated=True)
self.assertEqual('#error=invalid_request' in response['Location'], True)
def test_id_token_token_response(self):
"""
Implicit client requesting `id_token token` receives both id token
and access token as the result of the authorization request.
"""
data = {
'client_id': self.client.client_id,
'redirect_uri': self.client.default_redirect_uri,
'response_type': self.client.response_type,
'scope': 'openid email',
'state': self.state,
'nonce': self.nonce,
'allow': 'Accept',
}
response = self._auth_request('post', data, is_user_authenticated=True)
self.assertIn('access_token', response['Location'])
self.assertIn('id_token', response['Location'])
# same for public client
data['client_id'] = self.client_public.client_id,
data['redirect_uri'] = self.client_public.default_redirect_uri,
data['response_type'] = self.client_public.response_type,
response = self._auth_request('post', data, is_user_authenticated=True)
self.assertIn('access_token', response['Location'])
self.assertIn('id_token', response['Location'])
def test_id_token_response(self):
"""
Implicit client requesting `id_token` receives
only an id token as the result of the authorization request.
"""
data = {
'client_id': self.client_no_access.client_id,
'redirect_uri': self.client_no_access.default_redirect_uri,
'response_type': self.client_no_access.response_type,
'scope': 'openid email',
'state': self.state,
'nonce': self.nonce,
'allow': 'Accept',
}
response = self._auth_request('post', data, is_user_authenticated=True)
self.assertNotIn('access_token', response['Location'])
self.assertIn('id_token', response['Location'])
# same for public client
data['client_id'] = self.client_public_no_access.client_id,
data['redirect_uri'] = self.client_public_no_access.default_redirect_uri,
data['response_type'] = self.client_public_no_access.response_type,
response = self._auth_request('post', data, is_user_authenticated=True)
self.assertNotIn('access_token', response['Location'])
self.assertIn('id_token', response['Location'])
def test_id_token_token_at_hash(self):
"""
Implicit client requesting `id_token token` receives
`at_hash` in `id_token`.
"""
data = {
'client_id': self.client.client_id,
'redirect_uri': self.client.default_redirect_uri,
'response_type': self.client.response_type,
'scope': 'openid email',
'state': self.state,
'nonce': self.nonce,
'allow': 'Accept',
}
response = self._auth_request('post', data, is_user_authenticated=True)
self.assertIn('id_token', response['Location'])
# obtain `id_token` portion of Location
components = urlsplit(response['Location'])
fragment = parse_qs(components[4])
id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload()
self.assertIn('at_hash', id_token)
def test_id_token_at_hash(self):
"""
Implicit client requesting `id_token` should not receive
`at_hash` in `id_token`.
"""
data = {
'client_id': self.client_no_access.client_id,
'redirect_uri': self.client_no_access.default_redirect_uri,
'response_type': self.client_no_access.response_type,
'scope': 'openid email',
'state': self.state,
'nonce': self.nonce,
'allow': 'Accept',
}
response = self._auth_request('post', data, is_user_authenticated=True)
self.assertIn('id_token', response['Location'])
# obtain `id_token` portion of Location
components = urlsplit(response['Location'])
fragment = parse_qs(components[4])
id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload()
self.assertNotIn('at_hash', id_token)

View file

@ -0,0 +1,46 @@
from django.test import TestCase
from oidc_provider.lib.claims import ScopeClaims
from oidc_provider.tests.app.utils import create_fake_user
class ClaimsTestCase(TestCase):
def setUp(self):
self.user = create_fake_user()
self.scopes = ['openid', 'address', 'email', 'phone', 'profile']
self.scopeClaims = ScopeClaims(self.user, self.scopes)
def test_clean_dic(self):
""" assert that _clean_dic function returns a clean dictionnary
(no empty claims) """
dict_to_clean = {
'phone_number_verified': '',
'middle_name': '',
'name': 'John Doe',
'website': '',
'profile': '',
'family_name': 'Doe',
'birthdate': '',
'preferred_username': '',
'picture': '',
'zoneinfo': '',
'locale': '',
'gender': '',
'updated_at': '',
'address': {},
'given_name': 'John',
'email_verified': '',
'nickname': '',
'email': u'johndoe@example.com',
'phone_number': '',
}
clean_dict = self.scopeClaims._clean_dic(dict_to_clean)
self.assertEquals(
clean_dict,
{
'family_name': 'Doe',
'given_name': 'John',
'name': 'John Doe',
'email': u'johndoe@example.com'
}
)

View file

@ -0,0 +1,16 @@
from django.core.management import call_command
from django.test import TestCase
from django.utils.six import StringIO
class CommandsTest(TestCase):
def test_creatersakey_output(self):
out = StringIO()
call_command('creatersakey', stdout=out)
self.assertIn('RSA key successfully created', out.getvalue())
def test_makemigrations_output(self):
out = StringIO()
call_command('makemigrations', 'oidc_provider', stdout=out)
self.assertIn('No changes detected in app', out.getvalue())

View file

@ -1,12 +0,0 @@
from django.core.management import call_command
from django.test import TestCase, override_settings
from django.utils.six import StringIO
class CreateRSAKeyTest(TestCase):
@override_settings(BASE_DIR='/tmp')
def test_command_output(self):
out = StringIO()
call_command('creatersakey', stdout=out)
self.assertIn('RSA key successfully created', out.getvalue())

View file

@ -1,8 +1,7 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from oidc_provider.views import * from oidc_provider.tests.app.utils import create_fake_user
from oidc_provider.tests.app.utils import *
class UserInfoTestCase(TestCase): class UserInfoTestCase(TestCase):

View file

@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse
from django.test import RequestFactory from django.test import RequestFactory
from django.test import TestCase from django.test import TestCase
from oidc_provider.views import * from oidc_provider.views import ProviderInfoView
class ProviderInfoTestCase(TestCase): class ProviderInfoTestCase(TestCase):

View file

@ -1,3 +1,7 @@
from datetime import timedelta
import json
import uuid
from base64 import b64encode from base64 import b64encode
try: try:
from urllib.parse import urlencode from urllib.parse import urlencode
@ -5,15 +9,30 @@ except ImportError:
from urllib import urlencode from urllib import urlencode
from django.core.management import call_command from django.core.management import call_command
from django.core.urlresolvers import reverse
from django.test import RequestFactory, override_settings from django.test import RequestFactory, override_settings
from django.test import TestCase from django.test import TestCase
from django.utils import timezone
from jwkest.jwk import KEYS from jwkest.jwk import KEYS
from jwkest.jws import JWS
from jwkest.jwt import JWT from jwkest.jwt import JWT
from mock import patch from mock import patch
from oidc_provider.lib.utils.token import * from oidc_provider.lib.utils.token import create_code
from oidc_provider.tests.app.utils import * from oidc_provider.models import Token
from oidc_provider.views import * from oidc_provider.tests.app.utils import (
create_fake_user,
create_fake_client,
FAKE_CODE_CHALLENGE,
FAKE_CODE_VERIFIER,
FAKE_NONCE,
FAKE_RANDOM_STRING,
)
from oidc_provider.views import (
JwksView,
TokenView,
userinfo,
)
class TokenTestCase(TestCase): class TokenTestCase(TestCase):
@ -208,14 +227,14 @@ class TokenTestCase(TestCase):
response = TokenView.as_view()(request) response = TokenView.as_view()(request)
self.assertEqual(response.status_code == 405, True, self.assertEqual(response.status_code == 405, True,
msg=request.method+' request does not return a 405 status.') msg=request.method + ' request does not return a 405 status.')
request = self.factory.post(url) request = self.factory.post(url)
response = TokenView.as_view()(request) response = TokenView.as_view()(request)
self.assertEqual(response.status_code == 400, True, self.assertEqual(response.status_code == 400, True,
msg=request.method+' request does not return a 400 status.') msg=request.method + ' request does not return a 400 status.')
def test_client_authentication(self): def test_client_authentication(self):
""" """
@ -304,6 +323,21 @@ class TokenTestCase(TestCase):
self.assertEqual(id_token.get('nonce'), None) self.assertEqual(id_token.get('nonce'), None)
def test_id_token_contains_at_hash(self):
"""
If access_token is included, the id_token SHOULD contain an at_hash.
"""
code = self._create_code()
post_data = self._auth_code_post_data(code=code.code)
response = self._post_request(post_data)
response_dic = json.loads(response.content.decode('utf-8'))
id_token = JWT().unpack(response_dic['id_token'].encode('utf-8')).payload()
self.assertTrue(id_token.get('at_hash'))
def test_idtoken_sign_validation(self): def test_idtoken_sign_validation(self):
""" """
We MUST validate the signature of the ID Token according to JWS We MUST validate the signature of the ID Token according to JWS
@ -311,7 +345,7 @@ class TokenTestCase(TestCase):
the JOSE Header. the JOSE Header.
""" """
SIGKEYS = self._get_keys() SIGKEYS = self._get_keys()
RSAKEYS = [ k for k in SIGKEYS if k.kty == 'RSA' ] RSAKEYS = [k for k in SIGKEYS if k.kty == 'RSA']
code = self._create_code() code = self._create_code()

View file

@ -1,3 +1,5 @@
import json
from datetime import timedelta from datetime import timedelta
try: try:
from urllib.parse import urlencode from urllib.parse import urlencode
@ -9,9 +11,15 @@ from django.test import RequestFactory
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
from oidc_provider.lib.utils.token import * from oidc_provider.lib.utils.token import (
from oidc_provider.models import * create_id_token,
from oidc_provider.tests.app.utils import * create_token,
)
from oidc_provider.tests.app.utils import (
create_fake_user,
create_fake_client,
FAKE_NONCE,
)
from oidc_provider.views import userinfo from oidc_provider.views import userinfo

View file

@ -1,16 +1,14 @@
from django.conf.urls import patterns, include, url from django.conf.urls import url
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from oidc_provider.views import * from oidc_provider import views
urlpatterns = [ urlpatterns = [
url(r'^authorize/?$', views.AuthorizeView.as_view(), name='authorize'),
url(r'^token/?$', csrf_exempt(views.TokenView.as_view()), name='token'),
url(r'^userinfo/?$', csrf_exempt(views.userinfo), name='userinfo'),
url(r'^logout/?$', views.LogoutView.as_view(), name='logout'),
url(r'^authorize/?$', AuthorizeView.as_view(), name='authorize'), url(r'^\.well-known/openid-configuration/?$', views.ProviderInfoView.as_view(), name='provider_info'),
url(r'^token/?$', csrf_exempt(TokenView.as_view()), name='token'), url(r'^jwks/?$', views.JwksView.as_view(), name='jwks'),
url(r'^userinfo/?$', csrf_exempt(userinfo), name='userinfo'),
url(r'^logout/?$', LogoutView.as_view(), name='logout'),
url(r'^\.well-known/openid-configuration/?$', ProviderInfoView.as_view(), name='provider_info'),
url(r'^jwks/?$', JwksView.as_view(), name='jwks'),
] ]

View file

@ -1,3 +1,5 @@
import logging
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from django.contrib.auth.views import redirect_to_login, logout from django.contrib.auth.views import redirect_to_login, logout
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -9,9 +11,14 @@ from django.views.generic import View
from jwkest import long_to_base64 from jwkest import long_to_base64
from oidc_provider.lib.claims import StandardScopeClaims from oidc_provider.lib.claims import StandardScopeClaims
from oidc_provider.lib.endpoints.authorize import * from oidc_provider.lib.endpoints.authorize import AuthorizeEndpoint
from oidc_provider.lib.endpoints.token import * from oidc_provider.lib.endpoints.token import TokenEndpoint
from oidc_provider.lib.errors import * from oidc_provider.lib.errors import (
AuthorizeError,
ClientIdError,
RedirectUriError,
TokenError,
)
from oidc_provider.lib.utils.common import redirect, get_site_url, get_issuer from oidc_provider.lib.utils.common import redirect, get_site_url, get_issuer
from oidc_provider.lib.utils.oauth2 import protected_resource_view from oidc_provider.lib.utils.oauth2 import protected_resource_view
from oidc_provider.models import RESPONSE_TYPE_CHOICES, RSAKey from oidc_provider.models import RESPONSE_TYPE_CHOICES, RSAKey
@ -193,8 +200,8 @@ class ProviderInfoView(View):
# See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes # See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
dic['subject_types_supported'] = ['public'] dic['subject_types_supported'] = ['public']
dic['token_endpoint_auth_methods_supported'] = [ 'client_secret_post', dic['token_endpoint_auth_methods_supported'] = ['client_secret_post',
'client_secret_basic' ] 'client_secret_basic']
return JsonResponse(dic) return JsonResponse(dic)

114
runtests.py Normal file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env python
import os
import sys
import django
from django.conf import settings
DEFAULT_SETTINGS = dict(
DEBUG = False,
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
},
SITE_ID = 1,
MIDDLEWARE_CLASSES = [
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
],
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
],
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'oidc_provider': {
'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
},
},
},
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
'oidc_provider',
],
SECRET_KEY = 'this-should-be-top-secret',
ROOT_URLCONF = 'oidc_provider.tests.app.urls',
TEMPLATE_DIRS = [
'oidc_provider/tests/templates',
],
USE_TZ = True,
# OIDC Provider settings.
SITE_URL = 'http://localhost:8000',
OIDC_USERINFO = 'oidc_provider.tests.app.utils.userinfo',
)
def runtests(*test_args):
if not settings.configured:
settings.configure(**DEFAULT_SETTINGS)
django.setup()
parent = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, parent)
try:
from django.test.runner import DiscoverRunner
runner_class = DiscoverRunner
if not test_args:
test_args = ["oidc_provider.tests"]
except ImportError:
from django.test.simple import DjangoTestSuiteRunner
runner_class = DjangoTestSuiteRunner
if not test_args:
test_args = ["tests"]
failures = runner_class(verbosity=1, interactive=True, failfast=False).run_tests(test_args)
sys.exit(failures)
if __name__ == "__main__":
runtests(*sys.argv[1:])

View file

@ -7,7 +7,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
setup( setup(
name='django-oidc-provider', name='django-oidc-provider',
version='0.3.6', version='0.3.7',
packages=[ packages=[
'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints', 'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints',
'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/tests/app', 'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/tests/app',
@ -34,9 +34,10 @@ setup(
'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
], ],
test_suite='runtests.runtests',
tests_require=[ tests_require=[
'pyjwkest==1.1.0', 'pyjwkest==1.1.0',
'mock==1.3.0', 'mock==2.0.0',
], ],
install_requires=[ install_requires=[

12
tox.ini
View file

@ -1,21 +1,23 @@
[tox] [tox]
envlist= envlist=
clean,py{27,34}-django{17,18,19},py35-django{18,19},stats clean,
py27-django{17,18,19,110},
py34-django{17,18,19,110},
py35-django{18,19,110},
[testenv] [testenv]
deps = deps =
django17: django>=1.7,<1.8 django17: django>=1.7,<1.8
django18: django>=1.8,<1.9 django18: django>=1.8,<1.9
django19: django>=1.9,<2.0 django19: django>=1.9,<1.10
django110: django>=1.10,<1.11
coverage coverage
mock mock
commands = commands =
pip uninstall --yes django-oidc-provider coverage run setup.py test
pip install -e .
coverage run --omit=.tox/*,oidc_provider/tests/* {envbindir}/django-admin.py test {posargs:oidc_provider} --settings=oidc_provider.tests.app.settings
[testenv:clean] [testenv:clean]