commit
6f2204f78f
30 changed files with 653 additions and 227 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -10,3 +10,5 @@ src/
|
||||||
.venv
|
.venv
|
||||||
.idea
|
.idea
|
||||||
docs/_build/
|
docs/_build/
|
||||||
|
.eggs/
|
||||||
|
.python-version
|
||||||
|
|
|
@ -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
|
||||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
==================
|
==================
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
60
oidc_provider/migrations/0017_auto_20160811_1954.py
Normal file
60
oidc_provider/migrations/0017_auto_20160811_1954.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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):
|
||||||
|
|
||||||
|
|
|
@ -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'
|
|
|
@ -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)),
|
||||||
)
|
]
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
|
46
oidc_provider/tests/test_claims.py
Normal file
46
oidc_provider/tests/test_claims.py
Normal 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'
|
||||||
|
}
|
||||||
|
)
|
16
oidc_provider/tests/test_commands.py
Normal file
16
oidc_provider/tests/test_commands.py
Normal 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())
|
|
@ -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())
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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'),
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
114
runtests.py
Normal 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:])
|
5
setup.py
5
setup.py
|
@ -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
12
tox.ini
|
@ -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]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue