Merge pull request #8 from juanifioren/v0.4.x

keeping it updated
This commit is contained in:
Wojciech Bartosiak 2016-10-04 19:15:38 +02:00 committed by GitHub
commit 60b2cf27af
25 changed files with 450 additions and 260 deletions

View file

@ -4,13 +4,24 @@ All notable changes to this project will be documented in this file.
### [Unreleased] ### [Unreleased]
### [0.4.1] - 2016-10-03
##### Changed
- Update pyjwkest to version 1.3.0.
- Use Cryptodome instead of Crypto lib.
### [0.4.0] - 2016-09-12
##### Added ##### Added
- Support for Hybrid Flow.
- New attributes for Clients: Website url, logo, contact email, terms url.
- Polish translations. - Polish translations.
- Examples section in documentation. - Examples section in documentation.
##### Fixed ##### Fixed
- CORS in discovery and userinfo endpoint. - CORS in discovery and userinfo endpoint.
- Client type public bug when created using the admin. - Client type public bug when created using the admin.
- Missing OIDC_TOKEN_EXPIRE setting on implicit flow.
### [0.3.7] - 2016-08-31 ### [0.3.7] - 2016-08-31

View file

@ -2,8 +2,8 @@
[![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=develop)](https://travis-ci.org/juanifioren/django-oidc-provider) [![Documentation Status](https://readthedocs.org/projects/django-oidc-provider/badge/?version=v0.4.x)](http://django-oidc-provider.readthedocs.io/en/v0.4.x/?badge=v0.4.x)
[![PyPI Downloads](https://img.shields.io/pypi/dm/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider) [![Travis](https://travis-ci.org/juanifioren/django-oidc-provider.svg?branch=v0.4.x)](https://travis-ci.org/juanifioren/django-oidc-provider)
## About OpenID ## About OpenID
@ -15,7 +15,7 @@ OpenID Connect is a simple identity layer on top of the OAuth 2.0 protocol, whic
Support for Python 3 and 2. Also latest versions of django. Support for Python 3 and 2. Also latest versions of django.
[Read docs for more info](http://django-oidc-provider.readthedocs.org/) or [see the changelog here](https://github.com/juanifioren/django-oidc-provider/blob/master/CHANGELOG.md). [Read docs for more info](http://django-oidc-provider.readthedocs.org/).
## Contributing ## Contributing

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View file

@ -1,7 +1,7 @@
Welcome to Django OIDC Provider Documentation! Welcome to Django OIDC Provider Documentation!
============================================== ==============================================
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. And as a side effect a fair implementation of OAuth2.0 too. This tiny (but powerful!) package can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. And as a side effect a fair implementation of OAuth2.0 too. Covers Authorization Code, Implicit and Hybrid flows.
Also implements the following specifications: Also implements the following specifications:
@ -13,7 +13,6 @@ Also implements the following specifications:
Before getting started there are some important things that you should know: Before getting started there are some important things that you should know:
* Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. * Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that.
* This library covers **Authorization Code Flow** and **Implicit Flow**, NO support for **Hybrid Flow** at this moment.
* Supports only for requesting Claims using Scope values. * Supports only for requesting Claims using Scope values.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -27,9 +26,10 @@ Contents:
sections/relyingparties sections/relyingparties
sections/serverkeys sections/serverkeys
sections/templates sections/templates
sections/claims sections/scopesclaims
sections/userconsent sections/userconsent
sections/oauth2 sections/oauth2
sections/accesstokens
sections/settings sections/settings
sections/examples sections/examples
sections/contribute sections/contribute

View file

@ -0,0 +1,64 @@
.. _accesstokens:
Access Tokens
#############
At the end of the login process, an access token is generated. This access token is the thing that's passed along with every API call (e.g. userinfo endpoint) as proof that the call was made by a specific person from a specific app.
Access tokens generally have a lifetime of only a couple of hours, you can use ``OIDC_TOKEN_EXPIRE`` to set custom expiration that suit your needs.
Obtaining an Access token
=========================
Go to the admin site and create a confidential client with ``response_type = code`` and ``redirect_uri = http://example.org/``.
Open your browser and accept consent at::
http://localhost:8000/authorize?client_id=651462&redirect_uri=http://example.org/&response_type=code&scope=openid email profile&state=123123
In the redirected URL you should have a ``code`` parameter included as query string::
http://example.org/?code=b9cedb346ee04f15ab1d3ac13da92002&state=123123
We use ``code`` value to obtain ``access_token`` and ``refresh_token``::
curl -X POST \
-H "Cache-Control: no-cache" \
-H "Content-Type: application/x-www-form-urlencoded" \
"http://localhost:8000/token/" \
-d "client_id=651462" \
-d "client_secret=37b1c4ff826f8d78bd45e25bad75a2c0" \
-d "code=b9cedb346ee04f15ab1d3ac13da92002" \
-d "redirect_uri=http://example.org/" \
-d "grant_type=authorization_code"
Example response::
{
"access_token": "82b35f3d810f4cf49dd7a52d4b22a594",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "0bac2d80d75d46658b0b31d3778039bb",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6..."
}
Then you can grab the access token and ask user data by doing a GET request to the ``/userinfo`` endpoint::
curl -X GET \
-H "Cache-Control: no-cache" \
"http://localhost:8000/userinfo/?access_token=82b35f3d810f4cf49dd7a52d4b22a594"
Expiration and Refresh of Access Tokens
=======================================
If you receive a ``401 Unauthorized`` status when issuing access token probably means that has expired.
The RP application obtains a new access token by sending a POST request to the ``/token`` endpoint with the following request parameters::
curl -X POST \
-H "Cache-Control: no-cache" \
-H "Content-Type: application/x-www-form-urlencoded" \
"http://localhost:8000/token/" \
-d "client_id=651462" \
-d "grant_type=refresh_token" \
-d "refresh_token=0bac2d80d75d46658b0b31d3778039bb"

View file

@ -18,14 +18,22 @@ Properties
* ``client_type``: Values are ``confidential`` and ``public``. * ``client_type``: Values are ``confidential`` and ``public``.
* ``client_id``: Client unique identifier. * ``client_id``: Client unique identifier.
* ``client_secret``: Client secret for confidential applications. * ``client_secret``: Client secret for confidential applications.
* ``response_type``: Values are ``code``, ``id_token`` and ``id_token token``. * ``response_type``: Values depends of wich flow you want use.
* ``jwt_alg``: Clients can choose wich algorithm will be used to sign id_tokens. Values are ``HS256`` and ``RS256``. * ``jwt_alg``: Clients can choose wich algorithm will be used to sign id_tokens. Values are ``HS256`` and ``RS256``.
* ``date_created``: Date automatically added when created. * ``date_created``: Date automatically added when created.
* ``redirect_uris``: List of redirect URIs.
Optional information:
* ``website_url``: Website URL of your client.
* ``terms_url``: External reference to the privacy policy of the client.
* ``contact_email``: Contact email.
* ``logo``: Logo image.
Using the admin Using the admin
=============== ===============
We suggest you to use Django admin to easily manage your clients: We suggest you to use Django admin to easily manage your clients:
.. image:: ../images/client_creation.png .. image:: ../images/client_creation.png
:align: center :align: center

View file

@ -1,13 +1,13 @@
.. _claims: .. _scopesclaims:
Standard Claims Scopes and Claims
############### #################
This subset of OpenID Connect defines a set of standard Claims. They are returned in the UserInfo Response. This subset of OpenID Connect defines a set of standard Claims. They are returned in the UserInfo Response.
The package comes with a setting called ``OIDC_USERINFO``, basically it refers to a class that MUST have a class-method named ``get_by_user``, this will be called with a Django ``User`` instance and returns an object with all the claims of the user as attributes. The package comes with a setting called ``OIDC_USERINFO``, basically it refers to a function that will be called with ``claims`` (dict) and ``user`` (user instance). It returns the ``claims`` dict with all the claims populated.
List of all the attributes grouped by scopes: List of all the ``claims`` keys grouped by scopes:
+--------------------+----------------+-----------------------+------------------------+ +--------------------+----------------+-----------------------+------------------------+
| profile | email | phone | address | | profile | email | phone | address |
@ -41,15 +41,18 @@ List of all the attributes grouped by scopes:
| updated_at | | | | | updated_at | | | |
+--------------------+----------------+-----------------------+------------------------+ +--------------------+----------------+-----------------------+------------------------+
How to populate standard claims
===============================
Somewhere in your Django ``settings.py``:: Somewhere in your Django ``settings.py``::
OIDC_USERINFO = 'myproject.oidc_provider_settings.userinfo' OIDC_USERINFO = 'myproject.oidc_provider_settings.userinfo'
Then create the function for the ``OIDC_USERINFO`` setting:: Then inside your ``oidc_provider_settings.py`` file create the function for the ``OIDC_USERINFO`` setting::
def userinfo(claims, user): def userinfo(claims, user):
# Populate claims dict.
claims['name'] = '{0} {1}'.format(user.first_name, user.last_name) claims['name'] = '{0} {1}'.format(user.first_name, user.last_name)
claims['given_name'] = user.first_name claims['given_name'] = user.first_name
claims['family_name'] = user.last_name claims['family_name'] = user.last_name
@ -58,5 +61,52 @@ Then create the function for the ``OIDC_USERINFO`` setting::
return claims return claims
Now test an Authorization Request using these scopes ``openid profile email`` and see how user attributes are returned.
.. note:: .. note::
Please **DO NOT** add extra keys or delete the existing ones in the ``claims`` dict. If you want to add extra claims to some scopes you can use the ``OIDC_EXTRA_SCOPE_CLAIMS`` setting. Please **DO NOT** add extra keys or delete the existing ones in the ``claims`` dict. If you want to add extra claims to some scopes you can use the ``OIDC_EXTRA_SCOPE_CLAIMS`` setting.
How to add custom scopes and claims
===================================
The ``OIDC_EXTRA_SCOPE_CLAIMS`` setting is used to add extra scopes specific for your app. Is just a class that inherit from ``oidc_provider.lib.claims.ScopeClaims``. You can create or modify scopes by adding this methods into it:
* ``info_scopename`` class property for setting the verbose name and description.
* ``scope_scopename`` method for returning some information related.
Let's say that you want add your custom ``foo`` scope for your OAuth2/OpenID provider. So when a client (RP) makes an Authorization Request containing ``foo`` in the list of scopes, it will be listed in the consent page (``templates/oidc_provider/authorize.html``) and then some specific claims like ``bar`` will be returned from the ``/userinfo`` response.
Somewhere in your Django ``settings.py``::
OIDC_USERINFO = 'yourproject.oidc_provider_settings.CustomScopeClaims'
Inside your oidc_provider_settings.py file add the following class::
from django.utils.translation import ugettext as _
from oidc_provider.lib.claims import ScopeClaims
class CustomScopeClaims(ScopeClaims):
info_foo = (
_(u'Foo'),
_(u'Some description for the scope.'),
)
def scope_foo(self):
# self.user - Django user instance.
# self.userinfo - Dict returned by OIDC_USERINFO function.
# self.scopes - List of scopes requested.
dic = {
'bar': 'Something dynamic here',
}
return dic
# If you want to change the description of the profile scope, you can redefine it.
info_profile = (
_(u'Profile'),
_(u'Another description.'),
)
.. note::
If a field is empty or ``None`` inside the dictionary you return on the ``scope_scopename`` method, it will be cleaned from the response.

View file

@ -48,54 +48,15 @@ OIDC_EXTRA_SCOPE_CLAIMS
OPTIONAL. ``str``. A string with the location of your class. Default is ``oidc_provider.lib.claims.ScopeClaims``. OPTIONAL. ``str``. A string with the location of your class. Default is ``oidc_provider.lib.claims.ScopeClaims``.
Used to add extra scopes specific for your app. This class MUST inherit ``ScopeClaims``. Used to add extra scopes specific for your app. OpenID Connect RP's will use scope values to specify what access privileges are being requested for Access Tokens.
OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens. Read more about how to implement it in :ref:`scopesclaims` section.
`Here <http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims>`_ you have the standard scopes defined by the protocol.
You can create or modify scopes using:
* ``info_scopename`` class property for setting the verbose name and description.
* ``scope_scopename`` method for returning some information related.
Check out an example of how to implement it::
from django.utils.translation import ugettext as _
from oidc_provider.lib.claims import ScopeClaims
class MyAppScopeClaims(ScopeClaims):
info_books = (
_(u'Books'), # Verbose name of the scope.
_(u'Access to your books.'), # Description of the scope.
)
def scope_books(self):
# Here, for example, you can search books for this user.
# self.user - Django user instance.
# self.userinfo - Instance of your custom OIDC_USERINFO class.
# self.scopes - List of scopes requested.
dic = {
'books_readed': books_readed_count,
}
return dic
# If you want to change the description of the profile scope, you can redefine it.
info_profile = (
_(u'Profile'),
_(u'Another description.'),
)
.. note::
If a field is empty or ``None`` inside the dictionary your return on ``scope_scopename`` method, it will be cleaned from the response.
OIDC_IDTOKEN_EXPIRE OIDC_IDTOKEN_EXPIRE
=================== ===================
OPTIONAL. ``int``. Token object expiration after been delivered. OPTIONAL. ``int``. ID Token expiration after been delivered.
Expressed in seconds. Default is ``60*10``. Expressed in seconds. Default is ``60*10``.
@ -155,7 +116,7 @@ Expressed in days. Default is ``30*3``.
OIDC_TOKEN_EXPIRE OIDC_TOKEN_EXPIRE
================= =================
OPTIONAL. ``int``. Token object expiration after been created. OPTIONAL. ``int``. Token object (access token) expiration after been created.
Expressed in seconds. Default is ``60*60``. Expressed in seconds. Default is ``60*60``.

View file

@ -1,5 +1,5 @@
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

View file

@ -1,2 +1,2 @@
django==1.9 django==1.10
https://github.com/juanifioren/django-oidc-provider/archive/v0.3.x.zip https://github.com/juanifioren/django-oidc-provider/archive/v0.4.x.zip

View file

@ -4,6 +4,7 @@ from uuid import uuid4
from django.forms import ModelForm from django.forms import ModelForm
from django.contrib import admin from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from oidc_provider.models import Client, Code, Token, RSAKey from oidc_provider.models import Client, Code, Token, RSAKey
@ -48,6 +49,17 @@ class ClientForm(ModelForm):
@admin.register(Client) @admin.register(Client)
class ClientAdmin(admin.ModelAdmin): class ClientAdmin(admin.ModelAdmin):
fieldsets = [
[_(u''), {
'fields': ('name', 'client_type', 'response_type','_redirect_uris', 'jwt_alg'),
}],
[_(u'Credentials'), {
'fields': ('client_id', 'client_secret'),
}],
[_(u'Information'), {
'fields': ('contact_email', 'website_url', 'terms_url', 'logo', 'date_created'),
}],
]
form = ClientForm form = ClientForm
list_display = ['name', 'client_id', 'response_type', 'date_created'] list_display = ['name', 'client_id', 'response_type', 'date_created']
readonly_fields = ['date_created'] readonly_fields = ['date_created']

View file

@ -14,7 +14,6 @@ from oidc_provider.lib.errors import (
ClientIdError, ClientIdError,
RedirectUriError, RedirectUriError,
) )
from oidc_provider.lib.utils.params import Params
from oidc_provider.lib.utils.token import ( from oidc_provider.lib.utils.token import (
create_code, create_code,
create_id_token, create_id_token,
@ -35,25 +34,27 @@ class AuthorizeEndpoint(object):
def __init__(self, request): def __init__(self, request):
self.request = request self.request = request
self.params = Params() self.params = {}
self._extract_params() self._extract_params()
# Determine which flow to use. # Determine which flow to use.
if self.params.response_type in ['code']: if self.params['response_type'] in ['code']:
self.grant_type = 'authorization_code' self.grant_type = 'authorization_code'
elif self.params.response_type in ['id_token', 'id_token token', 'token']: elif self.params['response_type'] in ['id_token', 'id_token token', 'token']:
self.grant_type = 'implicit' self.grant_type = 'implicit'
elif self.params['response_type'] in ['code token', 'code id_token', 'code id_token token']:
self.grant_type = 'hybrid'
else: else:
self.grant_type = None self.grant_type = None
# Determine if it's an OpenID Authentication request (or OAuth2). # Determine if it's an OpenID Authentication request (or OAuth2).
self.is_authentication = 'openid' in self.params.scope self.is_authentication = 'openid' in self.params['scope']
def _extract_params(self): def _extract_params(self):
""" """
Get all the params used by the Authorization Code Flow Get all the params used by the Authorization Code Flow
(and also for the Implicit). (and also for the Implicit and Hybrid).
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
""" """
@ -62,102 +63,99 @@ class AuthorizeEndpoint(object):
query_dict = (self.request.POST if self.request.method == 'POST' query_dict = (self.request.POST if self.request.method == 'POST'
else self.request.GET) else self.request.GET)
self.params.client_id = query_dict.get('client_id', '') self.params['client_id'] = query_dict.get('client_id', '')
self.params.redirect_uri = query_dict.get('redirect_uri', '') self.params['redirect_uri'] = query_dict.get('redirect_uri', '')
self.params.response_type = query_dict.get('response_type', '') self.params['response_type'] = query_dict.get('response_type', '')
self.params.scope = query_dict.get('scope', '').split() self.params['scope'] = query_dict.get('scope', '').split()
self.params.state = query_dict.get('state', '') self.params['state'] = query_dict.get('state', '')
self.params['nonce'] = query_dict.get('nonce', '')
self.params.nonce = query_dict.get('nonce', '') self.params['prompt'] = query_dict.get('prompt', '')
self.params.prompt = query_dict.get('prompt', '') self.params['code_challenge'] = query_dict.get('code_challenge', '')
self.params.code_challenge = query_dict.get('code_challenge', '') self.params['code_challenge_method'] = query_dict.get('code_challenge_method', '')
self.params.code_challenge_method = query_dict.get('code_challenge_method', '')
def validate_params(self): def validate_params(self):
# Client validation. # Client validation.
try: try:
self.client = Client.objects.get(client_id=self.params.client_id) self.client = Client.objects.get(client_id=self.params['client_id'])
except Client.DoesNotExist: except Client.DoesNotExist:
logger.debug('[Authorize] Invalid client identifier: %s', self.params.client_id) logger.debug('[Authorize] Invalid client identifier: %s', self.params['client_id'])
raise ClientIdError() raise ClientIdError()
# Redirect URI validation. # Redirect URI validation.
if self.is_authentication and not self.params.redirect_uri: if self.is_authentication and not self.params['redirect_uri']:
logger.debug('[Authorize] Missing redirect uri.') logger.debug('[Authorize] Missing redirect uri.')
raise RedirectUriError() raise RedirectUriError()
clean_redirect_uri = urlsplit(self.params.redirect_uri) clean_redirect_uri = urlsplit(self.params['redirect_uri'])
clean_redirect_uri = urlunsplit(clean_redirect_uri._replace(query='')) clean_redirect_uri = urlunsplit(clean_redirect_uri._replace(query=''))
if not (clean_redirect_uri in self.client.redirect_uris): if not (clean_redirect_uri in self.client.redirect_uris):
logger.debug('[Authorize] Invalid redirect uri: %s', self.params.redirect_uri) logger.debug('[Authorize] Invalid redirect uri: %s', self.params['redirect_uri'])
raise RedirectUriError() raise RedirectUriError()
# Grant type validation. # Grant type validation.
if not self.grant_type: if not self.grant_type:
logger.debug('[Authorize] Invalid response type: %s', self.params.response_type) logger.debug('[Authorize] Invalid response type: %s', self.params['response_type'])
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type', raise AuthorizeError(self.params['redirect_uri'], 'unsupported_response_type', self.grant_type)
self.grant_type)
# Nonce parameter validation. # Nonce parameter validation.
if self.is_authentication and self.grant_type == 'implicit' and not self.params.nonce: if self.is_authentication and self.grant_type == 'implicit' and not self.params['nonce']:
raise AuthorizeError(self.params.redirect_uri, 'invalid_request', raise AuthorizeError(self.params['redirect_uri'], 'invalid_request', self.grant_type)
self.grant_type)
# Response type parameter validation. # Response type parameter validation.
if self.is_authentication and self.params.response_type != self.client.response_type: if self.is_authentication and self.params['response_type'] != self.client.response_type:
raise AuthorizeError(self.params.redirect_uri, 'invalid_request', raise AuthorizeError(self.params['redirect_uri'], 'invalid_request', self.grant_type)
self.grant_type)
# PKCE validation of the transformation method. # PKCE validation of the transformation method.
if self.params.code_challenge: if self.params['code_challenge']:
if not (self.params.code_challenge_method in ['plain', 'S256']): if not (self.params['code_challenge_method'] in ['plain', 'S256']):
raise AuthorizeError(self.params.redirect_uri, 'invalid_request', self.grant_type) raise AuthorizeError(self.params['redirect_uri'], 'invalid_request', self.grant_type)
def create_response_uri(self): def create_response_uri(self):
uri = urlsplit(self.params.redirect_uri) uri = urlsplit(self.params['redirect_uri'])
query_params = parse_qs(uri.query) query_params = parse_qs(uri.query)
query_fragment = parse_qs(uri.fragment) query_fragment = parse_qs(uri.fragment)
try: try:
if self.grant_type == 'authorization_code': if self.grant_type in ['authorization_code', 'hybrid']:
code = create_code( code = create_code(
user=self.request.user, user=self.request.user,
client=self.client, client=self.client,
scope=self.params.scope, scope=self.params['scope'],
nonce=self.params.nonce, nonce=self.params['nonce'],
is_authentication=self.is_authentication, is_authentication=self.is_authentication,
code_challenge=self.params.code_challenge, code_challenge=self.params['code_challenge'],
code_challenge_method=self.params.code_challenge_method) code_challenge_method=self.params['code_challenge_method'])
code.save() code.save()
if self.grant_type == 'authorization_code':
query_params['code'] = code.code query_params['code'] = code.code
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 in ['implicit', 'hybrid']:
elif self.grant_type == 'implicit':
token = create_token( token = create_token(
user=self.request.user, user=self.request.user,
client=self.client, client=self.client,
scope=self.params.scope) scope=self.params['scope'])
# Check if response_type is an OpenID request with value 'id_token token' # Check if response_type must include access_token in the response.
# or it's an OAuth2 Implicit Flow request. if self.params['response_type'] in ['id_token token', 'token', 'code token', 'code id_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. # We don't need id_token if it's an OAuth2 request.
if self.is_authentication: if self.is_authentication:
kwargs = { kwargs = {
"user": self.request.user, 'user': self.request.user,
"aud": self.client.client_id, 'aud': self.client.client_id,
"nonce": self.params.nonce, 'nonce': self.params['nonce'],
"request": self.request 'request': self.request,
'scope': self.params['scope'],
} }
# Include at_hash when access_token is being returned. # Include at_hash when access_token is being returned.
if 'access_token' in query_fragment: if 'access_token' in query_fragment:
kwargs['at_hash'] = token.at_hash kwargs['at_hash'] = token.at_hash
id_token_dic = create_id_token(**kwargs) 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 # Check if response_type must include id_token in the response.
if self.params['response_type'] in ['id_token', 'id_token token', 'code id_token', 'code id_token token']:
query_fragment['id_token'] = encode_id_token(id_token_dic, self.client)
else: else:
id_token_dic = {} id_token_dic = {}
@ -165,18 +163,19 @@ class AuthorizeEndpoint(object):
token.id_token = id_token_dic token.id_token = id_token_dic
token.save() token.save()
query_fragment['token_type'] = 'bearer' # Code parameter must be present if it's Hybrid Flow.
# TODO: Create setting 'OIDC_TOKEN_EXPIRE'. if self.grant_type == 'hybrid':
query_fragment['expires_in'] = 60 * 10 query_fragment['code'] = code.code
query_fragment['state'] = self.params.state if self.params.state else '' query_fragment['token_type'] = 'bearer'
query_fragment['expires_in'] = settings.get('OIDC_TOKEN_EXPIRE')
query_fragment['state'] = self.params['state'] if self.params['state'] else ''
except Exception as error: except Exception as error:
logger.debug('[Authorize] Error when trying to create response uri: %s', error) logger.debug('[Authorize] Error when trying to create response uri: %s', error)
raise AuthorizeError( raise AuthorizeError(self.params['redirect_uri'], 'server_error', self.grant_type)
self.params.redirect_uri,
'server_error',
self.grant_type)
uri = uri._replace(query=urlencode(query_params, doseq=True)) uri = uri._replace(query=urlencode(query_params, doseq=True))
uri = uri._replace(fragment=urlencode(query_fragment, doseq=True)) uri = uri._replace(fragment=urlencode(query_fragment, doseq=True))
@ -201,7 +200,7 @@ class AuthorizeEndpoint(object):
'date_given': date_given, 'date_given': date_given,
} }
) )
uc.scope = self.params.scope uc.scope = self.params['scope']
# Rewrite expires_at and date_given if object already exists. # Rewrite expires_at and date_given if object already exists.
if not created: if not created:
@ -218,10 +217,8 @@ class AuthorizeEndpoint(object):
""" """
value = False value = False
try: try:
uc = UserConsent.objects.get(user=self.request.user, uc = UserConsent.objects.get(user=self.request.user, client=self.client)
client=self.client) if (set(self.params['scope']).issubset(uc.scope)) and not (uc.has_expired()):
if (set(self.params.scope).issubset(uc.scope)) and \
not (uc.has_expired()):
value = True value = True
except UserConsent.DoesNotExist: except UserConsent.DoesNotExist:
pass pass
@ -232,9 +229,9 @@ class AuthorizeEndpoint(object):
""" """
Return a list with the description of all the scopes requested. Return a list with the description of all the scopes requested.
""" """
scopes = StandardScopeClaims.get_scopes_info(self.params.scope) scopes = StandardScopeClaims.get_scopes_info(self.params['scope'])
if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'): if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'):
scopes_extra = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True).get_scopes_info(self.params.scope) scopes_extra = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True).get_scopes_info(self.params['scope'])
for index_extra, scope_extra in enumerate(scopes_extra): for index_extra, scope_extra in enumerate(scopes_extra):
for index, scope in enumerate(scopes[:]): for index, scope in enumerate(scopes[:]):
if scope_extra['scope'] == scope['scope']: if scope_extra['scope'] == scope['scope']:

View file

@ -12,7 +12,6 @@ from django.http import JsonResponse
from oidc_provider.lib.errors import ( from oidc_provider.lib.errors import (
TokenError, TokenError,
) )
from oidc_provider.lib.utils.params import Params
from oidc_provider.lib.utils.token import ( from oidc_provider.lib.utils.token import (
create_id_token, create_id_token,
create_token, create_token,
@ -33,23 +32,22 @@ class TokenEndpoint(object):
def __init__(self, request): def __init__(self, request):
self.request = request self.request = request
self.params = Params() self.params = {}
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 = self._extract_client_auth()
self.params.client_id = client_id self.params['client_id'] = client_id
self.params.client_secret = client_secret self.params['client_secret'] = client_secret
self.params.redirect_uri = unquote(self.request.POST.get('redirect_uri', '')) self.params['redirect_uri'] = unquote(self.request.POST.get('redirect_uri', ''))
self.params.grant_type = self.request.POST.get('grant_type', '') self.params['grant_type'] = self.request.POST.get('grant_type', '')
self.params.code = self.request.POST.get('code', '') self.params['code'] = self.request.POST.get('code', '')
self.params.state = self.request.POST.get('state', '') self.params['state'] = self.request.POST.get('state', '')
self.params.scope = self.request.POST.get('scope', '') self.params['scope'] = self.request.POST.get('scope', '')
self.params.refresh_token = self.request.POST.get('refresh_token', '') self.params['refresh_token'] = self.request.POST.get('refresh_token', '')
# PKCE parameter.
# PKCE parameters. self.params['code_verifier'] = self.request.POST.get('code_verifier')
self.params.code_verifier = self.request.POST.get('code_verifier')
def _extract_client_auth(self): def _extract_client_auth(self):
""" """
@ -76,68 +74,68 @@ class TokenEndpoint(object):
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'])
except Client.DoesNotExist: except Client.DoesNotExist:
logger.debug('[Token] Client does not exist: %s', self.params.client_id) logger.debug('[Token] Client does not exist: %s', self.params['client_id'])
raise TokenError('invalid_client') raise TokenError('invalid_client')
if self.client.client_type == 'confidential': if self.client.client_type == 'confidential':
if not (self.client.client_secret == self.params.client_secret): if not (self.client.client_secret == self.params['client_secret']):
logger.debug('[Token] Invalid client secret: client %s do not have secret %s', logger.debug('[Token] Invalid client secret: client %s do not have secret %s',
self.client.client_id, self.client.client_secret) self.client.client_id, self.client.client_secret)
raise TokenError('invalid_client') raise TokenError('invalid_client')
if self.params.grant_type == 'authorization_code': if self.params['grant_type'] == 'authorization_code':
if not (self.params.redirect_uri in self.client.redirect_uris): if not (self.params['redirect_uri'] in self.client.redirect_uris):
logger.debug('[Token] Invalid redirect uri: %s', self.params.redirect_uri) logger.debug('[Token] Invalid redirect uri: %s', self.params['redirect_uri'])
raise TokenError('invalid_client') raise TokenError('invalid_client')
try: try:
self.code = Code.objects.get(code=self.params.code) self.code = Code.objects.get(code=self.params['code'])
except Code.DoesNotExist: except Code.DoesNotExist:
logger.debug('[Token] Code does not exist: %s', self.params.code) logger.debug('[Token] Code does not exist: %s', self.params['code'])
raise TokenError('invalid_grant') raise TokenError('invalid_grant')
if not (self.code.client == self.client) \ if not (self.code.client == self.client) \
or self.code.has_expired(): or self.code.has_expired():
logger.debug('[Token] Invalid code: invalid client or code has expired', logger.debug('[Token] Invalid code: invalid client or code has expired',
self.params.redirect_uri) self.params['redirect_uri'])
raise TokenError('invalid_grant') raise TokenError('invalid_grant')
# Validate PKCE parameters. # Validate PKCE parameters.
if self.params.code_verifier: if self.params['code_verifier']:
if self.code.code_challenge_method == 'S256': if self.code.code_challenge_method == 'S256':
new_code_challenge = urlsafe_b64encode( new_code_challenge = urlsafe_b64encode(
hashlib.sha256(self.params.code_verifier.encode('ascii')).digest() hashlib.sha256(self.params['code_verifier'].encode('ascii')).digest()
).decode('utf-8').replace('=', '') ).decode('utf-8').replace('=', '')
else: else:
new_code_challenge = self.params.code_verifier new_code_challenge = self.params['code_verifier']
# TODO: We should explain the error. # TODO: We should explain the error.
if not (new_code_challenge == self.code.code_challenge): if not (new_code_challenge == self.code.code_challenge):
raise TokenError('invalid_grant') raise TokenError('invalid_grant')
elif self.params.grant_type == 'refresh_token': elif self.params['grant_type'] == 'refresh_token':
if not self.params.refresh_token: if not self.params['refresh_token']:
logger.debug('[Token] Missing refresh token') logger.debug('[Token] Missing refresh token')
raise TokenError('invalid_grant') raise TokenError('invalid_grant')
try: try:
self.token = Token.objects.get(refresh_token=self.params.refresh_token, self.token = Token.objects.get(refresh_token=self.params['refresh_token'],
client=self.client) client=self.client)
except Token.DoesNotExist: except Token.DoesNotExist:
logger.debug('[Token] Refresh token does not exist: %s', self.params.refresh_token) logger.debug('[Token] Refresh token does not exist: %s', self.params['refresh_token'])
raise TokenError('invalid_grant') raise TokenError('invalid_grant')
else: else:
logger.debug('[Token] Invalid grant type: %s', self.params.grant_type) logger.debug('[Token] Invalid grant type: %s', self.params['grant_type'])
raise TokenError('unsupported_grant_type') raise TokenError('unsupported_grant_type')
def create_response_dic(self): def create_response_dic(self):
if self.params.grant_type == 'authorization_code': if self.params['grant_type'] == 'authorization_code':
return self.create_code_response_dic() return self.create_code_response_dic()
elif self.params.grant_type == 'refresh_token': elif self.params['grant_type'] == 'refresh_token':
return self.create_refresh_response_dic() return self.create_refresh_response_dic()
def create_code_response_dic(self): def create_code_response_dic(self):
@ -153,6 +151,7 @@ class TokenEndpoint(object):
nonce=self.code.nonce, nonce=self.code.nonce,
at_hash=token.at_hash, at_hash=token.at_hash,
request=self.request, request=self.request,
scope=self.params['scope'],
) )
else: else:
id_token_dic = {} id_token_dic = {}
@ -188,6 +187,7 @@ class TokenEndpoint(object):
nonce=None, nonce=None,
at_hash=token.at_hash, at_hash=token.at_hash,
request=self.request, request=self.request,
scope=self.params['scope'],
) )
else: else:
id_token_dic = {} id_token_dic = {}

View file

@ -42,7 +42,7 @@ def get_issuer(site_url=None, request=None):
.split('/.well-known/openid-configuration')[0] .split('/.well-known/openid-configuration')[0]
issuer = site_url + path issuer = site_url + path
return issuer return str(issuer)
def default_userinfo(claims, user): def default_userinfo(claims, user):

View file

@ -1,7 +0,0 @@
class Params(object):
"""
The purpose of this class is for accesing params via dot notation.
"""
pass

View file

@ -2,7 +2,7 @@ from datetime import timedelta
import time import time
import uuid import uuid
from Crypto.PublicKey.RSA import importKey from Cryptodome.PublicKey.RSA import importKey
from django.utils import timezone from django.utils import timezone
from jwkest.jwk import RSAKey as jwk_RSAKey from jwkest.jwk import RSAKey as jwk_RSAKey
from jwkest.jwk import SYMKey from jwkest.jwk import SYMKey
@ -17,10 +17,9 @@ from oidc_provider.models import (
from oidc_provider import settings from oidc_provider import settings
def create_id_token(user, aud, nonce, at_hash=None, request=None): def create_id_token(user, aud, nonce, at_hash=None, request=None, scope=[]):
""" """
Receives a user object and aud (audience). Creates the id_token dictionary.
Then creates the id_token dictionary.
See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken
Return a dic. Return a dic.
@ -51,6 +50,9 @@ def create_id_token(user, aud, nonce, at_hash=None, request=None):
if at_hash: if at_hash:
dic['at_hash'] = at_hash dic['at_hash'] = at_hash
if ('email' in scope) and getattr(user, 'email', None):
dic['email'] = user.email
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)):

View file

@ -1,6 +1,6 @@
import os import os
from Crypto.PublicKey import RSA from Cryptodome.PublicKey import RSA
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from oidc_provider import settings from oidc_provider import settings

View file

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-09-12 14:08
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('oidc_provider', '0017_auto_20160811_1954'),
]
operations = [
migrations.AddField(
model_name='client',
name='contact_email',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='Contact Email'),
),
migrations.AddField(
model_name='client',
name='logo',
field=models.FileField(blank=True, default='', upload_to='oidc_provider/clients', verbose_name='Logo Image'),
),
migrations.AddField(
model_name='client',
name='terms_url',
field=models.CharField(blank=True, default='', help_text='External reference to the privacy policy of the client.', max_length=255, verbose_name='Terms URL'),
),
migrations.AddField(
model_name='client',
name='website_url',
field=models.CharField(blank=True, default='', max_length=255, verbose_name='Website URL'),
),
migrations.AlterField(
model_name='client',
name='jwt_alg',
field=models.CharField(choices=[('HS256', 'HS256'), ('RS256', 'RS256')], default='RS256', help_text='Algorithm used to encode ID Tokens.', max_length=10, verbose_name='JWT Algorithm'),
),
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)'), ('code token', 'code token (Hybrid Flow)'), ('code id_token', 'code id_token (Hybrid Flow)'), ('code id_token token', 'code id_token token (Hybrid Flow)')], max_length=30, verbose_name='Response Type'),
),
]

View file

@ -19,6 +19,9 @@ RESPONSE_TYPE_CHOICES = [
('code', 'code (Authorization Code Flow)'), ('code', 'code (Authorization Code Flow)'),
('id_token', 'id_token (Implicit Flow)'), ('id_token', 'id_token (Implicit Flow)'),
('id_token token', 'id_token token (Implicit Flow)'), ('id_token token', 'id_token token (Implicit Flow)'),
('code token', 'code token (Hybrid Flow)'),
('code id_token', 'code id_token (Hybrid Flow)'),
('code id_token token', 'code id_token token (Hybrid Flow)'),
] ]
JWT_ALGS = [ JWT_ALGS = [
@ -34,8 +37,12 @@ class Client(models.Model):
client_id = models.CharField(max_length=255, unique=True, verbose_name=_(u'Client ID')) client_id = models.CharField(max_length=255, unique=True, verbose_name=_(u'Client ID'))
client_secret = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Client SECRET')) client_secret = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Client SECRET'))
response_type = models.CharField(max_length=30, choices=RESPONSE_TYPE_CHOICES, verbose_name=_(u'Response Type')) response_type = models.CharField(max_length=30, choices=RESPONSE_TYPE_CHOICES, verbose_name=_(u'Response Type'))
jwt_alg = models.CharField(max_length=10, choices=JWT_ALGS, default='RS256', verbose_name=_(u'JWT Algorithm')) jwt_alg = models.CharField(max_length=10, choices=JWT_ALGS, default='RS256', verbose_name=_(u'JWT Algorithm'), help_text=_(u'Algorithm used to encode ID Tokens.'))
date_created = models.DateField(auto_now_add=True, verbose_name=_(u'Date Created')) date_created = models.DateField(auto_now_add=True, verbose_name=_(u'Date Created'))
website_url = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Website URL'))
terms_url = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Terms URL'), help_text=_(u'External reference to the privacy policy of the client.'))
contact_email = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Contact Email'))
logo = models.FileField(blank=True, default='', upload_to='oidc_provider/clients', verbose_name=_(u'Logo Image'))
_redirect_uris = models.TextField(default='', verbose_name=_(u'Redirect URIs'), help_text=_(u'Enter each URI on a new line.')) _redirect_uris = models.TextField(default='', verbose_name=_(u'Redirect URIs'), help_text=_(u'Enter each URI on a new line.'))

View file

@ -60,15 +60,14 @@ def create_fake_client(response_type, is_public=False):
def is_code_valid(url, user, client): def is_code_valid(url, user, client):
""" """
Check if the code inside the url is valid. Check if the code inside the url is valid. Supporting both query string and fragment.
""" """
try: try:
parsed = urlsplit(url) parsed = urlsplit(url)
params = parse_qs(parsed.query) params = parse_qs(parsed.query or parsed.fragment)
code = params['code'][0] code = params['code'][0]
code = Code.objects.get(code=code) code = Code.objects.get(code=code)
is_code_ok = (code.client == client) and \ is_code_ok = (code.client == client) and (code.user == user)
(code.user == user)
except: except:
is_code_ok = False is_code_ok = False

View file

@ -11,7 +11,10 @@ import uuid
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,
override_settings,
)
from django.test import TestCase from django.test import TestCase
from jwkest.jwt import JWT from jwkest.jwt import JWT
@ -25,19 +28,7 @@ from oidc_provider.tests.app.utils import (
from oidc_provider.views import AuthorizeView from oidc_provider.views import AuthorizeView
class AuthorizationCodeFlowTestCase(TestCase): class AuthorizeEndpointMixin(object):
"""
Test cases for Authorize Endpoint using Code Flow.
"""
def setUp(self):
call_command('creatersakey')
self.factory = RequestFactory()
self.user = create_fake_user()
self.client = create_fake_client(response_type='code')
self.client_public = create_fake_client(response_type='code', is_public=True)
self.state = uuid.uuid4().hex
self.nonce = uuid.uuid4().hex
def _auth_request(self, method, data={}, is_user_authenticated=False): def _auth_request(self, method, data={}, is_user_authenticated=False):
url = reverse('oidc_provider:authorize') url = reverse('oidc_provider:authorize')
@ -59,6 +50,21 @@ class AuthorizationCodeFlowTestCase(TestCase):
return response return response
class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin):
"""
Test cases for Authorize Endpoint using Code Flow.
"""
def setUp(self):
call_command('creatersakey')
self.factory = RequestFactory()
self.user = create_fake_user()
self.client = create_fake_client(response_type='code')
self.client_public = create_fake_client(response_type='code', is_public=True)
self.state = uuid.uuid4().hex
self.nonce = uuid.uuid4().hex
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
@ -94,8 +100,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
self.assertEqual(response.has_header('Location'), True) self.assertEqual(response.has_header('Location'), True)
# Should be an 'error' component in query. # Should be an 'error' component in query.
query_exists = 'error=' in response['Location'] self.assertIn('error=', response['Location'])
self.assertEqual(query_exists, True)
def test_user_not_logged(self): def test_user_not_logged(self):
""" """
@ -115,8 +120,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
response = self._auth_request('get', data) response = self._auth_request('get', data)
# Check if user was redirected to the login view. # Check if user was redirected to the login view.
login_url_exists = settings.get('LOGIN_URL') in response['Location'] self.assertIn(settings.get('LOGIN_URL'), response['Location'])
self.assertEqual(login_url_exists, True)
def test_user_consent_inputs(self): def test_user_consent_inputs(self):
""" """
@ -183,10 +187,8 @@ class AuthorizationCodeFlowTestCase(TestCase):
# Because user doesn't allow app, SHOULD exists an error parameter # Because user doesn't allow app, SHOULD exists an error parameter
# in the query. # in the query.
self.assertEqual('error=' in response['Location'], True, self.assertIn('error=', response['Location'], msg='error param is missing in query.')
msg='error param is missing in query.') self.assertIn('access_denied', response['Location'], msg='"access_denied" code is missing in query.')
self.assertEqual('access_denied' in response['Location'], True,
msg='"access_denied" code is missing in query.')
# Simulate user authorization. # Simulate user authorization.
data['allow'] = 'Accept' # Will be the value of the button. data['allow'] = 'Accept' # Will be the value of the button.
@ -201,8 +203,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
# Check if the state is returned. # Check if the state is returned.
state = (response['Location'].split('state='))[1].split('&')[0] state = (response['Location'].split('state='))[1].split('&')[0]
self.assertEqual(state == self.state, True, self.assertEqual(state, self.state, msg='State change or is missing.')
msg='State change or is missing.')
def test_user_consent_skipped(self): def test_user_consent_skipped(self):
""" """
@ -227,8 +228,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True): with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True):
response = self._auth_request('post', data, is_user_authenticated=True) response = self._auth_request('post', data, is_user_authenticated=True)
self.assertEqual('code' in response['Location'], True, self.assertIn('code', response['Location'], msg='Code is missing in the returned url.')
msg='Code is missing in the returned url.')
response = self._auth_request('post', data, is_user_authenticated=True) response = self._auth_request('post', data, is_user_authenticated=True)
@ -274,7 +274,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True): with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True):
response = self._auth_request('get', data, is_user_authenticated=True) response = self._auth_request('get', data, is_user_authenticated=True)
self.assertEqual('Request for Permission' in response.content.decode('utf-8'), True) self.assertIn('Request for Permission', response.content.decode('utf-8'))
def test_prompt_parameter(self): def test_prompt_parameter(self):
""" """
@ -294,15 +294,15 @@ class AuthorizationCodeFlowTestCase(TestCase):
response = self._auth_request('get', data) response = self._auth_request('get', data)
# An error is returned if an End-User is not already authenticated. # An error is returned if an End-User is not already authenticated.
self.assertEqual('login_required' in response['Location'], True) self.assertIn('login_required', response['Location'])
response = self._auth_request('get', data, is_user_authenticated=True) response = self._auth_request('get', data, is_user_authenticated=True)
# 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.assertIn('interaction_required', response['Location'])
class AuthorizationImplicitFlowTestCase(TestCase): class AuthorizationImplicitFlowTestCase(TestCase, AuthorizeEndpointMixin):
""" """
Test cases for Authorization Endpoint using Implicit Flow. Test cases for Authorization Endpoint using Implicit Flow.
""" """
@ -318,26 +318,6 @@ class AuthorizationImplicitFlowTestCase(TestCase):
self.state = uuid.uuid4().hex self.state = uuid.uuid4().hex
self.nonce = 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): def test_missing_nonce(self):
""" """
The `nonce` parameter is REQUIRED if you use the Implicit Flow. The `nonce` parameter is REQUIRED if you use the Implicit Flow.
@ -352,9 +332,9 @@ class AuthorizationImplicitFlowTestCase(TestCase):
response = self._auth_request('get', data, is_user_authenticated=True) response = self._auth_request('get', data, is_user_authenticated=True)
self.assertEqual('#error=invalid_request' in response['Location'], True) self.assertIn('#error=invalid_request', response['Location'])
def test_id_token_token_response(self): def test_idtoken_token_response(self):
""" """
Implicit client requesting `id_token token` receives both id token Implicit client requesting `id_token token` receives both id token
and access token as the result of the authorization request. and access token as the result of the authorization request.
@ -384,7 +364,7 @@ class AuthorizationImplicitFlowTestCase(TestCase):
self.assertIn('access_token', response['Location']) self.assertIn('access_token', response['Location'])
self.assertIn('id_token', response['Location']) self.assertIn('id_token', response['Location'])
def test_id_token_response(self): def test_idtoken_response(self):
""" """
Implicit client requesting `id_token` receives Implicit client requesting `id_token` receives
only an id token as the result of the authorization request. only an id token as the result of the authorization request.
@ -414,7 +394,7 @@ class AuthorizationImplicitFlowTestCase(TestCase):
self.assertNotIn('access_token', response['Location']) self.assertNotIn('access_token', response['Location'])
self.assertIn('id_token', response['Location']) self.assertIn('id_token', response['Location'])
def test_id_token_token_at_hash(self): def test_idtoken_token_at_hash(self):
""" """
Implicit client requesting `id_token token` receives Implicit client requesting `id_token token` receives
`at_hash` in `id_token`. `at_hash` in `id_token`.
@ -440,7 +420,7 @@ class AuthorizationImplicitFlowTestCase(TestCase):
self.assertIn('at_hash', id_token) self.assertIn('at_hash', id_token)
def test_id_token_at_hash(self): def test_idtoken_at_hash(self):
""" """
Implicit client requesting `id_token` should not receive Implicit client requesting `id_token` should not receive
`at_hash` in `id_token`. `at_hash` in `id_token`.
@ -465,3 +445,56 @@ class AuthorizationImplicitFlowTestCase(TestCase):
id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload()
self.assertNotIn('at_hash', id_token) self.assertNotIn('at_hash', id_token)
class AuthorizationHybridFlowTestCase(TestCase, AuthorizeEndpointMixin):
"""
Test cases for Authorization Endpoint using Hybrid Flow.
"""
def setUp(self):
call_command('creatersakey')
self.factory = RequestFactory()
self.user = create_fake_user()
self.client_code_idtoken_token = create_fake_client(response_type='code id_token token', is_public=True)
self.state = uuid.uuid4().hex
self.nonce = uuid.uuid4().hex
# Base data for the auth request.
self.data = {
'client_id': self.client_code_idtoken_token.client_id,
'redirect_uri': self.client_code_idtoken_token.default_redirect_uri,
'response_type': self.client_code_idtoken_token.response_type,
'scope': 'openid email',
'state': self.state,
'nonce': self.nonce,
'allow': 'Accept',
}
def test_code_idtoken_token_response(self):
"""
Implicit client requesting `id_token token` receives both id token
and access token as the result of the authorization request.
"""
response = self._auth_request('post', self.data, is_user_authenticated=True)
self.assertIn('#', response['Location'])
self.assertIn('access_token', response['Location'])
self.assertIn('id_token', response['Location'])
self.assertIn('state', response['Location'])
self.assertIn('code', response['Location'])
# Validate code.
is_code_ok = is_code_valid(url=response['Location'],
user=self.user,
client=self.client_code_idtoken_token)
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
@override_settings(OIDC_TOKEN_EXPIRE=36000)
def test_access_token_expiration(self):
"""
Add ten hours of expiration to access_token. Check for the expires_in query in fragment.
"""
response = self._auth_request('post', self.data, is_user_authenticated=True)
self.assertIn('expires_in=36000', response['Location'])

