commit
c70dab13a8
48 changed files with 1472 additions and 597 deletions
|
@ -2,10 +2,15 @@ language: python
|
||||||
python:
|
python:
|
||||||
- "2.7"
|
- "2.7"
|
||||||
- "3.4"
|
- "3.4"
|
||||||
|
- "3.5"
|
||||||
env:
|
env:
|
||||||
- DJANGO=1.7
|
- DJANGO=1.7
|
||||||
- DJANGO=1.8
|
- DJANGO=1.8
|
||||||
- DJANGO=1.9
|
- DJANGO=1.9
|
||||||
|
matrix:
|
||||||
|
exclude:
|
||||||
|
- python: "3.5"
|
||||||
|
env: DJANGO=1.7
|
||||||
install:
|
install:
|
||||||
- pip install -q django==$DJANGO
|
- pip install -q django==$DJANGO
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
|
|
36
CHANGELOG.md
36
CHANGELOG.md
|
@ -4,8 +4,44 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### [Unreleased]
|
### [Unreleased]
|
||||||
|
|
||||||
|
### [0.3.6] - 2016-07-07
|
||||||
|
|
||||||
|
##### Changed
|
||||||
|
- OIDC_USERINFO setting.
|
||||||
|
|
||||||
|
### [0.3.5] - 2016-06-21
|
||||||
|
|
||||||
|
##### Added
|
||||||
|
- Field date_given in UserConsent model.
|
||||||
|
- Verbose names to all model fields.
|
||||||
|
- Customize scopes names and descriptions on authorize template.
|
||||||
|
|
||||||
|
##### Changed
|
||||||
|
- OIDC_EXTRA_SCOPE_CLAIMS setting.
|
||||||
|
|
||||||
|
### [0.3.4] - 2016-06-10
|
||||||
|
|
||||||
|
##### Changed
|
||||||
|
- Make SITE_URL setting optional.
|
||||||
|
|
||||||
|
##### Fixed
|
||||||
|
- Missing migration.
|
||||||
|
|
||||||
|
### [0.3.3] - 2016-05-03
|
||||||
|
|
||||||
|
##### Fixed
|
||||||
|
- Important bug with PKCE and form submit in Auth Request.
|
||||||
|
|
||||||
|
### [0.3.2] - 2016-04-26
|
||||||
|
|
||||||
##### Added
|
##### Added
|
||||||
- Choose type of client on creation.
|
- Choose type of client on creation.
|
||||||
|
- Implement Proof Key for Code Exchange by OAuth Public Clients.
|
||||||
|
- Support for prompt parameter.
|
||||||
|
- Support for different client JWT tokens algorithm.
|
||||||
|
|
||||||
|
##### Fixed
|
||||||
|
- Not auto-approve requests for non-confidential clients (publics).
|
||||||
|
|
||||||
### [0.3.1] - 2016-03-09
|
### [0.3.1] - 2016-03-09
|
||||||
|
|
||||||
|
|
|
@ -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/v0.2.x/CHANGELOG.md).
|
[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).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -23,5 +23,5 @@ We love contributions, so please feel free to fix bugs, improve things, provide
|
||||||
|
|
||||||
* Fork the project.
|
* Fork the project.
|
||||||
* Make your feature addition or bug fix.
|
* Make your feature addition or bug fix.
|
||||||
* Add tests for it inside `oidc_provider/tests`. Then run all and ensure everything is OK (read docs for how to test in all envs).
|
* Add tests for it inside `oidc_provider/tests`. Then run all and ensure everything is OK (read docs for how to test in all envs).
|
||||||
* Send pull request to the specific version branch.
|
* Send pull request to the specific version branch.
|
||||||
|
|
|
@ -53,9 +53,9 @@ author = u'Juan Ignacio Fiorentino'
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = u'0.2'
|
version = u'0.3'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = u'0.2.5'
|
release = u'0.3.x'
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
|
BIN
docs/images/client_creation.png
Normal file
BIN
docs/images/client_creation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
|
@ -3,13 +3,18 @@ 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.
|
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.
|
||||||
|
|
||||||
|
Also implements the following specifications:
|
||||||
|
|
||||||
|
* `OAuth 2.0 for Native Apps <https://tools.ietf.org/html/draft-ietf-oauth-native-apps-01>`_
|
||||||
|
* `Proof Key for Code Exchange by OAuth Public Clients <https://tools.ietf.org/html/rfc7636>`_
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
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 cover **Authorization Code Flow** and **Implicit Flow**, NO support for **Hybrid Flow** at this moment.
|
* This library covers **Authorization Code Flow** and **Implicit Flow**, NO support for **Hybrid Flow** at this moment.
|
||||||
* Only support for requesting Claims using Scope Values.
|
* Supports only for requesting Claims using Scope values.
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -17,12 +22,13 @@ Contents:
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
sections/installation
|
sections/installation
|
||||||
sections/clients
|
sections/relyingparties
|
||||||
sections/serverkeys
|
sections/serverkeys
|
||||||
sections/templates
|
sections/templates
|
||||||
sections/claims
|
sections/claims
|
||||||
|
sections/userconsent
|
||||||
sections/oauth2
|
sections/oauth2
|
||||||
sections/settings
|
sections/settings
|
||||||
sections/contribute
|
sections/contribute
|
||||||
|
@ -34,4 +40,3 @@ Indices and tables
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`modindex`
|
* :ref:`modindex`
|
||||||
* :ref:`search`
|
* :ref:`search`
|
||||||
|
|
||||||
|
|
|
@ -12,17 +12,17 @@ List of all the attributes grouped by scopes:
|
||||||
+--------------------+----------------+-----------------------+------------------------+
|
+--------------------+----------------+-----------------------+------------------------+
|
||||||
| profile | email | phone | address |
|
| profile | email | phone | address |
|
||||||
+====================+================+=======================+========================+
|
+====================+================+=======================+========================+
|
||||||
| name | email | phone_number | address_formatted |
|
| name | email | phone_number | formatted |
|
||||||
+--------------------+----------------+-----------------------+------------------------+
|
+--------------------+----------------+-----------------------+------------------------+
|
||||||
| given_name | email_verified | phone_number_verified | address_street_address |
|
| given_name | email_verified | phone_number_verified | street_address |
|
||||||
+--------------------+----------------+-----------------------+------------------------+
|
+--------------------+----------------+-----------------------+------------------------+
|
||||||
| family_name | | | address_locality |
|
| family_name | | | locality |
|
||||||
+--------------------+----------------+-----------------------+------------------------+
|
+--------------------+----------------+-----------------------+------------------------+
|
||||||
| middle_name | | | address_region |
|
| middle_name | | | region |
|
||||||
+--------------------+----------------+-----------------------+------------------------+
|
+--------------------+----------------+-----------------------+------------------------+
|
||||||
| nickname | | | address_postal_code |
|
| nickname | | | postal_code |
|
||||||
+--------------------+----------------+-----------------------+------------------------+
|
+--------------------+----------------+-----------------------+------------------------+
|
||||||
| preferred_username | | | address_country |
|
| preferred_username | | | country |
|
||||||
+--------------------+----------------+-----------------------+------------------------+
|
+--------------------+----------------+-----------------------+------------------------+
|
||||||
| profile | | | |
|
| profile | | | |
|
||||||
+--------------------+----------------+-----------------------+------------------------+
|
+--------------------+----------------+-----------------------+------------------------+
|
||||||
|
@ -41,35 +41,22 @@ List of all the attributes grouped by scopes:
|
||||||
| updated_at | | | |
|
| updated_at | | | |
|
||||||
+--------------------+----------------+-----------------------+------------------------+
|
+--------------------+----------------+-----------------------+------------------------+
|
||||||
|
|
||||||
Example using a django model::
|
Somewhere in your Django ``settings.py``::
|
||||||
|
|
||||||
from django.conf import settings
|
OIDC_USERINFO = 'myproject.oidc_provider_settings.userinfo'
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
|
|
||||||
class UserInfo(models.Model):
|
Then create the function for the ``OIDC_USERINFO`` setting::
|
||||||
|
|
||||||
GENDER_CHOICES = [
|
def userinfo(claims, user):
|
||||||
('F', 'Female'),
|
|
||||||
('M', 'Male'),
|
|
||||||
]
|
|
||||||
|
|
||||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True)
|
claims['name'] = '{0} {1}'.format(user.first_name, user.last_name)
|
||||||
|
claims['given_name'] = user.first_name
|
||||||
given_name = models.CharField(max_length=255, blank=True, null=True)
|
claims['family_name'] = user.last_name
|
||||||
family_name = models.CharField(max_length=255, blank=True, null=True)
|
claims['email'] = user.email
|
||||||
gender = models.CharField(max_length=100, choices=GENDER_CHOICES, null=True)
|
claims['address']['street_address'] = '...'
|
||||||
birthdate = models.DateField(null=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True, null=True)
|
|
||||||
|
|
||||||
email_verified = models.NullBooleanField(default=False)
|
return claims
|
||||||
|
|
||||||
phone_number = models.CharField(max_length=255, blank=True, null=True)
|
.. note::
|
||||||
phone_number_verified = models.NullBooleanField(default=False)
|
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.
|
||||||
|
|
||||||
address_locality = models.CharField(max_length=255, blank=True, null=True)
|
|
||||||
address_country = models.CharField(max_length=255, blank=True, null=True)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_user(cls, user):
|
|
||||||
return cls.objects.get(user=user)
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
.. _clients:
|
|
||||||
|
|
||||||
Clients
|
|
||||||
#######
|
|
||||||
|
|
||||||
Also known as Relying Parties (RP). User and client creation it's up to you. This is because is out of the scope in the core implementation of OIDC.
|
|
||||||
So, there are different ways to create your Clients. By displaying a HTML form or maybe if you have internal thrusted Clients you can create them programatically.
|
|
||||||
|
|
||||||
`Read more about client creation from OAuth2 spec <http://tools.ietf.org/html/rfc6749#section-2>`_
|
|
||||||
|
|
||||||
For your users, the tipical situation is that you provide them a login and a registration page.
|
|
||||||
|
|
||||||
If you want to test the provider without getting to deep into this topics you can:
|
|
||||||
|
|
||||||
Create a user with ``python manage.py createsuperuser`` and clients using Django admin:
|
|
||||||
|
|
||||||
.. image:: http://i64.tinypic.com/2dsfgoy.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
Or also you can create a client programmatically with Django shell ``python manage.py shell``::
|
|
||||||
|
|
||||||
>>> from oidc_provider.models import Client
|
|
||||||
>>> c = Client(name='Some Client', client_id='123', client_secret='456', response_type='code', redirect_uris=['http://example.com/'])
|
|
||||||
>>> c.save()
|
|
|
@ -7,7 +7,7 @@ We love contributions, so please feel free to fix bugs, improve things, provide
|
||||||
|
|
||||||
* Fork the project.
|
* Fork the project.
|
||||||
* Make your feature addition or bug fix.
|
* Make your feature addition or bug fix.
|
||||||
* Add tests for it inside ``oidc_provider/tests``. Then run all and ensure everything is OK (read docs for how to test in all envs).
|
* Add tests for it inside ``oidc_provider/tests``. Then run all and ensure everything is OK (read docs for how to test in all envs).
|
||||||
* Send pull request to the specific version branch.
|
* Send pull request to the specific version branch.
|
||||||
|
|
||||||
Running Tests
|
Running Tests
|
||||||
|
@ -15,10 +15,24 @@ Running Tests
|
||||||
|
|
||||||
Use `tox <https://pypi.python.org/pypi/tox>`_ for running tests in each of the environments, also to run coverage among::
|
Use `tox <https://pypi.python.org/pypi/tox>`_ for running tests in each of the environments, also to run coverage among::
|
||||||
|
|
||||||
|
# Run all tests.
|
||||||
$ tox
|
$ tox
|
||||||
|
|
||||||
|
# Run a particular test file with Python 2.7 and Django 1.9.
|
||||||
|
$ tox -e py27-django19 oidc_provider.tests.test_authorize_endpoint
|
||||||
|
|
||||||
If you have a Django project properly configured with the package. Then just run tests as normal::
|
If you have a Django project properly configured with the package. Then just run tests as normal::
|
||||||
|
|
||||||
$ python manage.py test --settings oidc_provider.tests.app.settings oidc_provider
|
$ python manage.py test --settings oidc_provider.tests.app.settings oidc_provider
|
||||||
|
|
||||||
Also tests run on every commit to the project, we use `travis <https://travis-ci.org/juanifioren/django-oidc-provider/>`_ for this.
|
Also tests run on every commit to the project, we use `travis <https://travis-ci.org/juanifioren/django-oidc-provider/>`_ for this.
|
||||||
|
|
||||||
|
Improve Documentation
|
||||||
|
=====================
|
||||||
|
|
||||||
|
We use `Sphinx <http://www.sphinx-doc.org/>`_ for generate this documentation. I you want to add or modify something just:
|
||||||
|
|
||||||
|
* Install Sphinx ``pip install sphinx`` and this theme ``pip install sphinx-rtd-theme``.
|
||||||
|
* Move inside the docs folder. ``cd docs/``
|
||||||
|
* Generate the HTML. ``make html``
|
||||||
|
* Open ``docs/_build/html/index.html`` on a browser.
|
||||||
|
|
|
@ -6,7 +6,7 @@ Installation
|
||||||
Requirements
|
Requirements
|
||||||
============
|
============
|
||||||
|
|
||||||
* Python: ``2.7`` ``3.4``
|
* Python: ``2.7`` ``3.4`` ``3.5``
|
||||||
* Django: ``1.7`` ``1.8`` ``1.9``
|
* Django: ``1.7`` ``1.8`` ``1.9``
|
||||||
|
|
||||||
Quick Installation
|
Quick Installation
|
||||||
|
@ -46,5 +46,4 @@ Generate server RSA key and run migrations (if you don't)::
|
||||||
|
|
||||||
Add required variables to your project settings::
|
Add required variables to your project settings::
|
||||||
|
|
||||||
SITE_URL = 'http://localhost:8000'
|
|
||||||
LOGIN_URL = '/accounts/login/'
|
LOGIN_URL = '/accounts/login/'
|
||||||
|
|
54
docs/sections/relyingparties.rst
Normal file
54
docs/sections/relyingparties.rst
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
.. _relyingparties:
|
||||||
|
|
||||||
|
Relying Parties
|
||||||
|
###############
|
||||||
|
|
||||||
|
Relying Parties (RP) creation it's up to you. This is because is out of the scope in the core implementation of OIDC.
|
||||||
|
So, there are different ways to create your Clients (RP). By displaying a HTML form or maybe if you have internal thrusted Clients you can create them programatically.
|
||||||
|
|
||||||
|
OAuth defines two client types, based on their ability to maintain the confidentiality of their client credentials:
|
||||||
|
|
||||||
|
* ``confidential``: Clients capable of maintaining the confidentiality of their credentials (e.g., client implemented on a secure server with restricted access to the client credentials).
|
||||||
|
* ``public``: Clients incapable of maintaining the confidentiality of their credentials (e.g., clients executing on the device used by the resource owner, such as an installed native application or a web browser-based application), and incapable of secure client authentication via any other means.
|
||||||
|
|
||||||
|
Properties
|
||||||
|
==========
|
||||||
|
|
||||||
|
* ``name``: Human-readable name for your client.
|
||||||
|
* ``client_type``: Values are ``confidential`` and ``public``.
|
||||||
|
* ``client_id``: Client unique identifier.
|
||||||
|
* ``client_secret``: Client secret for confidential applications.
|
||||||
|
* ``response_type``: Values are ``code``, ``id_token`` and ``id_token token``.
|
||||||
|
* ``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.
|
||||||
|
|
||||||
|
Using the admin
|
||||||
|
===============
|
||||||
|
|
||||||
|
We suggest you to use Django admin to easily manage your clients:
|
||||||
|
|
||||||
|
.. image:: ../images/client_creation.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
For re-generating ``client_secret``, when you are in the Client editing view, select "Client type" to be ``public``. Then after saving, select back to be ``confidential`` and save again.
|
||||||
|
|
||||||
|
Custom view
|
||||||
|
===========
|
||||||
|
|
||||||
|
If for some reason you need to create your own view to manage them, you can grab the form class that the admin makes use of. Located in ``oidc_provider.admin.ClientForm``.
|
||||||
|
|
||||||
|
Some built-in logic that comes with it:
|
||||||
|
|
||||||
|
* Automatic ``client_id`` and ``client_secret`` generation.
|
||||||
|
* Empty ``client_secret`` when ``client_type`` is equal to ``public``.
|
||||||
|
|
||||||
|
Programmatically
|
||||||
|
================
|
||||||
|
|
||||||
|
You can create a Client programmatically with Django shell ``python manage.py shell``::
|
||||||
|
|
||||||
|
>>> from oidc_provider.models import Client
|
||||||
|
>>> c = Client(name='Some Client', client_id='123', client_secret='456', response_type='code', redirect_uris=['http://example.com/'])
|
||||||
|
>>> c.save()
|
||||||
|
|
||||||
|
`Read more about client creation from OAuth2 spec <http://tools.ietf.org/html/rfc6749#section-2>`_
|
|
@ -5,13 +5,6 @@ Settings
|
||||||
|
|
||||||
Customize your provider so fit your project needs.
|
Customize your provider so fit your project needs.
|
||||||
|
|
||||||
SITE_URL
|
|
||||||
========
|
|
||||||
|
|
||||||
REQUIRED. ``str``. The OP server url.
|
|
||||||
|
|
||||||
For example ``http://localhost:8000``.
|
|
||||||
|
|
||||||
LOGIN_URL
|
LOGIN_URL
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
@ -19,6 +12,15 @@ REQUIRED. ``str``. Used to log the user in. `Read more in Django docs <https://d
|
||||||
|
|
||||||
``str``. Default is ``/accounts/login/``.
|
``str``. Default is ``/accounts/login/``.
|
||||||
|
|
||||||
|
SITE_URL
|
||||||
|
========
|
||||||
|
|
||||||
|
OPTIONAL. ``str``. The OP server url.
|
||||||
|
|
||||||
|
If not specified will be automatically generated using ``request.scheme`` and ``request.get_host()``.
|
||||||
|
|
||||||
|
For example ``http://localhost:8000``.
|
||||||
|
|
||||||
OIDC_AFTER_USERLOGIN_HOOK
|
OIDC_AFTER_USERLOGIN_HOOK
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
@ -44,34 +46,36 @@ Expressed in seconds. Default is ``60*10``.
|
||||||
OIDC_EXTRA_SCOPE_CLAIMS
|
OIDC_EXTRA_SCOPE_CLAIMS
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
OPTIONAL. ``str``. A string with the location of your class. Default is ``oidc_provider.lib.claims.AbstractScopeClaims``.
|
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 ``AbstractScopeClaims``.
|
Used to add extra scopes specific for your app. This class MUST inherit ``ScopeClaims``.
|
||||||
|
|
||||||
OpenID Connect Clients 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.
|
||||||
|
|
||||||
`Here <http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims>`_ you have the standard scopes defined by the protocol.
|
`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::
|
Check out an example of how to implement it::
|
||||||
|
|
||||||
from oidc_provider.lib.claims import AbstractScopeClaims
|
from django.utils.translation import ugettext as _
|
||||||
|
from oidc_provider.lib.claims import ScopeClaims
|
||||||
|
|
||||||
class MyAppScopeClaims(AbstractScopeClaims):
|
class MyAppScopeClaims(ScopeClaims):
|
||||||
|
|
||||||
def setup(self):
|
info_books = (
|
||||||
# Here you can load models that will be used
|
_(u'Books'), # Verbose name of the scope.
|
||||||
# in more than one scope for example.
|
_(u'Access to your books.'), # Description of the scope.
|
||||||
# print self.user
|
)
|
||||||
# print self.scopes
|
|
||||||
try:
|
|
||||||
self.some_model = SomeModel.objects.get(user=self.user)
|
|
||||||
except SomeModel.DoesNotExist:
|
|
||||||
# Create an empty model object.
|
|
||||||
self.some_model = SomeModel()
|
|
||||||
|
|
||||||
def scope_books(self, user):
|
def scope_books(self):
|
||||||
|
# Here, for example, you can search books for this user.
|
||||||
# Here 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 = {
|
dic = {
|
||||||
'books_readed': books_readed_count,
|
'books_readed': books_readed_count,
|
||||||
|
@ -79,11 +83,14 @@ Check out an example of how to implement it::
|
||||||
|
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
You can create our own scopes using the convention:
|
# If you want to change the description of the profile scope, you can redefine it.
|
||||||
|
info_profile = (
|
||||||
|
_(u'Profile'),
|
||||||
|
_(u'Another description.'),
|
||||||
|
)
|
||||||
|
|
||||||
``def scope_SCOPENAMEHERE(self, user):``
|
.. note::
|
||||||
|
If a field is empty or ``None`` inside the dictionary your return on ``scope_scopename`` method, it will be cleaned from the response.
|
||||||
If a field is empty or ``None`` will be cleaned from the response.
|
|
||||||
|
|
||||||
OIDC_IDTOKEN_EXPIRE
|
OIDC_IDTOKEN_EXPIRE
|
||||||
===================
|
===================
|
||||||
|
@ -100,9 +107,9 @@ OPTIONAL. ``str`` or ``(list, tuple)``.
|
||||||
A string with the location of your function hook or ``list`` or ``tuple`` with hook functions.
|
A string with the location of your function hook or ``list`` or ``tuple`` with hook functions.
|
||||||
Here you can add extra dictionary values specific for your app into id_token.
|
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 function receives a ``id_token`` dictionary and ``user`` instance
|
||||||
and returns it with additional fields.
|
and returns it with additional fields.
|
||||||
|
|
||||||
Default is::
|
Default is::
|
||||||
|
@ -155,4 +162,21 @@ Expressed in seconds. Default is ``60*60``.
|
||||||
OIDC_USERINFO
|
OIDC_USERINFO
|
||||||
=============
|
=============
|
||||||
|
|
||||||
OPTIONAL. ``str``. A string with the location of your class. Read **Standard Claims** section.
|
OPTIONAL. ``str``. A string with the location of your function. Read **Standard Claims** section.
|
||||||
|
|
||||||
|
The function receives a ``claims`` dictionary with all the standard claims and ``user`` instance. Must returns the ``claims`` dict again.
|
||||||
|
|
||||||
|
Example usage::
|
||||||
|
|
||||||
|
def userinfo(claims, user):
|
||||||
|
|
||||||
|
claims['name'] = '{0} {1}'.format(user.first_name, user.last_name)
|
||||||
|
claims['given_name'] = user.first_name
|
||||||
|
claims['family_name'] = user.last_name
|
||||||
|
claims['email'] = user.email
|
||||||
|
claims['address']['street_address'] = '...'
|
||||||
|
|
||||||
|
return claims
|
||||||
|
|
||||||
|
.. 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.
|
||||||
|
|
19
docs/sections/userconsent.rst
Normal file
19
docs/sections/userconsent.rst
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
.. _userconsent:
|
||||||
|
|
||||||
|
User Consent
|
||||||
|
############
|
||||||
|
|
||||||
|
The package store some information after the user grant access to some client. For example, you can use the ``UserConsent`` model to list applications that the user have authorized access. Like Google does `here <https://security.google.com/settings/security/permissions>`_.
|
||||||
|
|
||||||
|
>>> from oidc_provider.models import UserConsent
|
||||||
|
>>> UserConsent.objects.filter(user__email='some@email.com')
|
||||||
|
[<UserConsent: Example Client - some@email.com>]
|
||||||
|
|
||||||
|
Properties
|
||||||
|
==========
|
||||||
|
|
||||||
|
* ``user``: Django user object.
|
||||||
|
* ``client``: Relying Party object.
|
||||||
|
* ``expires_at``: Expiration date of the consent.
|
||||||
|
* ``scope``: Scopes authorized.
|
||||||
|
* ``date_given``: Date of the authorization.
|
2
example_project/.gitignore
vendored
2
example_project/.gitignore
vendored
|
@ -1,3 +1,3 @@
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.pem
|
*.pem
|
||||||
|
static/
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Example Project
|
# Example Project
|
||||||
|
|
||||||
![Example Project](http://s12.postimg.org/e4uwlsi0d/Screenshot_from_2016_02_02_13_15_26.png)
|
![Example Project](http://i.imgur.com/IK3OZjx.png)
|
||||||
|
|
||||||
Run your own OIDC provider in a second. This is a Django app with all the necessary things to work with `django-oidc-provider` package.
|
Run your own OIDC provider in a second. This is a Django app with all the necessary things to work with `django-oidc-provider` package.
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,800);
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Open Sans', sans-serif;
|
background-color: #536dfe;
|
||||||
font-weight: 400;
|
display: flex;
|
||||||
height: auto;
|
min-height: 100vh;
|
||||||
padding-top: 10px;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.huge.header {
|
#main-container {
|
||||||
font-size: 46px;
|
flex: 1 0 auto;
|
||||||
font-weight: 800;
|
padding-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.segment {
|
footer {
|
||||||
font-size: 18px;
|
padding-top: 0px !important;
|
||||||
font-weight: 300;
|
|
||||||
}
|
}
|
|
@ -6,42 +6,46 @@
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
||||||
|
|
||||||
<title>OIDC Provider Example</title>
|
<title>OpenID Provider Example</title>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/semantic.min.css">
|
<link rel="stylesheet" href="http://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/css/materialize.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/custom.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'css/custom.css' %}">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<div class="ui page grid fixed large blue inverted menu">
|
|
||||||
<a href="{% url 'home' %}" class=" item">django-oidc-provider</a>
|
|
||||||
<div class="right menu">
|
|
||||||
{% if user.is_authenticated %}
|
|
||||||
<a href="#" class="item">{{ user.email }}</a>
|
|
||||||
{% if user.is_superuser %}
|
|
||||||
<a href="{% url 'admin:index' %}" class="item">Admin</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url 'logout' %}" class="item"><i class="remove icon"></i></a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'login' %}" class="item">Login</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
<div class="navbar-fixed">
|
||||||
|
<nav class="white">
|
||||||
<div class="ui stackable page grid">
|
<div class="nav-wrapper">
|
||||||
<div class="column">
|
<a href="{% url 'home' %}" class="brand-logo center black-text">OpenID Provider</a>
|
||||||
<div class="ui right aligned secondary segment">
|
<ul id="nav-mobile" class="right hide-on-med-and-down">
|
||||||
View the project <a href="https://github.com/juanifioren/django-oidc-provider" target="_BLANK">on Github</a>. Example by <a href="https://github.com/juanifioren" target="_BLANK">Juan Ignacio Fiorentino</a>.
|
{% if user.is_authenticated %}
|
||||||
|
{% if user.is_superuser %}
|
||||||
|
<li><a href="{% url 'admin:index' %}" class="black-text">{% trans 'Admin' %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><a href="{% url 'logout' %}" class="black-text">X</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{% url 'login' %}" class="black-text"><i class="material-icons left">input</i> {% trans 'Login' %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
<div id="main-container" class="container">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/semantic.min.js"></script>
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="page-footer white">
|
||||||
|
<div class="footer-copyright">
|
||||||
|
<div class="container black-text"><a href="https://www.linkedin.com/in/juanifioren" target="_BLANK">Created by Juan Ignacio Fiorentino</a><a class="right" href="https://github.com/juanifioren/django-oidc-provider" target="_BLANK">View on Github</a></div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/js/materialize.min.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -4,46 +4,21 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="ui stackable page grid">
|
<div class="row">
|
||||||
<div class="row">
|
<div class="col s12 m10 offset-m1">
|
||||||
<div class="center aligned column">
|
<div class="card hoverable">
|
||||||
<i class="thumbs outline up massive icon"></i>
|
<div class="card-content">
|
||||||
<h1 class="ui huge header">Congratulations! It works. <div class="sub header">... what's next?</div></h1>
|
<h1 class="center-align flow-text indigo-text text-lighten-1">Example of an OpenID Connect 1.0 Provider. Built with the <a href="https://www.djangoproject.com/" target="_BLANK"><u>Django Framework</u></a> and <a href="https://github.com/juanifioren/django-oidc-provider" target="_BLANK"><u>django-oidc-provider</u></a> package.</h1>
|
||||||
</div>
|
<p class="flow-text">Start by creating your clients <a href="{% url 'admin:index' %}oidc_provider/client/">here</a>.</p>
|
||||||
</div>
|
<p class="flow-text">Also check that you've created at least one server key, do it <a href="{% url 'admin:index' %}oidc_provider/rsakey/">here</a>.</p>
|
||||||
<div class="row">
|
<div class="collection with-header">
|
||||||
<div class="column">
|
<div class="collection-header"><h4>Server Endpoints</h4></div>
|
||||||
<div class="ui segments">
|
<a href="{% url 'oidc_provider:provider_info' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:provider_info' %}</a>
|
||||||
<div class="ui segment">
|
<a href="{% url 'oidc_provider:jwks' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:jwks' %}</a>
|
||||||
<p>Now that you are an OpenID Connect Provider, start by creating your clients <a href="{% url 'admin:index' %}oidc_provider/client/" target="_BLANK">here</a>.</p>
|
<a href="{% url 'oidc_provider:authorize' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:authorize' %}</a>
|
||||||
<p>Also check that you've created at least one server key, do it <a href="{% url 'admin:index' %}oidc_provider/rsakey/" target="_BLANK">here</a>.</p>
|
<a href="{% url 'oidc_provider:token' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:token' %}</a>
|
||||||
<h2 class="ui header">Server Endpoints</h2>
|
<a href="{% url 'oidc_provider:userinfo' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:userinfo' %}</a>
|
||||||
<div class="ui list">
|
<a href="{% url 'oidc_provider:logout' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:logout' %}</a>
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:provider_info' %}" class="header">{% url 'oidc_provider:provider_info' %}</a>
|
|
||||||
<div class="description">The configuration information of the provider. <a href="http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:jwks' %}" class="header">{% url 'oidc_provider:jwks' %}</a>
|
|
||||||
<div class="description">JavaScript Object Notation (JSON) data structure that represents a cryptographic key.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:authorize' %}" class="header">{% url 'oidc_provider:authorize' %}</a>
|
|
||||||
<div class="description">This endpoint performs Authentication of the End-User. <a href="http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:token' %}" class="header">{% url 'oidc_provider:token' %}</a>
|
|
||||||
<div class="description">Used to obtain an Access Token, an ID Token, and optionally a Refresh Token. <a href="http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:userinfo' %}" class="header">{% url 'oidc_provider:userinfo' %}</a>
|
|
||||||
<div class="description">OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User. <a href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:logout' %}" class="header">{% url 'oidc_provider:logout' %}</a>
|
|
||||||
<div class="description">Used to notify the OP that the End-User has logged out of the site. <a href="http://openid.net/specs/openid-connect-session-1_0.html#RPLogout" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,28 +1,38 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="ui page grid">
|
<div class="row">
|
||||||
<div class="row">
|
<div class="col s12 m8 offset-m2 l6 offset-l3">
|
||||||
<div class="six wide centered column">
|
<div class="card hoverable">
|
||||||
{% if form.errors %}
|
<div class="card-content">
|
||||||
<div class="ui negative message">
|
<div class="row">
|
||||||
<p>Your username and password didn't match. Please try again.</p>
|
{% if form.errors %}
|
||||||
|
<h5 class="red-text text-darken-1 flow-text center-align">Your username and password didn't match. Please try again.</h5>
|
||||||
|
{% endif %}
|
||||||
|
<form class="col s12" method="post" action="{% url 'login' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
<div class="row">
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<i class="material-icons prefix">account_circle</i>
|
||||||
|
<input id="username" name="username" type="text" class="validate">
|
||||||
|
<label for="username">{% trans 'Username' %}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<i class="material-icons prefix">lock</i>
|
||||||
|
<input id="password" name="password" type="password" class="validate">
|
||||||
|
<label for="password">{% trans 'Password' %}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input class="waves-effect waves-light btn-large right green" type="submit" value="{% trans 'Enter' %}" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
<form class="ui form segment" method="post" action="{% url 'login' %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="next" value="{{ next }}">
|
|
||||||
<div class="field">
|
|
||||||
<label>Username</label>
|
|
||||||
<input type="text" name="username">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Password</label>
|
|
||||||
<input type="password" name="password">
|
|
||||||
</div>
|
|
||||||
<input class="ui submit big primary fluid button" type="submit" value="Enter" />
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,25 +2,24 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="ui page grid">
|
<div class="row">
|
||||||
<div class="nine wide centered column">
|
<div class="col s12 m8 offset-m2 l6 offset-l3">
|
||||||
<div class="ui segment">
|
<h4 class="grey-text text-lighten-3">Request for Permission</h4>
|
||||||
<h1 class="ui dividing header">Request for Permission</h1>
|
<div class="card hoverable">
|
||||||
<p>Client <i>{{ client.name }}</i> would like to access this information of you.</p>
|
<div class="card-content">
|
||||||
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
<p class="flow-text">Client <i>{{ client.name }}</i> would like to access this information of you.</p>
|
||||||
{% csrf_token %}
|
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
||||||
{{ hidden_inputs }}
|
{% csrf_token %}
|
||||||
<div class="ui bulleted list">
|
{{ hidden_inputs }}
|
||||||
{% for scope in params.scope %}
|
<ul class="collection">
|
||||||
<div class="item">{{ scope | capfirst }}</div>
|
{% for scope in params.scope %}
|
||||||
{% endfor %}
|
<li class="collection-item">{{ scope | capfirst }}</li>
|
||||||
</div>
|
{% endfor %}
|
||||||
<div class="ui fluid large buttons">
|
</ul>
|
||||||
<input class="ui button" type="submit" value="Cancel" />
|
<input class="waves-effect waves-light btn grey" type="submit" value="Cancel" />
|
||||||
<div class="or"></div>
|
<input name="allow" class="waves-effect waves-light btn green right" type="submit" value="Authorize" />
|
||||||
<input name="allow" class="positive ui button" type="submit" value="Authorize" />
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="ui page grid">
|
<div class="row">
|
||||||
<div class="nine wide centered column">
|
<div class="col s12 m8 offset-m2 l6 offset-l3">
|
||||||
<div class="ui icon negative large message">
|
<div class="card hoverable">
|
||||||
<i class="meh icon"></i>
|
<div class="card-content">
|
||||||
<div class="content">
|
<h4 class="center-align red-text text-lighten-1">{{ error }}</h4>
|
||||||
<div class="header">{{ error }}</div>
|
|
||||||
<p>{{ description }}</p>
|
<p>{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ urlpatterns = [
|
||||||
url(r'^accounts/login/$', auth_views.login, { 'template_name': 'login.html' }, name='login'),
|
url(r'^accounts/login/$', auth_views.login, { 'template_name': 'login.html' }, name='login'),
|
||||||
url(r'^accounts/logout/$', auth_views.logout, { 'next_page': '/' }, name='logout'),
|
url(r'^accounts/logout/$', auth_views.logout, { 'next_page': '/' }, name='logout'),
|
||||||
|
|
||||||
url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
|
url(r'^', include('oidc_provider.urls', namespace='oidc_provider')),
|
||||||
|
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,17 +3,22 @@ from django.utils.translation import ugettext as _
|
||||||
from oidc_provider import settings
|
from oidc_provider import settings
|
||||||
|
|
||||||
|
|
||||||
class AbstractScopeClaims(object):
|
STANDARD_CLAIMS = {
|
||||||
|
'name': '', 'given_name': '', 'family_name': '', 'middle_name': '', 'nickname': '',
|
||||||
|
'preferred_username': '', 'profile': '', 'picture': '', 'website': '', 'gender': '',
|
||||||
|
'birthdate': '', 'zoneinfo': '', 'locale': '', 'updated_at': '', 'email': '', 'email_verified': '',
|
||||||
|
'phone_number': '', 'phone_number_verified': '', 'address': { 'formatted': '',
|
||||||
|
'street_address': '', 'locality': '', 'region': '', 'postal_code': '', 'country': '', },
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ScopeClaims(object):
|
||||||
|
|
||||||
def __init__(self, user, scopes):
|
def __init__(self, user, scopes):
|
||||||
self.user = user
|
self.user = user
|
||||||
|
self.userinfo = settings.get('OIDC_USERINFO', import_str=True)(STANDARD_CLAIMS, self.user)
|
||||||
self.scopes = scopes
|
self.scopes = scopes
|
||||||
|
|
||||||
self.setup()
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create_response_dic(self):
|
def create_response_dic(self):
|
||||||
"""
|
"""
|
||||||
Generate the dic that will be jsonify. Checking scopes given vs
|
Generate the dic that will be jsonify. Checking scopes given vs
|
||||||
|
@ -25,7 +30,7 @@ class AbstractScopeClaims(object):
|
||||||
|
|
||||||
for scope in self.scopes:
|
for scope in self.scopes:
|
||||||
if scope in self._scopes_registered():
|
if scope in self._scopes_registered():
|
||||||
dic.update(getattr(self, 'scope_' + scope)(self.user))
|
dic.update(getattr(self, 'scope_' + scope)())
|
||||||
|
|
||||||
dic = self._clean_dic(dic)
|
dic = self._clean_dic(dic)
|
||||||
|
|
||||||
|
@ -39,7 +44,6 @@ class AbstractScopeClaims(object):
|
||||||
scopes = []
|
scopes = []
|
||||||
|
|
||||||
for name in self.__class__.__dict__:
|
for name in self.__class__.__dict__:
|
||||||
|
|
||||||
if name.startswith('scope_'):
|
if name.startswith('scope_'):
|
||||||
scope = name.split('scope_')[1]
|
scope = name.split('scope_')[1]
|
||||||
scopes.append(scope)
|
scopes.append(scope)
|
||||||
|
@ -60,65 +64,91 @@ class AbstractScopeClaims(object):
|
||||||
|
|
||||||
return aux_dic
|
return aux_dic
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_scopes_info(cls, scopes=[]):
|
||||||
|
scopes_info = []
|
||||||
|
|
||||||
class StandardScopeClaims(AbstractScopeClaims):
|
for name in cls.__dict__:
|
||||||
|
if name.startswith('info_'):
|
||||||
|
scope_name = name.split('info_')[1]
|
||||||
|
if scope_name in scopes:
|
||||||
|
touple_info = getattr(cls, name)
|
||||||
|
scopes_info.append({
|
||||||
|
'scope': scope_name,
|
||||||
|
'name': touple_info[0],
|
||||||
|
'description': touple_info[1],
|
||||||
|
})
|
||||||
|
|
||||||
|
return scopes_info
|
||||||
|
|
||||||
|
|
||||||
|
class StandardScopeClaims(ScopeClaims):
|
||||||
"""
|
"""
|
||||||
Based on OpenID Standard Claims.
|
Based on OpenID Standard Claims.
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
See: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setup(self):
|
info_profile = (
|
||||||
try:
|
_(u'Basic profile'),
|
||||||
self.userinfo = settings.get('OIDC_USERINFO',
|
_(u'Access to your basic information. Includes names, gender, birthdate and other information.'),
|
||||||
import_str=True).get_by_user(self.user)
|
)
|
||||||
except:
|
def scope_profile(self):
|
||||||
self.userinfo = None
|
|
||||||
|
|
||||||
def scope_profile(self, user):
|
|
||||||
dic = {
|
dic = {
|
||||||
'name': getattr(self.userinfo, 'name', None),
|
'name': self.userinfo.get('name'),
|
||||||
'given_name': getattr(self.userinfo, 'given_name', None),
|
'given_name': self.userinfo.get('given_name'),
|
||||||
'family_name': getattr(self.userinfo, 'family_name', None),
|
'family_name': self.userinfo.get('family_name'),
|
||||||
'middle_name': getattr(self.userinfo, 'middle_name', None),
|
'middle_name': self.userinfo.get('middle_name'),
|
||||||
'nickname': getattr(self.userinfo, 'nickname', None),
|
'nickname': self.userinfo.get('nickname'),
|
||||||
'preferred_username': getattr(self.userinfo, 'preferred_username', None),
|
'preferred_username': self.userinfo.get('preferred_username'),
|
||||||
'profile': getattr(self.userinfo, 'profile', None),
|
'profile': self.userinfo.get('profile'),
|
||||||
'picture': getattr(self.userinfo, 'picture', None),
|
'picture': self.userinfo.get('picture'),
|
||||||
'website': getattr(self.userinfo, 'website', None),
|
'website': self.userinfo.get('website'),
|
||||||
'gender': getattr(self.userinfo, 'gender', None),
|
'gender': self.userinfo.get('gender'),
|
||||||
'birthdate': getattr(self.userinfo, 'birthdate', None),
|
'birthdate': self.userinfo.get('birthdate'),
|
||||||
'zoneinfo': getattr(self.userinfo, 'zoneinfo', None),
|
'zoneinfo': self.userinfo.get('zoneinfo'),
|
||||||
'locale': getattr(self.userinfo, 'locale', None),
|
'locale': self.userinfo.get('locale'),
|
||||||
'updated_at': getattr(self.userinfo, 'updated_at', None),
|
'updated_at': self.userinfo.get('updated_at'),
|
||||||
}
|
}
|
||||||
|
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
def scope_email(self, user):
|
info_email = (
|
||||||
|
_(u'Email'),
|
||||||
|
_(u'Access to your email address.'),
|
||||||
|
)
|
||||||
|
def scope_email(self):
|
||||||
dic = {
|
dic = {
|
||||||
'email': getattr(self.user, 'email', None),
|
'email': self.userinfo.get('email'),
|
||||||
'email_verified': getattr(self.userinfo, 'email_verified', None),
|
'email_verified': self.userinfo.get('email_verified'),
|
||||||
}
|
}
|
||||||
|
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
def scope_phone(self, user):
|
info_phone = (
|
||||||
|
_(u'Phone number'),
|
||||||
|
_(u'Access to your phone number.'),
|
||||||
|
)
|
||||||
|
def scope_phone(self):
|
||||||
dic = {
|
dic = {
|
||||||
'phone_number': getattr(self.userinfo, 'phone_number', None),
|
'phone_number': self.userinfo.get('phone_number'),
|
||||||
'phone_number_verified': getattr(self.userinfo, 'phone_number_verified', None),
|
'phone_number_verified': self.userinfo.get('phone_number_verified'),
|
||||||
}
|
}
|
||||||
|
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
def scope_address(self, user):
|
info_address = (
|
||||||
|
_(u'Address information'),
|
||||||
|
_(u'Access to your address. Includes country, locality, street and other information.'),
|
||||||
|
)
|
||||||
|
def scope_address(self):
|
||||||
dic = {
|
dic = {
|
||||||
'address': {
|
'address': {
|
||||||
'formatted': getattr(self.userinfo, 'address_formatted', None),
|
'formatted': self.userinfo.get('address', {}).get('formatted'),
|
||||||
'street_address': getattr(self.userinfo, 'address_street_address', None),
|
'street_address': self.userinfo.get('address', {}).get('street_address'),
|
||||||
'locality': getattr(self.userinfo, 'address_locality', None),
|
'locality': self.userinfo.get('address', {}).get('locality'),
|
||||||
'region': getattr(self.userinfo, 'address_region', None),
|
'region': self.userinfo.get('address', {}).get('region'),
|
||||||
'postal_code': getattr(self.userinfo, 'address_postal_code', None),
|
'postal_code': self.userinfo.get('address', {}).get('postal_code'),
|
||||||
'country': getattr(self.userinfo, 'address_country', None),
|
'country': self.userinfo.get('address', {}).get('country'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
import logging
|
||||||
try:
|
try:
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
@ -8,6 +7,7 @@ except ImportError:
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from oidc_provider.lib.claims import StandardScopeClaims
|
||||||
from oidc_provider.lib.errors import *
|
from oidc_provider.lib.errors import *
|
||||||
from oidc_provider.lib.utils.params import *
|
from oidc_provider.lib.utils.params import *
|
||||||
from oidc_provider.lib.utils.token import *
|
from oidc_provider.lib.utils.token import *
|
||||||
|
@ -35,7 +35,7 @@ class AuthorizeEndpoint(object):
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
|
@ -54,38 +54,50 @@ class AuthorizeEndpoint(object):
|
||||||
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.code_challenge = query_dict.get('code_challenge', '')
|
||||||
|
self.params.code_challenge_method = query_dict.get('code_challenge_method', '')
|
||||||
|
|
||||||
def validate_params(self):
|
def validate_params(self):
|
||||||
|
# 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.
|
||||||
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()
|
||||||
|
|
||||||
if not self.grant_type:
|
|
||||||
logger.debug('[Authorize] Invalid response type: %s', self.params.response_type)
|
|
||||||
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type',
|
|
||||||
self.grant_type)
|
|
||||||
|
|
||||||
if self.is_authentication and self.grant_type == 'implicit' and not self.params.nonce:
|
|
||||||
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
|
||||||
self.grant_type)
|
|
||||||
|
|
||||||
if self.is_authentication and self.params.response_type != self.client.response_type:
|
|
||||||
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
|
||||||
self.grant_type)
|
|
||||||
|
|
||||||
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.
|
||||||
|
if not self.grant_type:
|
||||||
|
logger.debug('[Authorize] Invalid response type: %s', self.params.response_type)
|
||||||
|
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type',
|
||||||
|
self.grant_type)
|
||||||
|
|
||||||
|
# Nonce parameter validation.
|
||||||
|
if self.is_authentication and self.grant_type == 'implicit' and not self.params.nonce:
|
||||||
|
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
||||||
|
self.grant_type)
|
||||||
|
|
||||||
|
# Response type parameter validation.
|
||||||
|
if self.is_authentication and self.params.response_type != self.client.response_type:
|
||||||
|
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
||||||
|
self.grant_type)
|
||||||
|
|
||||||
|
# PKCE validation of the transformation method.
|
||||||
|
if self.params.code_challenge:
|
||||||
|
if not (self.params.code_challenge_method in ['plain', 'S256']):
|
||||||
|
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)
|
||||||
|
@ -99,8 +111,10 @@ class AuthorizeEndpoint(object):
|
||||||
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_method=self.params.code_challenge_method)
|
||||||
|
|
||||||
code.save()
|
code.save()
|
||||||
|
|
||||||
query_params['code'] = code.code
|
query_params['code'] = code.code
|
||||||
|
@ -112,8 +126,9 @@ class AuthorizeEndpoint(object):
|
||||||
id_token_dic = create_id_token(
|
id_token_dic = create_id_token(
|
||||||
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,
|
||||||
query_fragment['id_token'] = encode_id_token(id_token_dic)
|
request=self.request)
|
||||||
|
query_fragment['id_token'] = encode_id_token(id_token_dic, self.client)
|
||||||
else:
|
else:
|
||||||
id_token_dic = {}
|
id_token_dic = {}
|
||||||
|
|
||||||
|
@ -155,18 +170,24 @@ class AuthorizeEndpoint(object):
|
||||||
|
|
||||||
Return None.
|
Return None.
|
||||||
"""
|
"""
|
||||||
expires_at = timezone.now() + timedelta(
|
date_given = timezone.now()
|
||||||
|
expires_at = date_given + timedelta(
|
||||||
days=settings.get('OIDC_SKIP_CONSENT_EXPIRE'))
|
days=settings.get('OIDC_SKIP_CONSENT_EXPIRE'))
|
||||||
|
|
||||||
uc, created = UserConsent.objects.get_or_create(
|
uc, created = UserConsent.objects.get_or_create(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
client=self.client,
|
client=self.client,
|
||||||
defaults={'expires_at': expires_at})
|
defaults={
|
||||||
|
'expires_at': expires_at,
|
||||||
|
'date_given': date_given,
|
||||||
|
}
|
||||||
|
)
|
||||||
uc.scope = self.params.scope
|
uc.scope = self.params.scope
|
||||||
|
|
||||||
# Rewrite expires_at if object already exists.
|
# Rewrite expires_at and date_given if object already exists.
|
||||||
if not created:
|
if not created:
|
||||||
uc.expires_at = expires_at
|
uc.expires_at = expires_at
|
||||||
|
uc.date_given = date_given
|
||||||
|
|
||||||
uc.save()
|
uc.save()
|
||||||
|
|
||||||
|
@ -187,3 +208,19 @@ class AuthorizeEndpoint(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def get_scopes_information(self):
|
||||||
|
"""
|
||||||
|
Return a list with the description of all the scopes requested.
|
||||||
|
"""
|
||||||
|
scopes = StandardScopeClaims.get_scopes_info(self.params.scope)
|
||||||
|
if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'):
|
||||||
|
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, scope in enumerate(scopes[:]):
|
||||||
|
if scope_extra['scope'] == scope['scope']:
|
||||||
|
del scopes[index]
|
||||||
|
else:
|
||||||
|
scopes_extra = []
|
||||||
|
|
||||||
|
return scopes + scopes_extra
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from base64 import b64decode
|
from base64 import b64decode, urlsafe_b64encode
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
try:
|
try:
|
||||||
|
@ -30,14 +31,16 @@ class TokenEndpoint(object):
|
||||||
|
|
||||||
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.params.redirect_uri = unquote(self.request.POST.get('redirect_uri', ''))
|
||||||
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 parameters.
|
||||||
|
self.params.code_verifier = self.request.POST.get('code_verifier')
|
||||||
|
|
||||||
def _extract_client_auth(self):
|
def _extract_client_auth(self):
|
||||||
"""
|
"""
|
||||||
Get client credentials using HTTP Basic Authentication method.
|
Get client credentials using HTTP Basic Authentication method.
|
||||||
|
@ -68,10 +71,11 @@ class TokenEndpoint(object):
|
||||||
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 not (self.client.client_secret == self.params.client_secret):
|
if self.client.client_type == 'confidential':
|
||||||
logger.debug('[Token] Invalid client secret: client %s do not have secret %s',
|
if not (self.client.client_secret == self.params.client_secret):
|
||||||
self.client.client_id, self.client.client_secret)
|
logger.debug('[Token] Invalid client secret: client %s do not have secret %s',
|
||||||
raise TokenError('invalid_client')
|
self.client.client_id, self.client.client_secret)
|
||||||
|
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):
|
||||||
|
@ -90,6 +94,19 @@ class TokenEndpoint(object):
|
||||||
self.params.redirect_uri)
|
self.params.redirect_uri)
|
||||||
raise TokenError('invalid_grant')
|
raise TokenError('invalid_grant')
|
||||||
|
|
||||||
|
# Validate PKCE parameters.
|
||||||
|
if self.params.code_verifier:
|
||||||
|
if self.code.code_challenge_method == 'S256':
|
||||||
|
new_code_challenge = urlsafe_b64encode(
|
||||||
|
hashlib.sha256(self.params.code_verifier.encode('ascii')).digest()
|
||||||
|
).decode('utf-8').replace('=', '')
|
||||||
|
else:
|
||||||
|
new_code_challenge = self.params.code_verifier
|
||||||
|
|
||||||
|
# TODO: We should explain the error.
|
||||||
|
if not (new_code_challenge == self.code.code_challenge):
|
||||||
|
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')
|
||||||
|
@ -119,6 +136,7 @@ class TokenEndpoint(object):
|
||||||
user=self.code.user,
|
user=self.code.user,
|
||||||
aud=self.client.client_id,
|
aud=self.client.client_id,
|
||||||
nonce=self.code.nonce,
|
nonce=self.code.nonce,
|
||||||
|
request=self.request,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
id_token_dic = {}
|
id_token_dic = {}
|
||||||
|
@ -140,7 +158,7 @@ class TokenEndpoint(object):
|
||||||
'refresh_token': token.refresh_token,
|
'refresh_token': token.refresh_token,
|
||||||
'token_type': 'bearer',
|
'token_type': 'bearer',
|
||||||
'expires_in': settings.get('OIDC_TOKEN_EXPIRE'),
|
'expires_in': settings.get('OIDC_TOKEN_EXPIRE'),
|
||||||
'id_token': encode_id_token(id_token_dic),
|
'id_token': encode_id_token(id_token_dic, token.client),
|
||||||
}
|
}
|
||||||
|
|
||||||
return dic
|
return dic
|
||||||
|
@ -152,6 +170,7 @@ class TokenEndpoint(object):
|
||||||
user=self.token.user,
|
user=self.token.user,
|
||||||
aud=self.client.client_id,
|
aud=self.client.client_id,
|
||||||
nonce=None,
|
nonce=None,
|
||||||
|
request=self.request,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
id_token_dic = {}
|
id_token_dic = {}
|
||||||
|
@ -173,7 +192,7 @@ class TokenEndpoint(object):
|
||||||
'refresh_token': token.refresh_token,
|
'refresh_token': token.refresh_token,
|
||||||
'token_type': 'bearer',
|
'token_type': 'bearer',
|
||||||
'expires_in': settings.get('OIDC_TOKEN_EXPIRE'),
|
'expires_in': settings.get('OIDC_TOKEN_EXPIRE'),
|
||||||
'id_token': encode_id_token(id_token_dic),
|
'id_token': encode_id_token(id_token_dic, self.token.client),
|
||||||
}
|
}
|
||||||
|
|
||||||
return dic
|
return dic
|
||||||
|
|
|
@ -13,12 +13,31 @@ def redirect(uri):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def get_issuer():
|
def get_site_url(site_url=None, request=None):
|
||||||
|
"""
|
||||||
|
Construct the site url.
|
||||||
|
|
||||||
|
Orders to decide site url:
|
||||||
|
1. valid `site_url` parameter
|
||||||
|
2. valid `SITE_URL` in settings
|
||||||
|
3. construct from `request` object
|
||||||
|
"""
|
||||||
|
site_url = site_url or settings.get('SITE_URL')
|
||||||
|
if site_url:
|
||||||
|
return site_url
|
||||||
|
elif request:
|
||||||
|
return '{}://{}'.format(request.scheme, request.get_host())
|
||||||
|
else:
|
||||||
|
raise Exception('Either pass `site_url`, '
|
||||||
|
'or set `SITE_URL` in settings, '
|
||||||
|
'or pass `request` object.')
|
||||||
|
|
||||||
|
def get_issuer(site_url=None, request=None):
|
||||||
"""
|
"""
|
||||||
Construct the issuer full url. Basically is the site url with some path
|
Construct the issuer full url. Basically is the site url with some path
|
||||||
appended.
|
appended.
|
||||||
"""
|
"""
|
||||||
site_url = settings.get('SITE_URL')
|
site_url = get_site_url(site_url=site_url, request=request)
|
||||||
path = reverse('oidc_provider:provider_info') \
|
path = reverse('oidc_provider:provider_info') \
|
||||||
.split('/.well-known/openid-configuration')[0]
|
.split('/.well-known/openid-configuration')[0]
|
||||||
issuer = site_url + path
|
issuer = site_url + path
|
||||||
|
@ -26,14 +45,12 @@ def get_issuer():
|
||||||
return issuer
|
return issuer
|
||||||
|
|
||||||
|
|
||||||
class DefaultUserInfo(object):
|
def default_userinfo(claims, user):
|
||||||
"""
|
"""
|
||||||
Default class for setting OIDC_USERINFO.
|
Default function for setting OIDC_USERINFO.
|
||||||
|
`claims` is a dict that contains all the OIDC standard claims.
|
||||||
"""
|
"""
|
||||||
|
return claims
|
||||||
@classmethod
|
|
||||||
def get_by_user(cls, user):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def default_sub_generator(user):
|
def default_sub_generator(user):
|
||||||
|
|
|
@ -4,8 +4,8 @@ import uuid
|
||||||
|
|
||||||
from Crypto.PublicKey.RSA import importKey
|
from Crypto.PublicKey.RSA import importKey
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from hashlib import md5
|
|
||||||
from jwkest.jwk import RSAKey as jwk_RSAKey
|
from jwkest.jwk import RSAKey as jwk_RSAKey
|
||||||
|
from jwkest.jwk import SYMKey
|
||||||
from jwkest.jws import JWS
|
from jwkest.jws import JWS
|
||||||
|
|
||||||
from oidc_provider.lib.utils.common import get_issuer
|
from oidc_provider.lib.utils.common import get_issuer
|
||||||
|
@ -13,7 +13,7 @@ from oidc_provider.models import *
|
||||||
from oidc_provider import settings
|
from oidc_provider import settings
|
||||||
|
|
||||||
|
|
||||||
def create_id_token(user, aud, nonce):
|
def create_id_token(user, aud, nonce, request=None):
|
||||||
"""
|
"""
|
||||||
Receives a user object and aud (audience).
|
Receives a user object and aud (audience).
|
||||||
Then creates the id_token dictionary.
|
Then creates the id_token dictionary.
|
||||||
|
@ -33,7 +33,7 @@ def create_id_token(user, aud, nonce):
|
||||||
auth_time = int(time.mktime(user_auth_time.timetuple()))
|
auth_time = int(time.mktime(user_auth_time.timetuple()))
|
||||||
|
|
||||||
dic = {
|
dic = {
|
||||||
'iss': get_issuer(),
|
'iss': get_issuer(request=request),
|
||||||
'sub': sub,
|
'sub': sub,
|
||||||
'aud': str(aud),
|
'aud': str(aud),
|
||||||
'exp': exp_time,
|
'exp': exp_time,
|
||||||
|
@ -55,21 +55,26 @@ def create_id_token(user, aud, nonce):
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
|
|
||||||
def encode_id_token(payload):
|
def encode_id_token(payload, client):
|
||||||
"""
|
"""
|
||||||
Represent the ID Token as a JSON Web Token (JWT).
|
Represent the ID Token as a JSON Web Token (JWT).
|
||||||
|
|
||||||
Return a hash.
|
Return a hash.
|
||||||
"""
|
"""
|
||||||
keys = []
|
alg = client.jwt_alg
|
||||||
|
if alg == 'RS256':
|
||||||
|
keys = []
|
||||||
|
for rsakey in RSAKey.objects.all():
|
||||||
|
keys.append(jwk_RSAKey(key=importKey(rsakey.key), kid=rsakey.kid))
|
||||||
|
|
||||||
for rsakey in RSAKey.objects.all():
|
if not keys:
|
||||||
keys.append(jwk_RSAKey(key=importKey(rsakey.key), kid=rsakey.kid))
|
raise Exception('You must add at least one RSA Key.')
|
||||||
|
elif alg == 'HS256':
|
||||||
if not keys:
|
keys = [SYMKey(key=client.client_secret, alg=alg)]
|
||||||
raise Exception('You must add at least one RSA Key.')
|
else:
|
||||||
|
raise Exception('Unsupported key algorithm.')
|
||||||
|
|
||||||
_jws = JWS(payload, alg='RS256')
|
_jws = JWS(payload, alg=alg)
|
||||||
|
|
||||||
return _jws.sign_compact(keys)
|
return _jws.sign_compact(keys)
|
||||||
|
|
||||||
|
@ -95,7 +100,8 @@ def create_token(user, client, id_token_dic, scope):
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
def create_code(user, client, scope, nonce, is_authentication):
|
def create_code(user, client, scope, nonce, is_authentication,
|
||||||
|
code_challenge=None, code_challenge_method=None):
|
||||||
"""
|
"""
|
||||||
Create and populate a Code object.
|
Create and populate a Code object.
|
||||||
|
|
||||||
|
@ -104,7 +110,13 @@ def create_code(user, client, scope, nonce, is_authentication):
|
||||||
code = Code()
|
code = Code()
|
||||||
code.user = user
|
code.user = user
|
||||||
code.client = client
|
code.client = client
|
||||||
|
|
||||||
code.code = uuid.uuid4().hex
|
code.code = uuid.uuid4().hex
|
||||||
|
|
||||||
|
if code_challenge and code_challenge_method:
|
||||||
|
code.code_challenge = code_challenge
|
||||||
|
code.code_challenge_method = code_challenge_method
|
||||||
|
|
||||||
code.expires_at = timezone.now() + timedelta(
|
code.expires_at = timezone.now() + timedelta(
|
||||||
seconds=settings.get('OIDC_CODE_EXPIRE'))
|
seconds=settings.get('OIDC_CODE_EXPIRE'))
|
||||||
code.scope = scope
|
code.scope = scope
|
||||||
|
|
BIN
oidc_provider/locale/es/LC_MESSAGES/django.mo
Normal file
BIN
oidc_provider/locale/es/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
185
oidc_provider/locale/es/LC_MESSAGES/django.po
Normal file
185
oidc_provider/locale/es/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2016-07-26 17:16-0300\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#: lib/claims.py:92
|
||||||
|
msgid "Basic profile"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/claims.py:93
|
||||||
|
msgid ""
|
||||||
|
"Access to your basic information. Includes names, gender, birthdate and "
|
||||||
|
"other information."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/claims.py:116
|
||||||
|
msgid "Email"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/claims.py:117
|
||||||
|
msgid "Access to your email address."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/claims.py:128
|
||||||
|
msgid "Phone number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/claims.py:129
|
||||||
|
msgid "Access to your phone number."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/claims.py:140
|
||||||
|
msgid "Address information"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/claims.py:141
|
||||||
|
msgid ""
|
||||||
|
"Access to your address. Includes country, locality, street and other "
|
||||||
|
"information."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:29
|
||||||
|
msgid "Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:30
|
||||||
|
msgid "Client Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:30
|
||||||
|
msgid ""
|
||||||
|
"<b>Confidential</b> clients are capable of maintaining the confidentiality "
|
||||||
|
"of their credentials. <b>Public</b> clients are incapable."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:31
|
||||||
|
msgid "Client ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:32
|
||||||
|
msgid "Client SECRET"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:33
|
||||||
|
msgid "Response Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:34
|
||||||
|
msgid "JWT Algorithm"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:35
|
||||||
|
msgid "Date Created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:37
|
||||||
|
msgid "Redirect URIs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:37
|
||||||
|
msgid "Enter each URI on a new line."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:40 models.py:65
|
||||||
|
msgid "Client"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:41
|
||||||
|
msgid "Clients"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:64
|
||||||
|
msgid "User"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:66
|
||||||
|
msgid "Expiration Date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:67
|
||||||
|
msgid "Scopes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:92
|
||||||
|
msgid "Code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:93
|
||||||
|
msgid "Nonce"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:94
|
||||||
|
msgid "Is Authentication?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:95
|
||||||
|
msgid "Code Challenge"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:96
|
||||||
|
msgid "Code Challenge Method"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:99
|
||||||
|
msgid "Authorization Code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:100
|
||||||
|
msgid "Authorization Codes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:105
|
||||||
|
msgid "Access Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:106
|
||||||
|
msgid "Refresh Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:107
|
||||||
|
msgid "ID Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:117
|
||||||
|
msgid "Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:118
|
||||||
|
msgid "Tokens"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:123
|
||||||
|
msgid "Date Given"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:131
|
||||||
|
msgid "Key"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:131
|
||||||
|
msgid "Paste your private RSA Key here."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:134
|
||||||
|
msgid "RSA Key"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:135
|
||||||
|
msgid "RSA Keys"
|
||||||
|
msgstr ""
|
BIN
oidc_provider/locale/fr/LC_MESSAGES/django.mo
Normal file
BIN
oidc_provider/locale/fr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
189
oidc_provider/locale/fr/LC_MESSAGES/django.po
Normal file
189
oidc_provider/locale/fr/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2016-07-26 17:15-0300\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
|
||||||
|
#: lib/claims.py:92
|
||||||
|
msgid "Basic profile"
|
||||||
|
msgstr "Profil de base"
|
||||||
|
|
||||||
|
#: lib/claims.py:93
|
||||||
|
msgid ""
|
||||||
|
"Access to your basic information. Includes names, gender, birthdate and "
|
||||||
|
"other information."
|
||||||
|
msgstr "Accès à vos informations de base. Comprend vos noms, genre, "
|
||||||
|
"date de naissance et d'autres informations"
|
||||||
|
|
||||||
|
#: lib/claims.py:116
|
||||||
|
msgid "Email"
|
||||||
|
msgstr "Courriel"
|
||||||
|
|
||||||
|
#: lib/claims.py:117
|
||||||
|
msgid "Access to your email address."
|
||||||
|
msgstr "Accès à votre adresse email"
|
||||||
|
|
||||||
|
#: lib/claims.py:128
|
||||||
|
msgid "Phone number"
|
||||||
|
msgstr "Numéro de téléphone"
|
||||||
|
|
||||||
|
#: lib/claims.py:129
|
||||||
|
msgid "Access to your phone number."
|
||||||
|
msgstr "Accès à votre numéro de téléphone"
|
||||||
|
|
||||||
|
#: lib/claims.py:140
|
||||||
|
msgid "Address information"
|
||||||
|
msgstr "Informations liées à votre adresse"
|
||||||
|
|
||||||
|
#: lib/claims.py:141
|
||||||
|
msgid ""
|
||||||
|
"Access to your address. Includes country, locality, street and other "
|
||||||
|
"information."
|
||||||
|
msgstr "Accès à votre adresse. Comprend le pays, la ville, la rue "
|
||||||
|
"et d'autres informations"
|
||||||
|
|
||||||
|
#: models.py:29
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Nom"
|
||||||
|
|
||||||
|
#: models.py:30
|
||||||
|
msgid "Client Type"
|
||||||
|
msgstr "Type de client"
|
||||||
|
|
||||||
|
#: models.py:30
|
||||||
|
msgid ""
|
||||||
|
"<b>Confidential</b> clients are capable of maintaining the confidentiality "
|
||||||
|
"of their credentials. <b>Public</b> clients are incapable."
|
||||||
|
msgstr ""
|
||||||
|
"<b>Confidentiel</b> les clients sont capable de maintenir la confidentialité "
|
||||||
|
" des paramètres de connexion. <b>Public</b> les clients n'en sont pas capable"
|
||||||
|
|
||||||
|
#: models.py:31
|
||||||
|
msgid "Client ID"
|
||||||
|
msgstr "Identifiant client"
|
||||||
|
|
||||||
|
#: models.py:32
|
||||||
|
msgid "Client SECRET"
|
||||||
|
msgstr "Code secret client"
|
||||||
|
|
||||||
|
#: models.py:33
|
||||||
|
msgid "Response Type"
|
||||||
|
msgstr "Type de réponse"
|
||||||
|
|
||||||
|
#: models.py:34
|
||||||
|
msgid "JWT Algorithm"
|
||||||
|
msgstr "Algorythme JWT"
|
||||||
|
|
||||||
|
#: models.py:35
|
||||||
|
msgid "Date Created"
|
||||||
|
msgstr "Date de création"
|
||||||
|
|
||||||
|
#: models.py:37
|
||||||
|
msgid "Redirect URIs"
|
||||||
|
msgstr "URIs utilisée pour la redirection"
|
||||||
|
|
||||||
|
#: models.py:37
|
||||||
|
msgid "Enter each URI on a new line."
|
||||||
|
msgstr "Entrez chaque URI à la ligne"
|
||||||
|
|
||||||
|
#: models.py:40 models.py:65
|
||||||
|
msgid "Client"
|
||||||
|
msgstr "Client"
|
||||||
|
|
||||||
|
#: models.py:41
|
||||||
|
msgid "Clients"
|
||||||
|
msgstr "Clients"
|
||||||
|
|
||||||
|
#: models.py:64
|
||||||
|
msgid "User"
|
||||||
|
msgstr "Utilisateur"
|
||||||
|
|
||||||
|
#: models.py:66
|
||||||
|
msgid "Expiration Date"
|
||||||
|
msgstr "Date d'expiration"
|
||||||
|
|
||||||
|
#: models.py:67
|
||||||
|
msgid "Scopes"
|
||||||
|
msgstr "Portées"
|
||||||
|
|
||||||
|
#: models.py:92
|
||||||
|
msgid "Code"
|
||||||
|
msgstr "Code"
|
||||||
|
|
||||||
|
#: models.py:93
|
||||||
|
msgid "Nonce"
|
||||||
|
msgstr "Valeur de circonstance"
|
||||||
|
|
||||||
|
#: models.py:94
|
||||||
|
msgid "Is Authentication?"
|
||||||
|
msgstr "Est authentifié ?"
|
||||||
|
|
||||||
|
#: models.py:95
|
||||||
|
msgid "Code Challenge"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:96
|
||||||
|
msgid "Code Challenge Method"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:99
|
||||||
|
msgid "Authorization Code"
|
||||||
|
msgstr "Code d'authorisation"
|
||||||
|
|
||||||
|
#: models.py:100
|
||||||
|
msgid "Authorization Codes"
|
||||||
|
msgstr "Codes d'authorisation"
|
||||||
|
|
||||||
|
#: models.py:105
|
||||||
|
msgid "Access Token"
|
||||||
|
msgstr "Jeton d'accès"
|
||||||
|
|
||||||
|
#: models.py:106
|
||||||
|
msgid "Refresh Token"
|
||||||
|
msgstr "Jeton de rafraichissement"
|
||||||
|
|
||||||
|
#: models.py:107
|
||||||
|
msgid "ID Token"
|
||||||
|
msgstr "Identifiant du jeton"
|
||||||
|
|
||||||
|
#: models.py:117
|
||||||
|
msgid "Token"
|
||||||
|
msgstr "Jeton"
|
||||||
|
|
||||||
|
#: models.py:118
|
||||||
|
msgid "Tokens"
|
||||||
|
msgstr "Jetons"
|
||||||
|
|
||||||
|
#: models.py:123
|
||||||
|
msgid "Date Given"
|
||||||
|
msgstr "Date donnée"
|
||||||
|
|
||||||
|
#: models.py:131
|
||||||
|
msgid "Key"
|
||||||
|
msgstr "Clé"
|
||||||
|
|
||||||
|
#: models.py:131
|
||||||
|
msgid "Paste your private RSA Key here."
|
||||||
|
msgstr "Collez votre clé privée RSA ici."
|
||||||
|
|
||||||
|
#: models.py:134
|
||||||
|
msgid "RSA Key"
|
||||||
|
msgstr "Clé RSA"
|
||||||
|
|
||||||
|
#: models.py:135
|
||||||
|
msgid "RSA Keys"
|
||||||
|
msgstr "Clés RSA"
|
25
oidc_provider/migrations/0013_auto_20160407_1912.py
Normal file
25
oidc_provider/migrations/0013_auto_20160407_1912.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2016-04-07 19:12
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oidc_provider', '0012_auto_20160405_2041'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='code',
|
||||||
|
name='code_challenge',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='code',
|
||||||
|
name='code_challenge_method',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
20
oidc_provider/migrations/0014_client_jwt_alg.py
Normal file
20
oidc_provider/migrations/0014_client_jwt_alg.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2016-04-25 18:02
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oidc_provider', '0013_auto_20160407_1912'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='client',
|
||||||
|
name='jwt_alg',
|
||||||
|
field=models.CharField(choices=[(b'HS256', b'HS256'), (b'RS256', b'RS256')], default=b'RS256', max_length=10, verbose_name='JWT Algorithm'),
|
||||||
|
),
|
||||||
|
]
|
65
oidc_provider/migrations/0015_change_client_code.py
Normal file
65
oidc_provider/migrations/0015_change_client_code.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-06-10 13:55
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oidc_provider', '0014_client_jwt_alg'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='_redirect_uris',
|
||||||
|
field=models.TextField(default='', help_text='Enter each URI on a new line.', verbose_name='Redirect URI'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='client_secret',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='client_type',
|
||||||
|
field=models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], default='confidential', help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.', max_length=30),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='jwt_alg',
|
||||||
|
field=models.CharField(choices=[('HS256', 'HS256'), ('RS256', 'RS256')], default='RS256', max_length=10, verbose_name='JWT Algorithm'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(default='', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='response_type',
|
||||||
|
field=models.CharField(choices=[('code', 'code (Authorization Code Flow)'), ('id_token', 'id_token (Implicit Flow)'), ('id_token token', 'id_token token (Implicit Flow)')], max_length=30),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='_scope',
|
||||||
|
field=models.TextField(default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='nonce',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='token',
|
||||||
|
name='_scope',
|
||||||
|
field=models.TextField(default=''),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userconsent',
|
||||||
|
name='_scope',
|
||||||
|
field=models.TextField(default=''),
|
||||||
|
),
|
||||||
|
]
|
165
oidc_provider/migrations/0016_userconsent_and_verbosenames.py
Normal file
165
oidc_provider/migrations/0016_userconsent_and_verbosenames.py
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.6 on 2016-06-10 17:53
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.utils.timezone import utc
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oidc_provider', '0015_change_client_code'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userconsent',
|
||||||
|
name='date_given',
|
||||||
|
field=models.DateTimeField(default=datetime.datetime(2016, 6, 10, 17, 53, 48, 889808, tzinfo=utc), verbose_name='Date Given'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='_redirect_uris',
|
||||||
|
field=models.TextField(default=b'', help_text='Enter each URI on a new line.', verbose_name='Redirect URIs'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='client_id',
|
||||||
|
field=models.CharField(max_length=255, unique=True, verbose_name='Client ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='client_secret',
|
||||||
|
field=models.CharField(blank=True, default=b'', max_length=255, verbose_name='Client SECRET'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='client_type',
|
||||||
|
field=models.CharField(choices=[(b'confidential', b'Confidential'), (b'public', b'Public')], default=b'confidential', help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.', max_length=30, verbose_name='Client Type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='date_created',
|
||||||
|
field=models.DateField(auto_now_add=True, verbose_name='Date Created'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(default=b'', max_length=100, verbose_name='Name'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='response_type',
|
||||||
|
field=models.CharField(choices=[(b'code', b'code (Authorization Code Flow)'), (b'id_token', b'id_token (Implicit Flow)'), (b'id_token token', b'id_token token (Implicit Flow)')], max_length=30, verbose_name='Response Type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='_scope',
|
||||||
|
field=models.TextField(default=b'', verbose_name='Scopes'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='client',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='code',
|
||||||
|
field=models.CharField(max_length=255, unique=True, verbose_name='Code'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='code_challenge',
|
||||||
|
field=models.CharField(max_length=255, null=True, verbose_name='Code Challenge'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='code_challenge_method',
|
||||||
|
field=models.CharField(max_length=255, null=True, verbose_name='Code Challenge Method'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='expires_at',
|
||||||
|
field=models.DateTimeField(verbose_name='Expiration Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='is_authentication',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Is Authentication?'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='nonce',
|
||||||
|
field=models.CharField(blank=True, default=b'', max_length=255, verbose_name='Nonce'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='code',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rsakey',
|
||||||
|
name='key',
|
||||||
|
field=models.TextField(help_text='Paste your private RSA Key here.', verbose_name='Key'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='token',
|
||||||
|
name='_id_token',
|
||||||
|
field=models.TextField(verbose_name='ID Token'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='token',
|
||||||
|
name='_scope',
|
||||||
|
field=models.TextField(default=b'', verbose_name='Scopes'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='token',
|
||||||
|
name='access_token',
|
||||||
|
field=models.CharField(max_length=255, unique=True, verbose_name='Access Token'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='token',
|
||||||
|
name='client',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='token',
|
||||||
|
name='expires_at',
|
||||||
|
field=models.DateTimeField(verbose_name='Expiration Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='token',
|
||||||
|
name='refresh_token',
|
||||||
|
field=models.CharField(max_length=255, null=True, unique=True, verbose_name='Refresh Token'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='token',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userconsent',
|
||||||
|
name='_scope',
|
||||||
|
field=models.TextField(default=b'', verbose_name='Scopes'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userconsent',
|
||||||
|
name='client',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.Client', verbose_name='Client'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userconsent',
|
||||||
|
name='expires_at',
|
||||||
|
field=models.DateTimeField(verbose_name='Expiration Date'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userconsent',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,27 +8,33 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
CLIENT_TYPE_CHOICES = [
|
||||||
|
('confidential', 'Confidential'),
|
||||||
|
('public', 'Public'),
|
||||||
|
]
|
||||||
|
|
||||||
|
RESPONSE_TYPE_CHOICES = [
|
||||||
|
('code', 'code (Authorization Code Flow)'),
|
||||||
|
('id_token', 'id_token (Implicit Flow)'),
|
||||||
|
('id_token token', 'id_token token (Implicit Flow)'),
|
||||||
|
]
|
||||||
|
|
||||||
|
JWT_ALGS = [
|
||||||
|
('HS256', 'HS256'),
|
||||||
|
('RS256', 'RS256'),
|
||||||
|
]
|
||||||
|
|
||||||
class Client(models.Model):
|
class Client(models.Model):
|
||||||
|
|
||||||
CLIENT_TYPE_CHOICES = [
|
name = models.CharField(max_length=100, default='', verbose_name=_(u'Name'))
|
||||||
('confidential', 'Confidential'),
|
client_type = models.CharField(max_length=30, choices=CLIENT_TYPE_CHOICES, default='confidential', verbose_name=_(u'Client Type'), help_text=_(u'<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.'))
|
||||||
('public', 'Public'),
|
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'))
|
||||||
|
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'))
|
||||||
|
date_created = models.DateField(auto_now_add=True, verbose_name=_(u'Date Created'))
|
||||||
|
|
||||||
RESPONSE_TYPE_CHOICES = [
|
_redirect_uris = models.TextField(default='', verbose_name=_(u'Redirect URIs'), help_text=_(u'Enter each URI on a new line.'))
|
||||||
('code', 'code (Authorization Code Flow)'),
|
|
||||||
('id_token', 'id_token (Implicit Flow)'),
|
|
||||||
('id_token token', 'id_token token (Implicit Flow)'),
|
|
||||||
]
|
|
||||||
|
|
||||||
name = models.CharField(max_length=100, default='')
|
|
||||||
client_type = models.CharField(max_length=30, choices=CLIENT_TYPE_CHOICES, default='confidential', help_text=_(u'<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.'))
|
|
||||||
client_id = models.CharField(max_length=255, unique=True)
|
|
||||||
client_secret = models.CharField(max_length=255, blank=True, default='')
|
|
||||||
response_type = models.CharField(max_length=30, choices=RESPONSE_TYPE_CHOICES)
|
|
||||||
date_created = models.DateField(auto_now_add=True)
|
|
||||||
|
|
||||||
_redirect_uris = models.TextField(default='', verbose_name=_(u'Redirect URI'), help_text=_(u'Enter each URI on a new line.'))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _(u'Client')
|
verbose_name = _(u'Client')
|
||||||
|
@ -39,7 +45,7 @@ class Client(models.Model):
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
def redirect_uris():
|
def redirect_uris():
|
||||||
def fget(self):
|
def fget(self):
|
||||||
return self._redirect_uris.splitlines()
|
return self._redirect_uris.splitlines()
|
||||||
|
@ -55,10 +61,10 @@ class Client(models.Model):
|
||||||
|
|
||||||
class BaseCodeTokenModel(models.Model):
|
class BaseCodeTokenModel(models.Model):
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_(u'User'))
|
||||||
client = models.ForeignKey(Client)
|
client = models.ForeignKey(Client, verbose_name=_(u'Client'))
|
||||||
expires_at = models.DateTimeField()
|
expires_at = models.DateTimeField(verbose_name=_(u'Expiration Date'))
|
||||||
_scope = models.TextField(default='')
|
_scope = models.TextField(default='', verbose_name=_(u'Scopes'))
|
||||||
|
|
||||||
def scope():
|
def scope():
|
||||||
def fget(self):
|
def fget(self):
|
||||||
|
@ -72,20 +78,22 @@ class BaseCodeTokenModel(models.Model):
|
||||||
return timezone.now() >= self.expires_at
|
return timezone.now() >= self.expires_at
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u'{0} - {1} ({2})'.format(self.client, self.user.email, self.expires_at)
|
return u'{0} - {1}'.format(self.client, self.user.email)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Code(BaseCodeTokenModel):
|
class Code(BaseCodeTokenModel):
|
||||||
|
|
||||||
code = models.CharField(max_length=255, unique=True)
|
code = models.CharField(max_length=255, unique=True, verbose_name=_(u'Code'))
|
||||||
nonce = models.CharField(max_length=255, blank=True, default='')
|
nonce = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Nonce'))
|
||||||
is_authentication = models.BooleanField(default=False)
|
is_authentication = models.BooleanField(default=False, verbose_name=_(u'Is Authentication?'))
|
||||||
|
code_challenge = models.CharField(max_length=255, null=True, verbose_name=_(u'Code Challenge'))
|
||||||
|
code_challenge_method = models.CharField(max_length=255, null=True, verbose_name=_(u'Code Challenge Method'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _(u'Authorization Code')
|
verbose_name = _(u'Authorization Code')
|
||||||
|
@ -94,9 +102,9 @@ class Code(BaseCodeTokenModel):
|
||||||
|
|
||||||
class Token(BaseCodeTokenModel):
|
class Token(BaseCodeTokenModel):
|
||||||
|
|
||||||
access_token = models.CharField(max_length=255, unique=True)
|
access_token = models.CharField(max_length=255, unique=True, verbose_name=_(u'Access Token'))
|
||||||
refresh_token = models.CharField(max_length=255, unique=True, null=True)
|
refresh_token = models.CharField(max_length=255, unique=True, null=True, verbose_name=_(u'Refresh Token'))
|
||||||
_id_token = models.TextField()
|
_id_token = models.TextField(verbose_name=_(u'ID Token'))
|
||||||
def id_token():
|
def id_token():
|
||||||
def fget(self):
|
def fget(self):
|
||||||
return json.loads(self._id_token)
|
return json.loads(self._id_token)
|
||||||
|
@ -112,13 +120,15 @@ class Token(BaseCodeTokenModel):
|
||||||
|
|
||||||
class UserConsent(BaseCodeTokenModel):
|
class UserConsent(BaseCodeTokenModel):
|
||||||
|
|
||||||
|
date_given = models.DateTimeField(verbose_name=_(u'Date Given'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('user', 'client')
|
unique_together = ('user', 'client')
|
||||||
|
|
||||||
|
|
||||||
class RSAKey(models.Model):
|
class RSAKey(models.Model):
|
||||||
|
|
||||||
key = models.TextField(help_text=_(u'Paste your private RSA Key here.'))
|
key = models.TextField(verbose_name=_(u'Key'), help_text=_(u'Paste your private RSA Key here.'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _(u'RSA Key')
|
verbose_name = _(u'RSA Key')
|
||||||
|
|
|
@ -4,6 +4,9 @@ from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
class DefaultSettings(object):
|
class DefaultSettings(object):
|
||||||
|
required_attrs = (
|
||||||
|
'LOGIN_URL',
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def LOGIN_URL(self):
|
def LOGIN_URL(self):
|
||||||
|
@ -15,7 +18,7 @@ class DefaultSettings(object):
|
||||||
@property
|
@property
|
||||||
def SITE_URL(self):
|
def SITE_URL(self):
|
||||||
"""
|
"""
|
||||||
REQUIRED. The OP server url.
|
OPTIONAL. The OP server url.
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -38,9 +41,9 @@ class DefaultSettings(object):
|
||||||
def OIDC_EXTRA_SCOPE_CLAIMS(self):
|
def OIDC_EXTRA_SCOPE_CLAIMS(self):
|
||||||
"""
|
"""
|
||||||
OPTIONAL. A string with the location of your class.
|
OPTIONAL. A string with the location of your class.
|
||||||
Used to add extra scopes specific for your app.
|
Used to add extra scopes specific for your app.
|
||||||
"""
|
"""
|
||||||
return 'oidc_provider.lib.claims.AbstractScopeClaims'
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def OIDC_IDTOKEN_EXPIRE(self):
|
def OIDC_IDTOKEN_EXPIRE(self):
|
||||||
|
@ -92,10 +95,10 @@ class DefaultSettings(object):
|
||||||
@property
|
@property
|
||||||
def OIDC_USERINFO(self):
|
def OIDC_USERINFO(self):
|
||||||
"""
|
"""
|
||||||
OPTIONAL. A string with the location of your class.
|
OPTIONAL. A string with the location of your function.
|
||||||
Used to add extra scopes specific for your app.
|
Used to populate standard claims with your user information.
|
||||||
"""
|
"""
|
||||||
return 'oidc_provider.lib.utils.common.DefaultUserInfo'
|
return 'oidc_provider.lib.utils.common.default_userinfo'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def OIDC_IDTOKEN_PROCESSING_HOOK(self):
|
def OIDC_IDTOKEN_PROCESSING_HOOK(self):
|
||||||
|
@ -131,7 +134,7 @@ def get(name, import_str=False):
|
||||||
value = getattr(default_settings, name)
|
value = getattr(default_settings, name)
|
||||||
value = getattr(settings, name)
|
value = getattr(settings, name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
if value is None:
|
if value is None and name in default_settings.required_attrs:
|
||||||
raise Exception('You must set ' + name + ' in your settings.')
|
raise Exception('You must set ' + name + ' in your settings.')
|
||||||
|
|
||||||
value = import_from_str(value) if import_str else value
|
value = import_from_str(value) if import_str else value
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
<p>Client <strong>{{ client.name }}</strong> would like to access this information of you ...</p>
|
<p>Client <strong>{{ client.name }}</strong> would like to access this information of you ...</p>
|
||||||
|
|
||||||
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{{ hidden_inputs }}
|
{{ hidden_inputs }}
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for scope in params.scope %}
|
{% for scope in scopes %}
|
||||||
<li>{{ scope | capfirst }}</li>
|
<li><strong>{{ scope.name }}</strong> <br><i>{{ scope.description }}</i></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<input type="submit" value="Decline" />
|
<input type="submit" value="Decline" />
|
||||||
|
|
|
@ -3,4 +3,6 @@
|
||||||
<input name="response_type" type="hidden" value="{{ params.response_type }}" />
|
<input name="response_type" type="hidden" value="{{ params.response_type }}" />
|
||||||
<input name="scope" type="hidden" value="{{ params.scope | join:' ' }}" />
|
<input name="scope" type="hidden" value="{{ params.scope | join:' ' }}" />
|
||||||
<input name="state" type="hidden" value="{{ params.state }}" />
|
<input name="state" type="hidden" value="{{ params.state }}" />
|
||||||
<input name="nonce" type="hidden" value="{{ params.nonce }}" />
|
{% if params.nonce %}<input name="nonce" type="hidden" value="{{ params.nonce }}" />{% endif %}
|
||||||
|
{% if params.code_challenge %}<input name="code_challenge" type="hidden" value="{{ params.code_challenge }}" />{% endif %}
|
||||||
|
{% if params.code_challenge_method %}<input name="code_challenge_method" type="hidden" value="{{ params.code_challenge_method }}" />{% endif %}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIICXgIBAAKBgQC/O5N0BxpMVbht7i0bFIQyD0q2O4mutyYLoAQn8skYEbDUmcwp
|
|
||||||
9dRe7GTHiDrMqJ3gW9hTZcYm7dt5rhjFqdCYK504PDOcK8LGkCN2CiWeRbCAwaz0
|
|
||||||
Wgh3oJfbTMuYV+LWLFAAPxN4cyN6RoE9mlk7vq7YNYVpdg0VNMAKvW95dQIDAQAB
|
|
||||||
AoGBAIBMdxw0G7e1Fxxh3E87z4lKaySiAzh91f+cps0qfTIxxEKOwMQyEv5weRjJ
|
|
||||||
VDG0ut8on5UsReoeUM5tOF99E92pEnenI7+VfnFf04xCLcdT0XGbKimb+5g6y1Pm
|
|
||||||
8630TD97tVO0ASHcrXOtkSTYNdAUDcqeJUTOwgW0OD3Hyb8BAkEAxODr/Mln86wu
|
|
||||||
NhnxEVf9wuEJxX6JUjnkh62wIWYbZU61D+pIrtofi/0+AYn/9IeBCTDNIM4qTzsC
|
|
||||||
HV/u/3nmwQJBAPiooD4FYBI1VOwZ7RZqR0ZyQN0IkBsfw95K789I1lBeXh34b6r6
|
|
||||||
dik4A72guaAZEuxTz3MPjbSrflGjq47fE7UCQQCPsDSrpvcGYbjMZXyKkvSywXlX
|
|
||||||
OXXRnE0NNReiGJqQArSk6/GmI634hpg1mVlER41GfuaHNdCtSLzPYY/Vx0tBAkAc
|
|
||||||
QFxkb4voxbJuWMu9HjoW4OhJtK1ax5MjcHQqouXmn7IlyZI2ZNqD+F9Ebjxo2jBy
|
|
||||||
NVt+gSfifRGPCP927hV5AkEAwFu9HZipddp8PM8tyF1G09+s3DVSCR3DLMBwX9NX
|
|
||||||
nGA9tOLYOSgG/HKLOWD1qT0G8r/vYtFuktCKMSidVMp5sw==
|
|
||||||
-----END RSA PRIVATE KEY-----
|
|
|
@ -53,7 +53,9 @@ TEMPLATE_DIRS = [
|
||||||
'oidc_provider/tests/templates',
|
'oidc_provider/tests/templates',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
# OIDC Provider settings.
|
# OIDC Provider settings.
|
||||||
|
|
||||||
SITE_URL = 'http://localhost:8000'
|
SITE_URL = 'http://localhost:8000'
|
||||||
OIDC_USERINFO = 'oidc_provider.tests.app.utils.FakeUserInfo'
|
OIDC_USERINFO = 'oidc_provider.tests.app.utils.userinfo'
|
||||||
|
|
|
@ -13,6 +13,8 @@ from oidc_provider.models import *
|
||||||
|
|
||||||
FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d'
|
FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d'
|
||||||
FAKE_RANDOM_STRING = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
FAKE_RANDOM_STRING = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||||||
|
FAKE_CODE_CHALLENGE = 'YlYXEqXuRm-Xgi2BOUiK50JW1KsGTX6F1TDnZSC8VTg'
|
||||||
|
FAKE_CODE_VERIFIER = 'SmxGa0XueyNh5bDgTcSrqzAh2_FmXEqU8kDT6CuXicw'
|
||||||
|
|
||||||
|
|
||||||
def create_fake_user():
|
def create_fake_user():
|
||||||
|
@ -31,7 +33,7 @@ def create_fake_user():
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def create_fake_client(response_type):
|
def create_fake_client(response_type, is_public=False):
|
||||||
"""
|
"""
|
||||||
Create a test client, response_type argument MUST be:
|
Create a test client, response_type argument MUST be:
|
||||||
'code', 'id_token' or 'id_token token'.
|
'code', 'id_token' or 'id_token token'.
|
||||||
|
@ -40,8 +42,12 @@ def create_fake_client(response_type):
|
||||||
"""
|
"""
|
||||||
client = Client()
|
client = Client()
|
||||||
client.name = 'Some Client'
|
client.name = 'Some Client'
|
||||||
client.client_id = '123'
|
client.client_id = str(random.randint(1, 999999)).zfill(6)
|
||||||
client.client_secret = '456'
|
if is_public:
|
||||||
|
client.client_type = 'public'
|
||||||
|
client.client_secret = ''
|
||||||
|
else:
|
||||||
|
client.client_secret = str(random.randint(1, 999999)).zfill(6)
|
||||||
client.response_type = response_type
|
client.response_type = response_type
|
||||||
client.redirect_uris = ['http://example.com/']
|
client.redirect_uris = ['http://example.com/']
|
||||||
|
|
||||||
|
@ -50,17 +56,6 @@ def create_fake_client(response_type):
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
def create_rsakey():
|
|
||||||
"""
|
|
||||||
Generate and save a sample RSA Key.
|
|
||||||
"""
|
|
||||||
fullpath = os.path.abspath(os.path.dirname(__file__)) + '/RSAKEY.pem'
|
|
||||||
|
|
||||||
with open(fullpath, 'r') as f:
|
|
||||||
key = f.read()
|
|
||||||
RSAKey(key=key).save()
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
@ -78,27 +73,16 @@ def is_code_valid(url, user, client):
|
||||||
return is_code_ok
|
return is_code_ok
|
||||||
|
|
||||||
|
|
||||||
class FakeUserInfo(object):
|
def userinfo(claims, user):
|
||||||
"""
|
"""
|
||||||
Fake class for setting OIDC_USERINFO.
|
Fake function for setting OIDC_USERINFO.
|
||||||
"""
|
"""
|
||||||
|
claims['given_name'] = 'John'
|
||||||
given_name = 'John'
|
claims['family_name'] = 'Doe'
|
||||||
family_name = 'Doe'
|
claims['name'] = '{0} {1}'.format(claims['given_name'], claims['family_name'])
|
||||||
nickname = 'johndoe'
|
claims['email'] = user.email
|
||||||
website = 'http://johndoe.com'
|
claims['address']['country'] = 'Argentina'
|
||||||
|
return claims
|
||||||
phone_number = '+49-89-636-48018'
|
|
||||||
phone_number_verified = True
|
|
||||||
|
|
||||||
address_street_address = 'Evergreen 742'
|
|
||||||
address_locality = 'Glendive'
|
|
||||||
address_region = 'Montana'
|
|
||||||
address_country = 'United States'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_user(cls, user):
|
|
||||||
return cls()
|
|
||||||
|
|
||||||
|
|
||||||
def fake_sub_generator(user):
|
def fake_sub_generator(user):
|
||||||
|
|
|
@ -6,6 +6,7 @@ import uuid
|
||||||
|
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core.management import call_command
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -22,10 +23,35 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
call_command('creatersakey')
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.user = create_fake_user()
|
self.user = create_fake_user()
|
||||||
self.client = create_fake_client(response_type='code')
|
self.client = create_fake_client(response_type='code')
|
||||||
|
self.client_public = create_fake_client(response_type='code', is_public=True)
|
||||||
|
self.client_implicit = create_fake_client(response_type='id_token token')
|
||||||
self.state = uuid.uuid4().hex
|
self.state = uuid.uuid4().hex
|
||||||
|
self.nonce = uuid.uuid4().hex
|
||||||
|
|
||||||
|
def _auth_request(self, method, data={}, is_user_authenticated=False):
|
||||||
|
url = reverse('oidc_provider:authorize')
|
||||||
|
|
||||||
|
if method.lower() == 'get':
|
||||||
|
query_str = urlencode(data).replace('+', '%20')
|
||||||
|
if query_str:
|
||||||
|
url += '?' + query_str
|
||||||
|
request = self.factory.get(url)
|
||||||
|
elif method.lower() == 'post':
|
||||||
|
request = self.factory.post(url, data=data)
|
||||||
|
else:
|
||||||
|
raise Exception('Method unsupported for an Authorization Request.')
|
||||||
|
|
||||||
|
# Simulate that the user is logged.
|
||||||
|
request.user = self.user if is_user_authenticated else AnonymousUser()
|
||||||
|
|
||||||
|
response = AuthorizeView.as_view()(request)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def test_missing_parameters(self):
|
def test_missing_parameters(self):
|
||||||
"""
|
"""
|
||||||
|
@ -35,11 +61,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
|
|
||||||
See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
||||||
"""
|
"""
|
||||||
url = reverse('oidc_provider:authorize')
|
response = self._auth_request('get')
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(bool(response.content), True)
|
self.assertEqual(bool(response.content), True)
|
||||||
|
@ -52,19 +74,15 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||||
"""
|
"""
|
||||||
# Create an authorize request with an unsupported response_type.
|
# Create an authorize request with an unsupported response_type.
|
||||||
query_str = urlencode({
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'response_type': 'something_wrong',
|
'response_type': 'something_wrong',
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
}).replace('+', '%20')
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
response = self._auth_request('get', data)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.has_header('Location'), True)
|
self.assertEqual(response.has_header('Location'), True)
|
||||||
|
@ -80,34 +98,20 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
|
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#Authenticates
|
See: http://openid.net/specs/openid-connect-core-1_0.html#Authenticates
|
||||||
"""
|
"""
|
||||||
query_str = urlencode({
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
}).replace('+', '%20')
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
response = self._auth_request('get', data)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
# 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']
|
login_url_exists = settings.get('LOGIN_URL') in response['Location']
|
||||||
self.assertEqual(login_url_exists, True)
|
self.assertEqual(login_url_exists, True)
|
||||||
|
|
||||||
# Check if the login will redirect to a valid url.
|
|
||||||
try:
|
|
||||||
next_value = response['Location'].split(REDIRECT_FIELD_NAME + '=')[1]
|
|
||||||
next_url = unquote(next_value)
|
|
||||||
is_next_ok = next_url == url
|
|
||||||
except:
|
|
||||||
is_next_ok = False
|
|
||||||
self.assertEqual(is_next_ok, True)
|
|
||||||
|
|
||||||
def test_user_consent_inputs(self):
|
def test_user_consent_inputs(self):
|
||||||
"""
|
"""
|
||||||
Once the End-User is authenticated, the Authorization Server MUST
|
Once the End-User is authenticated, the Authorization Server MUST
|
||||||
|
@ -116,21 +120,18 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
|
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#Consent
|
See: http://openid.net/specs/openid-connect-core-1_0.html#Consent
|
||||||
"""
|
"""
|
||||||
query_str = urlencode({
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
}).replace('+', '%20')
|
# PKCE parameters.
|
||||||
|
'code_challenge': FAKE_CODE_CHALLENGE,
|
||||||
|
'code_challenge_method': 'S256',
|
||||||
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
# Check if hidden inputs exists in the form,
|
# Check if hidden inputs exists in the form,
|
||||||
# also if their values are valid.
|
# also if their values are valid.
|
||||||
|
@ -140,6 +141,8 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
|
'code_challenge': FAKE_CODE_CHALLENGE,
|
||||||
|
'code_challenge_method': 'S256',
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value in iter(to_check.items()):
|
for key, value in iter(to_check.items()):
|
||||||
|
@ -159,23 +162,18 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
the parameters defined in Section 4.1.2 of OAuth 2.0 [RFC6749]
|
the parameters defined in Section 4.1.2 of OAuth 2.0 [RFC6749]
|
||||||
by adding them as query parameters to the redirect_uri.
|
by adding them as query parameters to the redirect_uri.
|
||||||
"""
|
"""
|
||||||
response_type = 'code'
|
data = {
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize')
|
|
||||||
|
|
||||||
post_data = {
|
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'response_type': response_type,
|
'response_type': 'code',
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
|
# PKCE parameters.
|
||||||
|
'code_challenge': FAKE_CODE_CHALLENGE,
|
||||||
|
'code_challenge_method': 'S256',
|
||||||
}
|
}
|
||||||
|
|
||||||
request = self.factory.post(url, data=post_data)
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
# 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.
|
||||||
|
@ -185,13 +183,9 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
msg='"access_denied" code is missing in query.')
|
msg='"access_denied" code is missing in query.')
|
||||||
|
|
||||||
# Simulate user authorization.
|
# Simulate user authorization.
|
||||||
post_data['allow'] = 'Accept' # Should be the value of the button.
|
data['allow'] = 'Accept' # Will be the value of the button.
|
||||||
|
|
||||||
request = self.factory.post(url, data=post_data)
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
is_code_ok = is_code_valid(url=response['Location'],
|
is_code_ok = is_code_valid(url=response['Location'],
|
||||||
user=self.user,
|
user=self.user,
|
||||||
|
@ -210,7 +204,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
list of scopes) and because they might be prompted for the same
|
list of scopes) and because they might be prompted for the same
|
||||||
authorization multiple times, the server skip it.
|
authorization multiple times, the server skip it.
|
||||||
"""
|
"""
|
||||||
post_data = {
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
|
@ -220,34 +214,25 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
request = self.factory.post(reverse('oidc_provider:authorize'),
|
request = self.factory.post(reverse('oidc_provider:authorize'),
|
||||||
data=post_data)
|
data=data)
|
||||||
# Simulate that the user is logged.
|
# Simulate that the user is logged.
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
|
|
||||||
with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True):
|
with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True):
|
||||||
response = AuthorizeView.as_view()(request)
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
|
|
||||||
self.assertEqual('code' in response['Location'], True,
|
self.assertEqual('code' in response['Location'], True,
|
||||||
msg='Code is missing in the returned url.')
|
msg='Code is missing in the returned url.')
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
|
|
||||||
is_code_ok = is_code_valid(url=response['Location'],
|
is_code_ok = is_code_valid(url=response['Location'],
|
||||||
user=self.user,
|
user=self.user,
|
||||||
client=self.client)
|
client=self.client)
|
||||||
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
|
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
|
||||||
|
|
||||||
del post_data['allow']
|
del data['allow']
|
||||||
query_str = urlencode(post_data).replace('+', '%20')
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
# Ensure user consent skip is enabled.
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
is_code_ok = is_code_valid(url=response['Location'],
|
is_code_ok = is_code_valid(url=response['Location'],
|
||||||
user=self.user,
|
user=self.user,
|
||||||
|
@ -255,10 +240,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
self.assertEqual(is_code_ok, True, msg='Code returned is invalid or missing.')
|
self.assertEqual(is_code_ok, True, msg='Code returned is invalid or missing.')
|
||||||
|
|
||||||
def test_response_uri_is_properly_constructed(self):
|
def test_response_uri_is_properly_constructed(self):
|
||||||
"""
|
data = {
|
||||||
TODO
|
|
||||||
"""
|
|
||||||
post_data = {
|
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri + "?redirect_state=xyz",
|
'redirect_uri': self.client.default_redirect_uri + "?redirect_state=xyz",
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
|
@ -267,100 +249,85 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
'allow': 'Accept',
|
'allow': 'Accept',
|
||||||
}
|
}
|
||||||
|
|
||||||
request = self.factory.post(reverse('oidc_provider:authorize'),
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
data=post_data)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
# TODO
|
||||||
|
|
||||||
is_code_ok = is_code_valid(url=response['Location'],
|
def test_public_client_auto_approval(self):
|
||||||
user=self.user,
|
|
||||||
client=self.client)
|
|
||||||
self.assertEqual(is_code_ok, True,
|
|
||||||
msg='Code returned is invalid.')
|
|
||||||
|
|
||||||
def test_scope_with_plus(self):
|
|
||||||
"""
|
"""
|
||||||
In query string, scope use `+` instead of the space url-encoded.
|
It's recommended not auto-approving requests for non-confidential clients.
|
||||||
"""
|
"""
|
||||||
scope_test = 'openid email profile'
|
data = {
|
||||||
|
'client_id': self.client_public.client_id,
|
||||||
query_str = urlencode({
|
|
||||||
'client_id': self.client.client_id,
|
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client_public.default_redirect_uri,
|
||||||
'scope': scope_test,
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
})
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True):
|
||||||
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
self.assertEqual('Request for Permission' in response.content.decode('utf-8'), True)
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
def test_implicit_missing_nonce(self):
|
||||||
|
|
||||||
self.assertEqual(scope_test in response.content.decode('utf-8'), True)
|
|
||||||
|
|
||||||
|
|
||||||
class ImplicitFlowTestCase(TestCase):
|
|
||||||
"""
|
|
||||||
Test cases for Authorize Endpoint using Implicit Grant Flow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
self.user = create_fake_user()
|
|
||||||
self.client = create_fake_client(response_type='id_token token')
|
|
||||||
self.state = uuid.uuid4().hex
|
|
||||||
self.nonce = uuid.uuid4().hex
|
|
||||||
create_rsakey()
|
|
||||||
|
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
query_str = urlencode({
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client_implicit.client_id,
|
||||||
'response_type': self.client.response_type,
|
'response_type': self.client_implicit.response_type,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client_implicit.default_redirect_uri,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
}).replace('+', '%20')
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
self.assertEqual('#error=invalid_request' in response['Location'], True)
|
self.assertEqual('#error=invalid_request' in response['Location'], True)
|
||||||
|
|
||||||
def test_access_token_response(self):
|
def test_implicit_access_token_response(self):
|
||||||
"""
|
"""
|
||||||
Unlike the Authorization Code flow, in which the client makes
|
Unlike the Authorization Code flow, in which the client makes
|
||||||
separate requests for authorization and for an access token, the client
|
separate requests for authorization and for an access token, the client
|
||||||
receives the access token as the result of the authorization request.
|
receives the access token as the result of the authorization request.
|
||||||
"""
|
"""
|
||||||
post_data = {
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client_implicit.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client_implicit.default_redirect_uri,
|
||||||
'response_type': self.client.response_type,
|
'response_type': self.client_implicit.response_type,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
'nonce': self.nonce,
|
'nonce': self.nonce,
|
||||||
'allow': 'Accept',
|
'allow': 'Accept',
|
||||||
}
|
}
|
||||||
|
|
||||||
request = self.factory.post(reverse('oidc_provider:authorize'),
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
data=post_data)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
self.assertEqual('access_token' in response['Location'], True)
|
self.assertEqual('access_token' in response['Location'], True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_parameter(self):
|
||||||
|
"""
|
||||||
|
Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
|
||||||
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
'client_id': self.client.client_id,
|
||||||
|
'response_type': self.client.response_type,
|
||||||
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
|
'scope': 'openid email',
|
||||||
|
'state': self.state,
|
||||||
|
}
|
||||||
|
|
||||||
|
data['prompt'] = 'none'
|
||||||
|
|
||||||
|
response = self._auth_request('get', data)
|
||||||
|
|
||||||
|
# An error is returned if an End-User is not already authenticated.
|
||||||
|
self.assertEqual('login_required' in response['Location'], 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.
|
||||||
|
self.assertEqual('interaction_required' in response['Location'], True)
|
||||||
|
|
|
@ -4,6 +4,7 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
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 jwkest.jwk import KEYS
|
from jwkest.jwk import KEYS
|
||||||
|
@ -23,10 +24,10 @@ class TokenTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
call_command('creatersakey')
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.user = create_fake_user()
|
self.user = create_fake_user()
|
||||||
self.client = create_fake_client(response_type='code')
|
self.client = create_fake_client(response_type='code')
|
||||||
create_rsakey()
|
|
||||||
|
|
||||||
def _auth_code_post_data(self, code):
|
def _auth_code_post_data(self, code):
|
||||||
"""
|
"""
|
||||||
|
@ -445,3 +446,22 @@ 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)
|
||||||
|
|
||||||
|
def test_pkce_parameters(self):
|
||||||
|
"""
|
||||||
|
Test Proof Key for Code Exchange by OAuth Public Clients.
|
||||||
|
https://tools.ietf.org/html/rfc7636
|
||||||
|
"""
|
||||||
|
code = create_code(user=self.user, client=self.client,
|
||||||
|
scope=['openid', 'email'], nonce=FAKE_NONCE, is_authentication=True,
|
||||||
|
code_challenge=FAKE_CODE_CHALLENGE, code_challenge_method='S256')
|
||||||
|
code.save()
|
||||||
|
|
||||||
|
post_data = self._auth_code_post_data(code=code.code)
|
||||||
|
|
||||||
|
# Add parameters.
|
||||||
|
post_data['code_verifier'] = FAKE_CODE_VERIFIER
|
||||||
|
|
||||||
|
response = self._post_request(post_data)
|
||||||
|
|
||||||
|
response_dic = json.loads(response.content.decode('utf-8'))
|
||||||
|
|
|
@ -1,13 +1,44 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from oidc_provider.lib.utils.common import get_issuer
|
from oidc_provider.lib.utils.common import get_issuer
|
||||||
|
|
||||||
|
|
||||||
|
class Request(object):
|
||||||
|
"""
|
||||||
|
Mock request object.
|
||||||
|
"""
|
||||||
|
scheme = 'http'
|
||||||
|
|
||||||
|
def get_host(self):
|
||||||
|
return 'host-from-request:8888'
|
||||||
|
|
||||||
|
|
||||||
class CommonTest(TestCase):
|
class CommonTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Test cases for common utils.
|
Test cases for common utils.
|
||||||
"""
|
"""
|
||||||
def test_get_issuer(self):
|
def test_get_issuer(self):
|
||||||
issuer = get_issuer()
|
request = Request()
|
||||||
self.assertEqual(issuer, settings.SITE_URL + '/openid')
|
|
||||||
|
# from default settings
|
||||||
|
self.assertEqual(get_issuer(),
|
||||||
|
'http://localhost:8000/openid')
|
||||||
|
|
||||||
|
# from custom settings
|
||||||
|
with self.settings(SITE_URL='http://otherhost:8000'):
|
||||||
|
self.assertEqual(get_issuer(),
|
||||||
|
'http://otherhost:8000/openid')
|
||||||
|
|
||||||
|
# `SITE_URL` not set, from `request`
|
||||||
|
with self.settings(SITE_URL=''):
|
||||||
|
self.assertEqual(get_issuer(request=request),
|
||||||
|
'http://host-from-request:8888/openid')
|
||||||
|
|
||||||
|
# use settings first if both are provided
|
||||||
|
self.assertEqual(get_issuer(request=request),
|
||||||
|
'http://localhost:8000/openid')
|
||||||
|
|
||||||
|
# `site_url` can even be overridden manually
|
||||||
|
self.assertEqual(get_issuer(site_url='http://127.0.0.1:9000',
|
||||||
|
request=request),
|
||||||
|
'http://127.0.0.1:9000/openid')
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from django.contrib.auth.views import redirect_to_login, logout
|
from django.contrib.auth.views import redirect_to_login, logout
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
@ -14,9 +12,9 @@ from oidc_provider.lib.claims import StandardScopeClaims
|
||||||
from oidc_provider.lib.endpoints.authorize import *
|
from oidc_provider.lib.endpoints.authorize import *
|
||||||
from oidc_provider.lib.endpoints.token import *
|
from oidc_provider.lib.endpoints.token import *
|
||||||
from oidc_provider.lib.errors import *
|
from oidc_provider.lib.errors import *
|
||||||
from oidc_provider.lib.utils.common import redirect, get_issuer
|
from oidc_provider.lib.utils.common import redirect, get_site_url, get_issuer
|
||||||
from oidc_provider.lib.utils.oauth2 import protected_resource_view
|
from oidc_provider.lib.utils.oauth2 import protected_resource_view
|
||||||
from oidc_provider.models import Client, RSAKey
|
from oidc_provider.models import RESPONSE_TYPE_CHOICES, RSAKey
|
||||||
from oidc_provider import settings
|
from oidc_provider import settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,20 +38,31 @@ class AuthorizeView(View):
|
||||||
if hook_resp:
|
if hook_resp:
|
||||||
return hook_resp
|
return hook_resp
|
||||||
|
|
||||||
if settings.get('OIDC_SKIP_CONSENT_ALWAYS'):
|
if settings.get('OIDC_SKIP_CONSENT_ALWAYS') and not (authorize.client.client_type == 'public') \
|
||||||
|
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():
|
if authorize.client_has_user_consent() and not (authorize.client.client_type == 'public') \
|
||||||
|
and not (authorize.params.prompt == 'consent'):
|
||||||
return redirect(authorize.create_response_uri())
|
return redirect(authorize.create_response_uri())
|
||||||
|
|
||||||
|
if authorize.params.prompt == 'none':
|
||||||
|
raise AuthorizeError(authorize.params.redirect_uri, 'interaction_required', authorize.grant_type)
|
||||||
|
|
||||||
|
if authorize.params.prompt == 'login':
|
||||||
|
return redirect_to_login(request.get_full_path())
|
||||||
|
|
||||||
|
if authorize.params.prompt == 'select_account':
|
||||||
|
# TODO: see how we can support multiple accounts for the end-user.
|
||||||
|
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 = {
|
||||||
'params': authorize.params,
|
'params': authorize.params,
|
||||||
}
|
}
|
||||||
hidden_inputs = render_to_string(
|
hidden_inputs = render_to_string('oidc_provider/hidden_inputs.html', context)
|
||||||
'oidc_provider/hidden_inputs.html', context)
|
|
||||||
|
|
||||||
# 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.
|
||||||
|
@ -64,12 +73,15 @@ class AuthorizeView(View):
|
||||||
'client': authorize.client,
|
'client': authorize.client,
|
||||||
'hidden_inputs': hidden_inputs,
|
'hidden_inputs': hidden_inputs,
|
||||||
'params': authorize.params,
|
'params': authorize.params,
|
||||||
|
'scopes': authorize.get_scopes_information(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'oidc_provider/authorize.html', context)
|
return render(request, 'oidc_provider/authorize.html', context)
|
||||||
else:
|
else:
|
||||||
path = request.get_full_path()
|
if authorize.params.prompt == 'none':
|
||||||
return redirect_to_login(path)
|
raise AuthorizeError(authorize.params.redirect_uri, 'login_required', authorize.grant_type)
|
||||||
|
|
||||||
|
return redirect_to_login(request.get_full_path())
|
||||||
|
|
||||||
except (ClientIdError, RedirectUriError) as error:
|
except (ClientIdError, RedirectUriError) as error:
|
||||||
context = {
|
context = {
|
||||||
|
@ -87,15 +99,12 @@ class AuthorizeView(View):
|
||||||
return redirect(uri)
|
return redirect(uri)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
authorize = AuthorizeEndpoint(request)
|
authorize = AuthorizeEndpoint(request)
|
||||||
|
|
||||||
allow = True if request.POST.get('allow') else False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
authorize.validate_params()
|
authorize.validate_params()
|
||||||
|
|
||||||
if not 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)
|
||||||
|
@ -148,17 +157,16 @@ def userinfo(request, *args, **kwargs):
|
||||||
}
|
}
|
||||||
|
|
||||||
standard_claims = StandardScopeClaims(token.user, token.scope)
|
standard_claims = StandardScopeClaims(token.user, token.scope)
|
||||||
|
|
||||||
dic.update(standard_claims.create_response_dic())
|
dic.update(standard_claims.create_response_dic())
|
||||||
|
|
||||||
extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)(
|
if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'):
|
||||||
token.user, token.scope)
|
extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)(token.user, token.scope)
|
||||||
|
dic.update(extra_claims.create_response_dic())
|
||||||
dic.update(extra_claims.create_response_dic())
|
|
||||||
|
|
||||||
response = JsonResponse(dic, status=200)
|
response = JsonResponse(dic, status=200)
|
||||||
response['Cache-Control'] = 'no-store'
|
response['Cache-Control'] = 'no-store'
|
||||||
response['Pragma'] = 'no-cache'
|
response['Pragma'] = 'no-cache'
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -167,21 +175,20 @@ class ProviderInfoView(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
dic = dict()
|
dic = dict()
|
||||||
|
|
||||||
dic['issuer'] = get_issuer()
|
site_url = get_site_url(request=request)
|
||||||
|
dic['issuer'] = get_issuer(site_url=site_url, request=request)
|
||||||
|
|
||||||
SITE_URL = settings.get('SITE_URL')
|
dic['authorization_endpoint'] = site_url + reverse('oidc_provider:authorize')
|
||||||
|
dic['token_endpoint'] = site_url + reverse('oidc_provider:token')
|
||||||
|
dic['userinfo_endpoint'] = site_url + reverse('oidc_provider:userinfo')
|
||||||
|
dic['end_session_endpoint'] = site_url + reverse('oidc_provider:logout')
|
||||||
|
|
||||||
dic['authorization_endpoint'] = SITE_URL + reverse('oidc_provider:authorize')
|
types_supported = [x[0] for x in RESPONSE_TYPE_CHOICES]
|
||||||
dic['token_endpoint'] = SITE_URL + reverse('oidc_provider:token')
|
|
||||||
dic['userinfo_endpoint'] = SITE_URL + reverse('oidc_provider:userinfo')
|
|
||||||
dic['end_session_endpoint'] = SITE_URL + reverse('oidc_provider:logout')
|
|
||||||
|
|
||||||
types_supported = [x[0] for x in Client.RESPONSE_TYPE_CHOICES]
|
|
||||||
dic['response_types_supported'] = types_supported
|
dic['response_types_supported'] = types_supported
|
||||||
|
|
||||||
dic['jwks_uri'] = SITE_URL + reverse('oidc_provider:jwks')
|
dic['jwks_uri'] = site_url + reverse('oidc_provider:jwks')
|
||||||
|
|
||||||
dic['id_token_signing_alg_values_supported'] = ['RS256']
|
dic['id_token_signing_alg_values_supported'] = ['HS256', 'RS256']
|
||||||
|
|
||||||
# See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
|
# See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
|
||||||
dic['subject_types_supported'] = ['public']
|
dic['subject_types_supported'] = ['public']
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -7,7 +7,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='django-oidc-provider',
|
name='django-oidc-provider',
|
||||||
version='0.3.1',
|
version='0.3.6',
|
||||||
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',
|
||||||
|
|
5
tox.ini
5
tox.ini
|
@ -1,7 +1,7 @@
|
||||||
[tox]
|
[tox]
|
||||||
|
|
||||||
envlist=
|
envlist=
|
||||||
clean,py{27,34}-django{17,18,19},stats
|
clean,py{27,34}-django{17,18,19},py35-django{18,19},stats
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
|
||||||
|
@ -13,8 +13,9 @@ deps =
|
||||||
mock
|
mock
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
|
pip uninstall --yes django-oidc-provider
|
||||||
pip install -e .
|
pip install -e .
|
||||||
coverage run --omit=.tox/*,oidc_provider/tests/* {envbindir}/django-admin.py test oidc_provider --settings=oidc_provider.tests.app.settings
|
coverage run --omit=.tox/*,oidc_provider/tests/* {envbindir}/django-admin.py test {posargs:oidc_provider} --settings=oidc_provider.tests.app.settings
|
||||||
|
|
||||||
[testenv:clean]
|
[testenv:clean]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue