Merge branch 'develop' into patch-1
This commit is contained in:
commit
2ed0d21f12
|
@ -5,7 +5,6 @@ python:
|
||||||
- "3.5"
|
- "3.5"
|
||||||
- "3.6"
|
- "3.6"
|
||||||
env:
|
env:
|
||||||
- DJANGO=1.7
|
|
||||||
- DJANGO=1.8
|
- DJANGO=1.8
|
||||||
- DJANGO=1.9
|
- DJANGO=1.9
|
||||||
- DJANGO=1.10
|
- DJANGO=1.10
|
||||||
|
@ -15,10 +14,6 @@ matrix:
|
||||||
exclude:
|
exclude:
|
||||||
- python: "2.7"
|
- python: "2.7"
|
||||||
env: DJANGO=2.0
|
env: DJANGO=2.0
|
||||||
- python: "3.5"
|
|
||||||
env: DJANGO=1.7
|
|
||||||
- python: "3.6"
|
|
||||||
env: DJANGO=1.7
|
|
||||||
install:
|
install:
|
||||||
- pip install tox coveralls
|
- pip install tox coveralls
|
||||||
script:
|
script:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Django OIDC Provider
|
# Django OpenID Connect Provider
|
||||||
|
|
||||||
[![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)
|
||||||
|
@ -11,7 +11,7 @@ OpenID Connect is a simple identity layer on top of the OAuth 2.0 protocol, whic
|
||||||
|
|
||||||
## About the package
|
## About the package
|
||||||
|
|
||||||
`django-oidc-provider` can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects.
|
`django-oidc-provider` can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect (and OAuth2) capabilities to your Django projects.
|
||||||
|
|
||||||
Support for Python 3 and 2. Also latest versions of django.
|
Support for Python 3 and 2. Also latest versions of django.
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,6 @@ Contents:
|
||||||
sections/signals
|
sections/signals
|
||||||
sections/examples
|
sections/examples
|
||||||
sections/contribute
|
sections/contribute
|
||||||
sections/contribute
|
|
||||||
sections/changelog
|
sections/changelog
|
||||||
..
|
..
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,11 @@ All notable changes to this project will be documented in this file.
|
||||||
Unreleased
|
Unreleased
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
* Added: token instrospection endpoint support (RFC7662).
|
||||||
|
* Added: request in password grant authenticate call.
|
||||||
|
* Changed: dropping support for Django versions before 1.8.
|
||||||
|
* Changed: pass token and request to OIDC_IDTOKEN_PROCESSING_HOOK.
|
||||||
|
|
||||||
0.6.0
|
0.6.0
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,12 @@ Requirements
|
||||||
============
|
============
|
||||||
|
|
||||||
* Python: ``2.7`` ``3.4`` ``3.5`` ``3.6``
|
* Python: ``2.7`` ``3.4`` ``3.5`` ``3.6``
|
||||||
* Django: ``1.7`` ``1.8`` ``1.9`` ``1.10`` ``1.11`` ``2.0``
|
* Django: ``1.8`` ``1.9`` ``1.10`` ``1.11`` ``2.0``
|
||||||
|
|
||||||
Quick Installation
|
Quick Installation
|
||||||
==================
|
==================
|
||||||
|
|
||||||
If you want to get started fast see our ``/example_project`` folder in your local installation. Or look at it `on github <https://github.com/juanifioren/django-oidc-provider/tree/v0.5.x/example_project>`_.
|
If you want to get started fast see our ``/example`` folder in your local installation. Or look at it `on github <https://github.com/juanifioren/django-oidc-provider/tree/master/example>`_.
|
||||||
|
|
||||||
Install the package using pip::
|
Install the package using pip::
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,13 @@ Used to add extra scopes specific for your app. OpenID Connect RP's will use sco
|
||||||
|
|
||||||
Read more about how to implement it in :ref:`scopesclaims` section.
|
Read more about how to implement it in :ref:`scopesclaims` section.
|
||||||
|
|
||||||
|
OIDC_IDTOKEN_INCLUDE_CLAIMS
|
||||||
|
==============================
|
||||||
|
|
||||||
|
OPTIONAL. ``bool``. If enabled, id_token will include standard claims of the user (email, first name, etc.).
|
||||||
|
|
||||||
|
Default is ``False``.
|
||||||
|
|
||||||
OIDC_IDTOKEN_EXPIRE
|
OIDC_IDTOKEN_EXPIRE
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
@ -81,12 +88,27 @@ Here you can add extra dictionary values specific for your app into id_token.
|
||||||
|
|
||||||
The ``list`` or ``tuple`` is useful when you want to set multiple hooks, i.e. one for permissions and second for some special field.
|
The ``list`` or ``tuple`` is useful when you want to set multiple hooks, i.e. one for permissions and second for some special field.
|
||||||
|
|
||||||
The function receives a ``id_token`` dictionary and ``user`` instance
|
The hook function receives following arguments:
|
||||||
and returns it with additional fields.
|
|
||||||
|
* ``id_token``: the ID token dictionary which contains at least the
|
||||||
|
basic claims (``iss``, ``sub``, ``aud``, ``exp``, ``iat``,
|
||||||
|
``auth_time``), but may also contain other claims. If several
|
||||||
|
processing hooks are configured, then the claims of the previous hook
|
||||||
|
are also present in the passed dictionary.
|
||||||
|
* ``user``: User object of the authenticating user,
|
||||||
|
* ``token``: the Token object created for the authentication request, and
|
||||||
|
* ``request``: Django request object of the authentication request.
|
||||||
|
|
||||||
|
The hook function should return the modified ID token as dictionary.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
It is a good idea to add ``**kwargs`` to the hook function argument
|
||||||
|
list so that the hook function will work even if new arguments are
|
||||||
|
added to the hook function call signature.
|
||||||
|
|
||||||
Default is::
|
Default is::
|
||||||
|
|
||||||
def default_idtoken_processing_hook(id_token, user):
|
def default_idtoken_processing_hook(id_token, user, token, request, **kwargs):
|
||||||
|
|
||||||
return id_token
|
return id_token
|
||||||
|
|
||||||
|
@ -103,12 +125,31 @@ Default is::
|
||||||
|
|
||||||
return str(user.id)
|
return str(user.id)
|
||||||
|
|
||||||
OIDC_IDTOKEN_INCLUDE_CLAIMS
|
OIDC_INTROSPECTION_PROCESSING_HOOK
|
||||||
==============================
|
==================================
|
||||||
|
|
||||||
OPTIONAL. ``bool``. If enabled, id_token will include standard claims of the user (email, first name, etc.).
|
OPTIONAL. ``str`` or ``(list, tuple)``.
|
||||||
|
|
||||||
Default is ``False``.
|
A string with the location of your function hook or ``list`` or ``tuple`` with hook functions.
|
||||||
|
Here you can add extra dictionary values specific to your valid response value for token introspection.
|
||||||
|
|
||||||
|
The function receives an ``introspection_response`` dictionary, a ``client`` instance and an ``id_token`` dictionary.
|
||||||
|
|
||||||
|
Default is::
|
||||||
|
|
||||||
|
def default_introspection_processing_hook(introspection_response, client, id_token):
|
||||||
|
|
||||||
|
return introspection_response
|
||||||
|
|
||||||
|
|
||||||
|
OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
OPTIONAL ``bool``
|
||||||
|
|
||||||
|
A flag which toggles whether the audience is matched against the client resource scope when calling the introspection endpoint.
|
||||||
|
|
||||||
|
Default is ``True``.
|
||||||
|
|
||||||
OIDC_SESSION_MANAGEMENT_ENABLE
|
OIDC_SESSION_MANAGEMENT_ENABLE
|
||||||
==============================
|
==============================
|
||||||
|
|
98
oidc_provider/lib/endpoints/introspection.py
Normal file
98
oidc_provider/lib/endpoints/introspection.py
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
|
|
||||||
|
from oidc_provider.lib.errors import TokenIntrospectionError
|
||||||
|
from oidc_provider.lib.utils.common import run_processing_hook
|
||||||
|
from oidc_provider.lib.utils.oauth2 import extract_client_auth
|
||||||
|
from oidc_provider.models import Token, Client
|
||||||
|
from oidc_provider import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
INTROSPECTION_SCOPE = 'token_introspection'
|
||||||
|
|
||||||
|
|
||||||
|
class TokenIntrospectionEndpoint(object):
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
self.request = request
|
||||||
|
self.params = {}
|
||||||
|
self.id_token = None
|
||||||
|
self.client = None
|
||||||
|
self._extract_params()
|
||||||
|
|
||||||
|
def _extract_params(self):
|
||||||
|
# Introspection only supports POST requests
|
||||||
|
self.params['token'] = self.request.POST.get('token')
|
||||||
|
client_id, client_secret = extract_client_auth(self.request)
|
||||||
|
self.params['client_id'] = client_id
|
||||||
|
self.params['client_secret'] = client_secret
|
||||||
|
|
||||||
|
def validate_params(self):
|
||||||
|
if not (self.params['client_id'] and self.params['client_secret']):
|
||||||
|
logger.debug('[Introspection] No client credentials provided')
|
||||||
|
raise TokenIntrospectionError()
|
||||||
|
if not self.params['token']:
|
||||||
|
logger.debug('[Introspection] No token provided')
|
||||||
|
raise TokenIntrospectionError()
|
||||||
|
try:
|
||||||
|
token = Token.objects.get(access_token=self.params['token'])
|
||||||
|
except Token.DoesNotExist:
|
||||||
|
logger.debug('[Introspection] Token does not exist: %s', self.params['token'])
|
||||||
|
raise TokenIntrospectionError()
|
||||||
|
if token.has_expired():
|
||||||
|
logger.debug('[Introspection] Token is not valid: %s', self.params['token'])
|
||||||
|
raise TokenIntrospectionError()
|
||||||
|
if not token.id_token:
|
||||||
|
logger.debug('[Introspection] Token not an authentication token: %s',
|
||||||
|
self.params['token'])
|
||||||
|
raise TokenIntrospectionError()
|
||||||
|
|
||||||
|
self.id_token = token.id_token
|
||||||
|
audience = self.id_token.get('aud')
|
||||||
|
if not audience:
|
||||||
|
logger.debug('[Introspection] No audience found for token: %s', self.params['token'])
|
||||||
|
raise TokenIntrospectionError()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.client = Client.objects.get(
|
||||||
|
client_id=self.params['client_id'],
|
||||||
|
client_secret=self.params['client_secret'])
|
||||||
|
except Client.DoesNotExist:
|
||||||
|
logger.debug('[Introspection] No valid client for id: %s',
|
||||||
|
self.params['client_id'])
|
||||||
|
raise TokenIntrospectionError()
|
||||||
|
if INTROSPECTION_SCOPE not in self.client.scope:
|
||||||
|
logger.debug('[Introspection] Client %s does not have introspection scope',
|
||||||
|
self.params['client_id'])
|
||||||
|
raise TokenIntrospectionError()
|
||||||
|
if settings.get('OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE') \
|
||||||
|
and audience not in self.client.scope:
|
||||||
|
logger.debug('[Introspection] Client %s does not audience scope %s',
|
||||||
|
self.params['client_id'], audience)
|
||||||
|
raise TokenIntrospectionError()
|
||||||
|
|
||||||
|
def create_response_dic(self):
|
||||||
|
response_dic = dict((k, self.id_token[k]) for k in ('sub', 'exp', 'iat', 'iss'))
|
||||||
|
response_dic['active'] = True
|
||||||
|
response_dic['client_id'] = self.id_token.get('aud')
|
||||||
|
response_dic['aud'] = self.client.client_id
|
||||||
|
|
||||||
|
response_dic = run_processing_hook(response_dic,
|
||||||
|
'OIDC_INTROSPECTION_PROCESSING_HOOK',
|
||||||
|
client=self.client,
|
||||||
|
id_token=self.id_token)
|
||||||
|
|
||||||
|
return response_dic
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def response(cls, dic, status=200):
|
||||||
|
"""
|
||||||
|
Create and return a response object.
|
||||||
|
"""
|
||||||
|
response = JsonResponse(dic, status=status)
|
||||||
|
response['Cache-Control'] = 'no-store'
|
||||||
|
response['Pragma'] = 'no-cache'
|
||||||
|
|
||||||
|
return response
|
|
@ -1,7 +1,7 @@
|
||||||
from base64 import b64decode, urlsafe_b64encode
|
import inspect
|
||||||
|
from base64 import urlsafe_b64encode
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
|
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
@ -10,6 +10,7 @@ from oidc_provider.lib.errors import (
|
||||||
TokenError,
|
TokenError,
|
||||||
UserAuthError,
|
UserAuthError,
|
||||||
)
|
)
|
||||||
|
from oidc_provider.lib.utils.oauth2 import extract_client_auth
|
||||||
from oidc_provider.lib.utils.token import (
|
from oidc_provider.lib.utils.token import (
|
||||||
create_id_token,
|
create_id_token,
|
||||||
create_token,
|
create_token,
|
||||||
|
@ -26,6 +27,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TokenEndpoint(object):
|
class TokenEndpoint(object):
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.params = {}
|
self.params = {}
|
||||||
|
@ -33,7 +35,7 @@ class TokenEndpoint(object):
|
||||||
self._extract_params()
|
self._extract_params()
|
||||||
|
|
||||||
def _extract_params(self):
|
def _extract_params(self):
|
||||||
client_id, client_secret = self._extract_client_auth()
|
client_id, client_secret = extract_client_auth(self.request)
|
||||||
|
|
||||||
self.params['client_id'] = client_id
|
self.params['client_id'] = client_id
|
||||||
self.params['client_secret'] = client_secret
|
self.params['client_secret'] = client_secret
|
||||||
|
@ -49,29 +51,6 @@ class TokenEndpoint(object):
|
||||||
self.params['username'] = self.request.POST.get('username', '')
|
self.params['username'] = self.request.POST.get('username', '')
|
||||||
self.params['password'] = self.request.POST.get('password', '')
|
self.params['password'] = self.request.POST.get('password', '')
|
||||||
|
|
||||||
def _extract_client_auth(self):
|
|
||||||
"""
|
|
||||||
Get client credentials using HTTP Basic Authentication method.
|
|
||||||
Or try getting parameters via POST.
|
|
||||||
See: http://tools.ietf.org/html/rfc6750#section-2.1
|
|
||||||
|
|
||||||
Return a string.
|
|
||||||
"""
|
|
||||||
auth_header = self.request.META.get('HTTP_AUTHORIZATION', '')
|
|
||||||
|
|
||||||
if re.compile('^Basic\s{1}.+$').match(auth_header):
|
|
||||||
b64_user_pass = auth_header.split()[1]
|
|
||||||
try:
|
|
||||||
user_pass = b64decode(b64_user_pass).decode('utf-8').split(':')
|
|
||||||
client_id, client_secret = tuple(user_pass)
|
|
||||||
except Exception:
|
|
||||||
client_id = client_secret = ''
|
|
||||||
else:
|
|
||||||
client_id = self.request.POST.get('client_id', '')
|
|
||||||
client_secret = self.request.POST.get('client_secret', '')
|
|
||||||
|
|
||||||
return (client_id, client_secret)
|
|
||||||
|
|
||||||
def validate_params(self):
|
def validate_params(self):
|
||||||
try:
|
try:
|
||||||
self.client = Client.objects.get(client_id=self.params['client_id'])
|
self.client = Client.objects.get(client_id=self.params['client_id'])
|
||||||
|
@ -118,7 +97,14 @@ class TokenEndpoint(object):
|
||||||
if not settings.get('OIDC_GRANT_TYPE_PASSWORD_ENABLE'):
|
if not settings.get('OIDC_GRANT_TYPE_PASSWORD_ENABLE'):
|
||||||
raise TokenError('unsupported_grant_type')
|
raise TokenError('unsupported_grant_type')
|
||||||
|
|
||||||
|
auth_args = (self.request,)
|
||||||
|
try:
|
||||||
|
inspect.getcallargs(authenticate, *auth_args)
|
||||||
|
except TypeError:
|
||||||
|
auth_args = ()
|
||||||
|
|
||||||
user = authenticate(
|
user = authenticate(
|
||||||
|
*auth_args,
|
||||||
username=self.params['username'],
|
username=self.params['username'],
|
||||||
password=self.params['password']
|
password=self.params['password']
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,6 +32,15 @@ class UserAuthError(Exception):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TokenIntrospectionError(Exception):
|
||||||
|
"""
|
||||||
|
Specific to the introspection endpoint. This error will be converted
|
||||||
|
to an "active: false" response, as per the spec.
|
||||||
|
See https://tools.ietf.org/html/rfc7662
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AuthorizeError(Exception):
|
class AuthorizeError(Exception):
|
||||||
|
|
||||||
_errors = {
|
_errors = {
|
||||||
|
|
|
@ -107,7 +107,8 @@ def default_after_end_session_hook(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def default_idtoken_processing_hook(id_token, user):
|
def default_idtoken_processing_hook(
|
||||||
|
id_token, user, token, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Hook to perform some additional actions to `id_token` dictionary just before serialization.
|
Hook to perform some additional actions to `id_token` dictionary just before serialization.
|
||||||
|
|
||||||
|
@ -117,12 +118,29 @@ def default_idtoken_processing_hook(id_token, user):
|
||||||
:param user: user for whom id_token is generated
|
:param user: user for whom id_token is generated
|
||||||
:type user: User
|
:type user: User
|
||||||
|
|
||||||
|
:param token: the Token object created for the authentication request
|
||||||
|
:type token: oidc_provider.models.Token
|
||||||
|
|
||||||
|
:param request: the request initiating this ID token processing
|
||||||
|
:type request: django.http.HttpRequest
|
||||||
|
|
||||||
:return: custom modified dictionary of values for `id_token`
|
:return: custom modified dictionary of values for `id_token`
|
||||||
:rtype dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
return id_token
|
return id_token
|
||||||
|
|
||||||
|
|
||||||
|
def default_introspection_processing_hook(introspection_response, client, id_token):
|
||||||
|
"""
|
||||||
|
Hook to customise the returned data from the token introspection endpoint
|
||||||
|
:param introspection_response:
|
||||||
|
:param client:
|
||||||
|
:param id_token:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return introspection_response
|
||||||
|
|
||||||
|
|
||||||
def get_browser_state_or_default(request):
|
def get_browser_state_or_default(request):
|
||||||
"""
|
"""
|
||||||
Determine value to use as session state.
|
Determine value to use as session state.
|
||||||
|
@ -130,3 +148,15 @@ def get_browser_state_or_default(request):
|
||||||
key = (request.session.session_key or
|
key = (request.session.session_key or
|
||||||
settings.get('OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY'))
|
settings.get('OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY'))
|
||||||
return sha224(key.encode('utf-8')).hexdigest()
|
return sha224(key.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def run_processing_hook(subject, hook_settings_name, **kwargs):
|
||||||
|
processing_hooks = settings.get(hook_settings_name)
|
||||||
|
if not isinstance(processing_hooks, (list, tuple)):
|
||||||
|
processing_hooks = [processing_hooks]
|
||||||
|
|
||||||
|
for hook_string in processing_hooks:
|
||||||
|
hook = settings.import_from_str(hook_string)
|
||||||
|
subject = hook(subject, **kwargs)
|
||||||
|
|
||||||
|
return subject
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from base64 import b64decode
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -28,6 +29,30 @@ def extract_access_token(request):
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
|
def extract_client_auth(request):
|
||||||
|
"""
|
||||||
|
Get client credentials using HTTP Basic Authentication method.
|
||||||
|
Or try getting parameters via POST.
|
||||||
|
See: http://tools.ietf.org/html/rfc6750#section-2.1
|
||||||
|
|
||||||
|
Return a tuple `(client_id, client_secret)`.
|
||||||
|
"""
|
||||||
|
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
||||||
|
|
||||||
|
if re.compile('^Basic\s{1}.+$').match(auth_header):
|
||||||
|
b64_user_pass = auth_header.split()[1]
|
||||||
|
try:
|
||||||
|
user_pass = b64decode(b64_user_pass).decode('utf-8').split(':')
|
||||||
|
client_id, client_secret = tuple(user_pass)
|
||||||
|
except Exception:
|
||||||
|
client_id = client_secret = ''
|
||||||
|
else:
|
||||||
|
client_id = request.POST.get('client_id', '')
|
||||||
|
client_secret = request.POST.get('client_secret', '')
|
||||||
|
|
||||||
|
return (client_id, client_secret)
|
||||||
|
|
||||||
|
|
||||||
def protected_resource_view(scopes=None):
|
def protected_resource_view(scopes=None):
|
||||||
"""
|
"""
|
||||||
View decorator. The client accesses protected resources by presenting the
|
View decorator. The client accesses protected resources by presenting the
|
||||||
|
|
|
@ -9,7 +9,7 @@ from jwkest.jwk import SYMKey
|
||||||
from jwkest.jws import JWS
|
from jwkest.jws import JWS
|
||||||
from jwkest.jwt import JWT
|
from jwkest.jwt import JWT
|
||||||
|
|
||||||
from oidc_provider.lib.utils.common import get_issuer
|
from oidc_provider.lib.utils.common import get_issuer, run_processing_hook
|
||||||
from oidc_provider.lib.claims import StandardScopeClaims
|
from oidc_provider.lib.claims import StandardScopeClaims
|
||||||
from oidc_provider.models import (
|
from oidc_provider.models import (
|
||||||
Code,
|
Code,
|
||||||
|
@ -62,13 +62,9 @@ def create_id_token(token, user, aud, nonce='', at_hash='', request=None, scope=
|
||||||
claims = StandardScopeClaims(token).create_response_dic()
|
claims = StandardScopeClaims(token).create_response_dic()
|
||||||
dic.update(claims)
|
dic.update(claims)
|
||||||
|
|
||||||
processing_hook = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK')
|
dic = run_processing_hook(
|
||||||
|
dic, 'OIDC_IDTOKEN_PROCESSING_HOOK',
|
||||||
if isinstance(processing_hook, (list, tuple)):
|
user=user, token=token, request=request)
|
||||||
for hook in processing_hook:
|
|
||||||
dic = settings.import_from_str(hook)(dic, user=user)
|
|
||||||
else:
|
|
||||||
dic = settings.import_from_str(processing_hook)(dic, user=user)
|
|
||||||
|
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
|
|
|
@ -129,6 +129,22 @@ class DefaultSettings(object):
|
||||||
"""
|
"""
|
||||||
return 'oidc_provider.lib.utils.common.default_idtoken_processing_hook'
|
return 'oidc_provider.lib.utils.common.default_idtoken_processing_hook'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def OIDC_INTROSPECTION_PROCESSING_HOOK(self):
|
||||||
|
"""
|
||||||
|
OPTIONAL. A string with the location of your function.
|
||||||
|
Used to update the response for a valid introspection token request.
|
||||||
|
"""
|
||||||
|
return 'oidc_provider.lib.utils.common.default_introspection_processing_hook'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def OIDC_INTROSPECTION_VALIDATE_AUDIENCE_SCOPE(self):
|
||||||
|
"""
|
||||||
|
OPTIONAL: A boolean to specify whether or not to verify that the introspection
|
||||||
|
resource has the requesting client id as one of its scopes.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def OIDC_GRANT_TYPE_PASSWORD_ENABLE(self):
|
def OIDC_GRANT_TYPE_PASSWORD_ENABLE(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
import django
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urlparse import parse_qs, urlsplit
|
from urlparse import parse_qs, urlsplit
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -109,7 +113,7 @@ def fake_sub_generator(user):
|
||||||
return user.email
|
return user.email
|
||||||
|
|
||||||
|
|
||||||
def fake_idtoken_processing_hook(id_token, user):
|
def fake_idtoken_processing_hook(id_token, user, **kwargs):
|
||||||
"""
|
"""
|
||||||
Fake function for inserting some keys into token. Testing OIDC_IDTOKEN_PROCESSING_HOOK.
|
Fake function for inserting some keys into token. Testing OIDC_IDTOKEN_PROCESSING_HOOK.
|
||||||
"""
|
"""
|
||||||
|
@ -118,7 +122,7 @@ def fake_idtoken_processing_hook(id_token, user):
|
||||||
return id_token
|
return id_token
|
||||||
|
|
||||||
|
|
||||||
def fake_idtoken_processing_hook2(id_token, user):
|
def fake_idtoken_processing_hook2(id_token, user, **kwargs):
|
||||||
"""
|
"""
|
||||||
Fake function for inserting some keys into token.
|
Fake function for inserting some keys into token.
|
||||||
Testing OIDC_IDTOKEN_PROCESSING_HOOK - tuple or list as param
|
Testing OIDC_IDTOKEN_PROCESSING_HOOK - tuple or list as param
|
||||||
|
@ -126,3 +130,34 @@ def fake_idtoken_processing_hook2(id_token, user):
|
||||||
id_token['test_idtoken_processing_hook2'] = FAKE_RANDOM_STRING
|
id_token['test_idtoken_processing_hook2'] = FAKE_RANDOM_STRING
|
||||||
id_token['test_idtoken_processing_hook_user_email2'] = user.email
|
id_token['test_idtoken_processing_hook_user_email2'] = user.email
|
||||||
return id_token
|
return id_token
|
||||||
|
|
||||||
|
|
||||||
|
def fake_idtoken_processing_hook3(id_token, user, token, **kwargs):
|
||||||
|
"""
|
||||||
|
Fake function for checking scope is passed to processing hook.
|
||||||
|
"""
|
||||||
|
id_token['scope_of_token_passed_to_processing_hook'] = token.scope
|
||||||
|
return id_token
|
||||||
|
|
||||||
|
|
||||||
|
def fake_idtoken_processing_hook4(id_token, user, **kwargs):
|
||||||
|
"""
|
||||||
|
Fake function for checking kwargs passed to processing hook.
|
||||||
|
"""
|
||||||
|
id_token['kwargs_passed_to_processing_hook'] = {
|
||||||
|
key: repr(value)
|
||||||
|
for (key, value) in kwargs.items()
|
||||||
|
}
|
||||||
|
return id_token
|
||||||
|
|
||||||
|
|
||||||
|
def fake_introspection_processing_hook(response_dict, client, id_token):
|
||||||
|
response_dict['test_introspection_processing_hook'] = FAKE_RANDOM_STRING
|
||||||
|
return response_dict
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthBackend:
|
||||||
|
def authenticate(self, *args, **kwargs):
|
||||||
|
if django.VERSION[0] >= 2 or (django.VERSION[0] == 1 and django.VERSION[1] >= 11):
|
||||||
|
assert len(args) > 0 and args[0]
|
||||||
|
return ModelBackend().authenticate(*args, **kwargs)
|
||||||
|
|
116
oidc_provider/tests/cases/test_introspection_endpoint.py
Normal file
116
oidc_provider/tests/cases/test_introspection_endpoint.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
|
||||||
|
from mock import patch
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
except ImportError:
|
||||||
|
from urllib import urlencode
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.test import TestCase, RequestFactory, override_settings
|
||||||
|
from django.utils import timezone
|
||||||
|
try:
|
||||||
|
from django.urls import reverse
|
||||||
|
except ImportError:
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from oidc_provider.tests.app.utils import (
|
||||||
|
create_fake_user,
|
||||||
|
create_fake_client,
|
||||||
|
create_fake_token,
|
||||||
|
FAKE_RANDOM_STRING)
|
||||||
|
from oidc_provider.lib.utils.token import create_id_token
|
||||||
|
from oidc_provider.views import TokenIntrospectionView
|
||||||
|
|
||||||
|
|
||||||
|
class IntrospectionTestCase(TestCase):
|
||||||
|
|
||||||
|
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.resource = create_fake_client(response_type='id_token token')
|
||||||
|
self.resource.scope = ['token_introspection', self.client.client_id]
|
||||||
|
self.resource.save()
|
||||||
|
self.token = create_fake_token(self.user, self.client.scope, self.client)
|
||||||
|
self.token.access_token = str(random.randint(1, 999999)).zfill(6)
|
||||||
|
self.now = time.time()
|
||||||
|
with patch('oidc_provider.lib.utils.token.time.time') as time_func:
|
||||||
|
time_func.return_value = self.now
|
||||||
|
self.token.id_token = create_id_token(self.token, self.user, self.client.client_id)
|
||||||
|
self.token.save()
|
||||||
|
|
||||||
|
def _assert_inactive(self, response):
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertJSONEqual(force_text(response.content), {'active': False})
|
||||||
|
|
||||||
|
def _make_request(self, **kwargs):
|
||||||
|
url = reverse('oidc_provider:token-introspection')
|
||||||
|
data = {
|
||||||
|
'client_id': kwargs.get('client_id', self.resource.client_id),
|
||||||
|
'client_secret': kwargs.get('client_secret', self.resource.client_secret),
|
||||||
|
'token': kwargs.get('access_token', self.token.access_token),
|
||||||
|
}
|
||||||
|
|
||||||
|
request = self.factory.post(url, data=urlencode(data),
|
||||||
|
content_type='application/x-www-form-urlencoded')
|
||||||
|
|
||||||
|
return TokenIntrospectionView.as_view()(request)
|
||||||
|
|
||||||
|
def test_no_client_params_returns_inactive(self):
|
||||||
|
response = self._make_request(client_id='')
|
||||||
|
self._assert_inactive(response)
|
||||||
|
|
||||||
|
def test_no_client_secret_returns_inactive(self):
|
||||||
|
response = self._make_request(client_secret='')
|
||||||
|
self._assert_inactive(response)
|
||||||
|
|
||||||
|
def test_invalid_client_returns_inactive(self):
|
||||||
|
response = self._make_request(client_id='invalid')
|
||||||
|
self._assert_inactive(response)
|
||||||
|
|
||||||
|
def test_token_not_found_returns_inactive(self):
|
||||||
|
response = self._make_request(access_token='invalid')
|
||||||
|
self._assert_inactive(response)
|
||||||
|
|
||||||
|
def test_scope_no_audience_returns_inactive(self):
|
||||||
|
self.resource.scope = ['token_introspection']
|
||||||
|
self.resource.save()
|
||||||
|
response = self._make_request()
|
||||||
|
self._assert_inactive(response)
|
||||||
|
|
||||||
|
def test_token_expired_returns_inactive(self):
|
||||||
|
self.token.expires_at = timezone.now() - timezone.timedelta(seconds=60)
|
||||||
|
self.token.save()
|
||||||
|
response = self._make_request()
|
||||||
|
self._assert_inactive(response)
|
||||||
|
|
||||||
|
def test_valid_request_returns_default_properties(self):
|
||||||
|
response = self._make_request()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertJSONEqual(force_text(response.content), {
|
||||||
|
'active': True,
|
||||||
|
'aud': self.resource.client_id,
|
||||||
|
'client_id': self.client.client_id,
|
||||||
|
'sub': str(self.user.pk),
|
||||||
|
'iat': int(self.now),
|
||||||
|
'exp': int(self.now + 600),
|
||||||
|
'iss': 'http://localhost:8000/openid',
|
||||||
|
})
|
||||||
|
|
||||||
|
@override_settings(OIDC_INTROSPECTION_PROCESSING_HOOK='oidc_provider.tests.app.utils.fake_introspection_processing_hook') # NOQA
|
||||||
|
def test_custom_introspection_hook_called_on_valid_request(self):
|
||||||
|
response = self._make_request()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertJSONEqual(force_text(response.content), {
|
||||||
|
'active': True,
|
||||||
|
'aud': self.resource.client_id,
|
||||||
|
'client_id': self.client.client_id,
|
||||||
|
'sub': str(self.user.pk),
|
||||||
|
'iat': int(self.now),
|
||||||
|
'exp': int(self.now + 600),
|
||||||
|
'iss': 'http://localhost:8000/openid',
|
||||||
|
'test_introspection_processing_hook': FAKE_RANDOM_STRING
|
||||||
|
})
|
|
@ -3,6 +3,7 @@ import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -256,6 +257,17 @@ class TokenTestCase(TestCase):
|
||||||
else:
|
else:
|
||||||
self.assertNotIn(claim, userinfo)
|
self.assertNotIn(claim, userinfo)
|
||||||
|
|
||||||
|
@override_settings(OIDC_GRANT_TYPE_PASSWORD_ENABLE=True,
|
||||||
|
AUTHENTICATION_BACKENDS=("oidc_provider.tests.app.utils.TestAuthBackend",))
|
||||||
|
def test_password_grant_passes_request_to_backend(self):
|
||||||
|
response = self._post_request(
|
||||||
|
post_data=self._password_grant_post_data(),
|
||||||
|
extras=self._password_grant_auth_header()
|
||||||
|
)
|
||||||
|
|
||||||
|
response_dict = json.loads(response.content.decode('utf-8'))
|
||||||
|
self.assertIn('access_token', response_dict)
|
||||||
|
|
||||||
@override_settings(OIDC_TOKEN_EXPIRE=720)
|
@override_settings(OIDC_TOKEN_EXPIRE=720)
|
||||||
def test_authorization_code(self):
|
def test_authorization_code(self):
|
||||||
"""
|
"""
|
||||||
|
@ -716,6 +728,46 @@ class TokenTestCase(TestCase):
|
||||||
self.assertEqual(id_token.get('test_idtoken_processing_hook2'), FAKE_RANDOM_STRING)
|
self.assertEqual(id_token.get('test_idtoken_processing_hook2'), FAKE_RANDOM_STRING)
|
||||||
self.assertEqual(id_token.get('test_idtoken_processing_hook_user_email2'), self.user.email)
|
self.assertEqual(id_token.get('test_idtoken_processing_hook_user_email2'), self.user.email)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
OIDC_IDTOKEN_PROCESSING_HOOK=(
|
||||||
|
'oidc_provider.tests.app.utils.fake_idtoken_processing_hook3'))
|
||||||
|
def test_additional_idtoken_processing_hook_scope_available(self):
|
||||||
|
"""
|
||||||
|
Test scope is available in OIDC_IDTOKEN_PROCESSING_HOOK.
|
||||||
|
"""
|
||||||
|
id_token = self._request_id_token_with_scope(
|
||||||
|
['openid', 'email', 'profile', 'dummy'])
|
||||||
|
self.assertEqual(
|
||||||
|
id_token.get('scope_of_token_passed_to_processing_hook'),
|
||||||
|
['openid', 'email', 'profile', 'dummy'])
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
OIDC_IDTOKEN_PROCESSING_HOOK=(
|
||||||
|
'oidc_provider.tests.app.utils.fake_idtoken_processing_hook4'))
|
||||||
|
def test_additional_idtoken_processing_hook_kwargs(self):
|
||||||
|
"""
|
||||||
|
Test correct kwargs are passed to OIDC_IDTOKEN_PROCESSING_HOOK.
|
||||||
|
"""
|
||||||
|
id_token = self._request_id_token_with_scope(['openid', 'profile'])
|
||||||
|
kwargs_passed = id_token.get('kwargs_passed_to_processing_hook')
|
||||||
|
assert kwargs_passed
|
||||||
|
self.assertEqual(kwargs_passed.get('token'),
|
||||||
|
'<Token: Some Client - johndoe@example.com>')
|
||||||
|
self.assertEqual(kwargs_passed.get('request'),
|
||||||
|
"<WSGIRequest: POST '/openid/token'>")
|
||||||
|
self.assertEqual(set(kwargs_passed.keys()), {'token', 'request'})
|
||||||
|
|
||||||
|
def _request_id_token_with_scope(self, scope):
|
||||||
|
code = self._create_code(scope)
|
||||||
|
|
||||||
|
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()
|
||||||
|
return id_token
|
||||||
|
|
||||||
def test_pkce_parameters(self):
|
def test_pkce_parameters(self):
|
||||||
"""
|
"""
|
||||||
Test Proof Key for Code Exchange by OAuth Public Clients.
|
Test Proof Key for Code Exchange by OAuth Public Clients.
|
||||||
|
|
|
@ -17,6 +17,7 @@ urlpatterns = [
|
||||||
url(r'^end-session/?$', views.EndSessionView.as_view(), name='end-session'),
|
url(r'^end-session/?$', views.EndSessionView.as_view(), name='end-session'),
|
||||||
url(r'^\.well-known/openid-configuration/?$', views.ProviderInfoView.as_view(),
|
url(r'^\.well-known/openid-configuration/?$', views.ProviderInfoView.as_view(),
|
||||||
name='provider-info'),
|
name='provider-info'),
|
||||||
|
url(r'^introspect/?$', views.TokenIntrospectionView.as_view(), name='token-introspection'),
|
||||||
url(r'^jwks/?$', views.JwksView.as_view(), name='jwks'),
|
url(r'^jwks/?$', views.JwksView.as_view(), name='jwks'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
from oidc_provider.lib.endpoints.introspection import TokenIntrospectionEndpoint
|
||||||
try:
|
try:
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
from urlparse import urlsplit, parse_qs, urlunsplit
|
from urlparse import urlsplit, parse_qs, urlunsplit
|
||||||
|
@ -34,7 +37,8 @@ from oidc_provider.lib.errors import (
|
||||||
ClientIdError,
|
ClientIdError,
|
||||||
RedirectUriError,
|
RedirectUriError,
|
||||||
TokenError,
|
TokenError,
|
||||||
UserAuthError)
|
UserAuthError,
|
||||||
|
TokenIntrospectionError)
|
||||||
from oidc_provider.lib.utils.common import (
|
from oidc_provider.lib.utils.common import (
|
||||||
redirect,
|
redirect,
|
||||||
get_site_url,
|
get_site_url,
|
||||||
|
@ -50,6 +54,7 @@ from oidc_provider.models import (
|
||||||
from oidc_provider import settings
|
from oidc_provider import settings
|
||||||
from oidc_provider import signals
|
from oidc_provider import signals
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
OIDC_TEMPLATES = settings.get('OIDC_TEMPLATES')
|
OIDC_TEMPLATES = settings.get('OIDC_TEMPLATES')
|
||||||
|
@ -230,10 +235,10 @@ class TokenView(View):
|
||||||
@protected_resource_view(['openid'])
|
@protected_resource_view(['openid'])
|
||||||
def userinfo(request, *args, **kwargs):
|
def userinfo(request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create a diccionary with all the requested claims about the End-User.
|
Create a dictionary with all the requested claims about the End-User.
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
|
||||||
|
|
||||||
Return a diccionary.
|
Return a dictionary.
|
||||||
"""
|
"""
|
||||||
token = kwargs['token']
|
token = kwargs['token']
|
||||||
|
|
||||||
|
@ -267,6 +272,7 @@ class ProviderInfoView(View):
|
||||||
dic['token_endpoint'] = site_url + reverse('oidc_provider:token')
|
dic['token_endpoint'] = site_url + reverse('oidc_provider:token')
|
||||||
dic['userinfo_endpoint'] = site_url + reverse('oidc_provider:userinfo')
|
dic['userinfo_endpoint'] = site_url + reverse('oidc_provider:userinfo')
|
||||||
dic['end_session_endpoint'] = site_url + reverse('oidc_provider:end-session')
|
dic['end_session_endpoint'] = site_url + reverse('oidc_provider:end-session')
|
||||||
|
dic['introspection_endpoint'] = site_url + reverse('oidc_provider:token-introspection')
|
||||||
|
|
||||||
types_supported = [x[0] for x in RESPONSE_TYPE_CHOICES]
|
types_supported = [x[0] for x in RESPONSE_TYPE_CHOICES]
|
||||||
dic['response_types_supported'] = types_supported
|
dic['response_types_supported'] = types_supported
|
||||||
|
@ -356,3 +362,19 @@ class CheckSessionIframeView(View):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
return render(request, 'oidc_provider/check_session_iframe.html', kwargs)
|
return render(request, 'oidc_provider/check_session_iframe.html', kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenIntrospectionView(View):
|
||||||
|
@method_decorator(csrf_exempt)
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
return super(TokenIntrospectionView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
introspection = TokenIntrospectionEndpoint(request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
introspection.validate_params()
|
||||||
|
dic = introspection.create_response_dic()
|
||||||
|
return TokenIntrospectionEndpoint.response(dic)
|
||||||
|
except TokenIntrospectionError:
|
||||||
|
return TokenIntrospectionEndpoint.response({'active': False})
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -20,6 +20,7 @@ setup(
|
||||||
author='Juan Ignacio Fiorentino',
|
author='Juan Ignacio Fiorentino',
|
||||||
author_email='juanifioren@gmail.com',
|
author_email='juanifioren@gmail.com',
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Environment :: Web Environment',
|
'Environment :: Web Environment',
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
|
15
tox.ini
15
tox.ini
|
@ -1,7 +1,7 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist=
|
envlist=
|
||||||
py27-django{17,18,19,110,111},
|
py27-django{18,19,110,111},
|
||||||
py34-django{17,18,19,110,111,20},
|
py34-django{18,19,110,111,20},
|
||||||
py35-django{18,19,110,111,20},
|
py35-django{18,19,110,111,20},
|
||||||
py36-django{18,19,110,111,20},
|
py36-django{18,19,110,111,20},
|
||||||
|
|
||||||
|
@ -15,7 +15,6 @@ deps =
|
||||||
pytest-django
|
pytest-django
|
||||||
pytest-flake8
|
pytest-flake8
|
||||||
pytest-cov
|
pytest-cov
|
||||||
django17: django>=1.7,<1.8
|
|
||||||
django18: django>=1.8,<1.9
|
django18: django>=1.8,<1.9
|
||||||
django19: django>=1.9,<1.10
|
django19: django>=1.9,<1.10
|
||||||
django110: django>=1.10,<1.11
|
django110: django>=1.10,<1.11
|
||||||
|
@ -25,6 +24,16 @@ deps =
|
||||||
commands =
|
commands =
|
||||||
pytest --flake8 --cov=oidc_provider {posargs}
|
pytest --flake8 --cov=oidc_provider {posargs}
|
||||||
|
|
||||||
|
[testenv:docs]
|
||||||
|
basepython = python2.7
|
||||||
|
changedir = docs
|
||||||
|
deps =
|
||||||
|
sphinx
|
||||||
|
sphinx_rtd_theme
|
||||||
|
commands =
|
||||||
|
mkdir -p _static/
|
||||||
|
sphinx-build -v -W -b html -d {envtmpdir}/doctrees -D html_static_path="_static" . {envtmpdir}/html
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
DJANGO_SETTINGS_MODULE = oidc_provider.tests.settings
|
DJANGO_SETTINGS_MODULE = oidc_provider.tests.settings
|
||||||
python_files = test_*.py
|
python_files = test_*.py
|
||||||
|
|
Loading…
Reference in a new issue