View file

@ -10,7 +10,10 @@ except ImportError:
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, 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 django.utils import timezone
from jwkest.jwk import KEYS from jwkest.jwk import KEYS
@ -148,7 +151,7 @@ class TokenTestCase(TestCase):
self.assertEqual(response_dic['token_type'], 'bearer') self.assertEqual(response_dic['token_type'], 'bearer')
self.assertEqual(response_dic['expires_in'], 720) self.assertEqual(response_dic['expires_in'], 720)
self.assertEqual(id_token['sub'], str(self.user.id)) self.assertEqual(id_token['sub'], str(self.user.id))
self.assertEqual(id_token['aud'], self.client.client_id) self.assertEqual(id_token['aud'], self.client.client_id);
def test_refresh_token(self): def test_refresh_token(self):
""" """

View file

@ -34,14 +34,20 @@ class UserInfoTestCase(TestCase):
""" """
Generate a valid token. Generate a valid token.
""" """
id_token_dic = create_id_token(self.user, scope = ['openid', 'email'] + extra_scope
self.client.client_id, FAKE_NONCE)
id_token_dic = create_id_token(
user=self.user,
aud=self.client.client_id,
nonce=FAKE_NONCE,
scope=scope,
)
token = create_token( token = create_token(
user=self.user, user=self.user,
client=self.client, client=self.client,
id_token_dic=id_token_dic, id_token_dic=id_token_dic,
scope=['openid', 'email'] + extra_scope) scope=scope)
token.save() token.save()
return token return token

View file

@ -1,6 +1,6 @@
import logging import logging
from Crypto.PublicKey import RSA from Cryptodome.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
from django.http import JsonResponse from django.http import JsonResponse
@ -46,24 +46,24 @@ class AuthorizeView(View):
return hook_resp return hook_resp
if settings.get('OIDC_SKIP_CONSENT_ALWAYS') and not (authorize.client.client_type == 'public') \ if settings.get('OIDC_SKIP_CONSENT_ALWAYS') and not (authorize.client.client_type == 'public') \
and not (authorize.params.prompt == 'consent'): and not (authorize.params['prompt'] == 'consent'):
return redirect(authorize.create_response_uri()) return redirect(authorize.create_response_uri())
if settings.get('OIDC_SKIP_CONSENT_ENABLE'): if settings.get('OIDC_SKIP_CONSENT_ENABLE'):
# Check if user previously give consent. # Check if user previously give consent.
if authorize.client_has_user_consent() and not (authorize.client.client_type == 'public') \ if authorize.client_has_user_consent() and not (authorize.client.client_type == 'public') \
and not (authorize.params.prompt == 'consent'): and not (authorize.params['prompt'] == 'consent'):
return redirect(authorize.create_response_uri()) return redirect(authorize.create_response_uri())
if authorize.params.prompt == 'none': if authorize.params['prompt'] == 'none':
raise AuthorizeError(authorize.params.redirect_uri, 'interaction_required', authorize.grant_type) raise AuthorizeError(authorize.params['redirect_uri'], 'interaction_required', authorize.grant_type)
if authorize.params.prompt == 'login': if authorize.params['prompt'] == 'login':
return redirect_to_login(request.get_full_path()) return redirect_to_login(request.get_full_path())
if authorize.params.prompt == 'select_account': if authorize.params['prompt'] == 'select_account':
# TODO: see how we can support multiple accounts for the end-user. # TODO: see how we can support multiple accounts for the end-user.
raise AuthorizeError(authorize.params.redirect_uri, 'account_selection_required', authorize.grant_type) raise AuthorizeError(authorize.params['redirect_uri'], 'account_selection_required', authorize.grant_type)
# Generate hidden inputs for the form. # Generate hidden inputs for the form.
context = { context = {
@ -73,8 +73,8 @@ class AuthorizeView(View):
# Remove `openid` from scope list # Remove `openid` from scope list
# since we don't need to print it. # since we don't need to print it.
if 'openid' in authorize.params.scope: if 'openid' in authorize.params['scope']:
authorize.params.scope.remove('openid') authorize.params['scope'].remove('openid')
context = { context = {
'client': authorize.client, 'client': authorize.client,
@ -85,8 +85,8 @@ class AuthorizeView(View):
return render(request, 'oidc_provider/authorize.html', context) return render(request, 'oidc_provider/authorize.html', context)
else: else:
if authorize.params.prompt == 'none': if authorize.params['prompt'] == 'none':
raise AuthorizeError(authorize.params.redirect_uri, 'login_required', authorize.grant_type) raise AuthorizeError(authorize.params['redirect_uri'], 'login_required', authorize.grant_type)
return redirect_to_login(request.get_full_path()) return redirect_to_login(request.get_full_path())
@ -100,8 +100,8 @@ class AuthorizeView(View):
except (AuthorizeError) as error: except (AuthorizeError) as error:
uri = error.create_uri( uri = error.create_uri(
authorize.params.redirect_uri, authorize.params['redirect_uri'],
authorize.params.state) authorize.params['state'])
return redirect(uri) return redirect(uri)
@ -112,7 +112,7 @@ class AuthorizeView(View):
authorize.validate_params() authorize.validate_params()
if not request.POST.get('allow'): if not request.POST.get('allow'):
raise AuthorizeError(authorize.params.redirect_uri, raise AuthorizeError(authorize.params['redirect_uri'],
'access_denied', 'access_denied',
authorize.grant_type) authorize.grant_type)
@ -125,8 +125,8 @@ class AuthorizeView(View):
except (AuthorizeError) as error: except (AuthorizeError) as error:
uri = error.create_uri( uri = error.create_uri(
authorize.params.redirect_uri, authorize.params['redirect_uri'],
authorize.params.state) authorize.params['state'])
return redirect(uri) return redirect(uri)
@ -134,7 +134,6 @@ class AuthorizeView(View):
class TokenView(View): class TokenView(View):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
token = TokenEndpoint(request) token = TokenEndpoint(request)
try: try:

View file

@ -7,7 +7,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
setup( setup(
name='django-oidc-provider', name='django-oidc-provider',
version='0.3.7', version='0.4.1',
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',
@ -36,11 +36,11 @@ setup(
], ],
test_suite='runtests.runtests', test_suite='runtests.runtests',
tests_require=[ tests_require=[
'pyjwkest==1.1.0', 'pyjwkest==1.3.0',
'mock==2.0.0', 'mock==2.0.0',
], ],
install_requires=[ install_requires=[
'pyjwkest==1.1.0', 'pyjwkest==1.3.0',
], ],
) )