commit
c70dab13a8
48 changed files with 1472 additions and 597 deletions
|
@ -2,10 +2,15 @@ language: python
|
|||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
env:
|
||||
- DJANGO=1.7
|
||||
- DJANGO=1.8
|
||||
- DJANGO=1.9
|
||||
matrix:
|
||||
exclude:
|
||||
- python: "3.5"
|
||||
env: DJANGO=1.7
|
||||
install:
|
||||
- pip install -q django==$DJANGO
|
||||
- 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]
|
||||
|
||||
### [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
|
||||
- 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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
[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
|
||||
|
||||
|
@ -23,5 +23,5 @@ We love contributions, so please feel free to fix bugs, improve things, provide
|
|||
|
||||
* Fork the project.
|
||||
* 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.
|
||||
|
|
|
@ -53,9 +53,9 @@ author = u'Juan Ignacio Fiorentino'
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'0.2'
|
||||
version = u'0.3'
|
||||
# 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
|
||||
# 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.
|
||||
|
||||
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:
|
||||
|
||||
* 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.
|
||||
* Only support for requesting Claims using Scope Values.
|
||||
* This library covers **Authorization Code Flow** and **Implicit Flow**, NO support for **Hybrid Flow** at this moment.
|
||||
* Supports only for requesting Claims using Scope values.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
@ -17,12 +22,13 @@ Contents:
|
|||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
sections/installation
|
||||
sections/clients
|
||||
sections/relyingparties
|
||||
sections/serverkeys
|
||||
sections/templates
|
||||
sections/claims
|
||||
sections/userconsent
|
||||
sections/oauth2
|
||||
sections/settings
|
||||
sections/contribute
|
||||
|
@ -34,4 +40,3 @@ Indices and tables
|
|||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
|
|
@ -12,17 +12,17 @@ List of all the attributes grouped by scopes:
|
|||
+--------------------+----------------+-----------------------+------------------------+
|
||||
| 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 | | | |
|
||||
+--------------------+----------------+-----------------------+------------------------+
|
||||
|
@ -41,35 +41,22 @@ List of all the attributes grouped by scopes:
|
|||
| updated_at | | | |
|
||||
+--------------------+----------------+-----------------------+------------------------+
|
||||
|
||||
Example using a django model::
|
||||
Somewhere in your Django ``settings.py``::
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
OIDC_USERINFO = 'myproject.oidc_provider_settings.userinfo'
|
||||
|
||||
|
||||
class UserInfo(models.Model):
|
||||
Then create the function for the ``OIDC_USERINFO`` setting::
|
||||
|
||||
GENDER_CHOICES = [
|
||||
('F', 'Female'),
|
||||
('M', 'Male'),
|
||||
]
|
||||
def userinfo(claims, user):
|
||||
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True)
|
||||
|
||||
given_name = models.CharField(max_length=255, blank=True, null=True)
|
||||
family_name = models.CharField(max_length=255, blank=True, null=True)
|
||||
gender = models.CharField(max_length=100, choices=GENDER_CHOICES, null=True)
|
||||
birthdate = models.DateField(null=True)
|
||||
updated_at = models.DateTimeField(auto_now=True, null=True)
|
||||
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'] = '...'
|
||||
|
||||
email_verified = models.NullBooleanField(default=False)
|
||||
return claims
|
||||
|
||||
phone_number = models.CharField(max_length=255, blank=True, null=True)
|
||||
phone_number_verified = models.NullBooleanField(default=False)
|
||||
|
||||
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)
|
||||
.. 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.
|
||||
|
|
|
@ -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.
|
||||
* 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.
|
||||
|
||||
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::
|
||||
|
||||
# Run all tests.
|
||||
$ 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::
|
||||
|
||||
$ 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.
|
||||
|
||||
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
|
||||
============
|
||||
|
||||
* Python: ``2.7`` ``3.4``
|
||||
* Python: ``2.7`` ``3.4`` ``3.5``
|
||||
* Django: ``1.7`` ``1.8`` ``1.9``
|
||||
|
||||
Quick Installation
|
||||
|
@ -46,5 +46,4 @@ Generate server RSA key and run migrations (if you don't)::
|
|||
|
||||
Add required variables to your project settings::
|
||||
|
||||
SITE_URL = 'http://localhost:8000'
|
||||
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.
|
||||
|
||||
SITE_URL
|
||||
========
|
||||
|
||||
REQUIRED. ``str``. The OP server url.
|
||||
|
||||
For example ``http://localhost:8000``.
|
||||
|
||||
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/``.
|
||||
|
||||
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
|
||||
=========================
|
||||
|
||||
|
@ -44,34 +46,36 @@ Expressed in seconds. Default is ``60*10``.
|
|||
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.
|
||||
|
||||
`Here <http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims>`_ you have the standard scopes defined by the protocol.
|
||||
|
||||
You can create or modify scopes using:
|
||||
|
||||
* ``info_scopename`` class property for setting the verbose name and description.
|
||||
* ``scope_scopename`` method for returning some information related.
|
||||
|
||||
Check out an example of how to implement it::
|
||||
|
||||
from 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):
|
||||
# Here you can load models that will be used
|
||||
# in more than one scope for example.
|
||||
# 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()
|
||||
info_books = (
|
||||
_(u'Books'), # Verbose name of the scope.
|
||||
_(u'Access to your books.'), # Description of the scope.
|
||||
)
|
||||
|
||||
def scope_books(self, user):
|
||||
|
||||
# Here you can search books for this user.
|
||||
def scope_books(self):
|
||||
# Here, for example, you can search books for this user.
|
||||
# self.user - Django user instance.
|
||||
# self.userinfo - Instance of your custom OIDC_USERINFO class.
|
||||
# self.scopes - List of scopes requested.
|
||||
|
||||
dic = {
|
||||
'books_readed': books_readed_count,
|
||||
|
@ -79,11 +83,14 @@ Check out an example of how to implement it::
|
|||
|
||||
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):``
|
||||
|
||||
If a field is empty or ``None`` will be cleaned from the response.
|
||||
.. note::
|
||||
If a field is empty or ``None`` inside the dictionary your return on ``scope_scopename`` method, it will be cleaned from the response.
|
||||
|
||||
OIDC_IDTOKEN_EXPIRE
|
||||
===================
|
||||
|
@ -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.
|
||||
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.
|
||||
|
||||
Default is::
|
||||
|
@ -155,4 +162,21 @@ Expressed in seconds. Default is ``60*60``.
|
|||
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
|
||||
*.pem
|
||||
|
||||
static/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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.
|
||||
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,800);
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-weight: 400;
|
||||
height: auto;
|
||||
padding-top: 10px;
|
||||
background-color: #536dfe;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ui.huge.header {
|
||||
font-size: 46px;
|
||||
font-weight: 800;
|
||||
#main-container {
|
||||
flex: 1 0 auto;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.ui.segment {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
footer {
|
||||
padding-top: 0px !important;
|
||||
}
|
|
@ -6,42 +6,46 @@
|
|||
<head>
|
||||
|
||||
<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, maximum-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-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' %}">
|
||||
|
||||
</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="ui stackable page grid">
|
||||
<div class="column">
|
||||
<div class="ui right aligned secondary segment">
|
||||
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>.
|
||||
<div class="navbar-fixed">
|
||||
<nav class="white">
|
||||
<div class="nav-wrapper">
|
||||
<a href="{% url 'home' %}" class="brand-logo center black-text">OpenID Provider</a>
|
||||
<ul id="nav-mobile" class="right hide-on-med-and-down">
|
||||
{% 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>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/semantic.min.js"></script>
|
||||
<div id="main-container" class="container">
|
||||
{% 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>
|
||||
</html>
|
|
@ -4,46 +4,21 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<div class="ui stackable page grid">
|
||||
<div class="row">
|
||||
<div class="center aligned column">
|
||||
<i class="thumbs outline up massive icon"></i>
|
||||
<h1 class="ui huge header">Congratulations! It works. <div class="sub header">... what's next?</div></h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="ui segments">
|
||||
<div class="ui segment">
|
||||
<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>
|
||||
<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>
|
||||
<h2 class="ui header">Server Endpoints</h2>
|
||||
<div class="ui list">
|
||||
<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 class="row">
|
||||
<div class="col s12 m10 offset-m1">
|
||||
<div class="card hoverable">
|
||||
<div class="card-content">
|
||||
<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>
|
||||
<p class="flow-text">Start by creating your clients <a href="{% url 'admin:index' %}oidc_provider/client/">here</a>.</p>
|
||||
<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="collection with-header">
|
||||
<div class="collection-header"><h4>Server Endpoints</h4></div>
|
||||
<a href="{% url 'oidc_provider:provider_info' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:provider_info' %}</a>
|
||||
<a href="{% url 'oidc_provider:jwks' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:jwks' %}</a>
|
||||
<a href="{% url 'oidc_provider:authorize' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:authorize' %}</a>
|
||||
<a href="{% url 'oidc_provider:token' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:token' %}</a>
|
||||
<a href="{% url 'oidc_provider:userinfo' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:userinfo' %}</a>
|
||||
<a href="{% url 'oidc_provider:logout' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:logout' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,28 +1,38 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="ui page grid">
|
||||
<div class="row">
|
||||
<div class="six wide centered column">
|
||||
{% if form.errors %}
|
||||
<div class="ui negative message">
|
||||
<p>Your username and password didn't match. Please try again.</p>
|
||||
<div class="row">
|
||||
<div class="col s12 m8 offset-m2 l6 offset-l3">
|
||||
<div class="card hoverable">
|
||||
<div class="card-content">
|
||||
<div class="row">
|
||||
{% 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>
|
||||
{% 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>
|
||||
|
|
|
@ -2,25 +2,24 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<div class="ui page grid">
|
||||
<div class="nine wide centered column">
|
||||
<div class="ui segment">
|
||||
<h1 class="ui dividing header">Request for Permission</h1>
|
||||
<p>Client <i>{{ client.name }}</i> would like to access this information of you.</p>
|
||||
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
||||
{% csrf_token %}
|
||||
{{ hidden_inputs }}
|
||||
<div class="ui bulleted list">
|
||||
{% for scope in params.scope %}
|
||||
<div class="item">{{ scope | capfirst }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="ui fluid large buttons">
|
||||
<input class="ui button" type="submit" value="Cancel" />
|
||||
<div class="or"></div>
|
||||
<input name="allow" class="positive ui button" type="submit" value="Authorize" />
|
||||
</div>
|
||||
</form>
|
||||
<div class="row">
|
||||
<div class="col s12 m8 offset-m2 l6 offset-l3">
|
||||
<h4 class="grey-text text-lighten-3">Request for Permission</h4>
|
||||
<div class="card hoverable">
|
||||
<div class="card-content">
|
||||
<p class="flow-text">Client <i>{{ client.name }}</i> would like to access this information of you.</p>
|
||||
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
||||
{% csrf_token %}
|
||||
{{ hidden_inputs }}
|
||||
<ul class="collection">
|
||||
{% for scope in params.scope %}
|
||||
<li class="collection-item">{{ scope | capfirst }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<input class="waves-effect waves-light btn grey" type="submit" value="Cancel" />
|
||||
<input name="allow" class="waves-effect waves-light btn green right" type="submit" value="Authorize" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<div class="ui page grid">
|
||||
<div class="nine wide centered column">
|
||||
<div class="ui icon negative large message">
|
||||
<i class="meh icon"></i>
|
||||
<div class="content">
|
||||
<div class="header">{{ error }}</div>
|
||||
<div class="row">
|
||||
<div class="col s12 m8 offset-m2 l6 offset-l3">
|
||||
<div class="card hoverable">
|
||||
<div class="card-content">
|
||||
<h4 class="center-align red-text text-lighten-1">{{ error }}</h4>
|
||||
<p>{{ description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@ urlpatterns = [
|
|||
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'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
|
||||
url(r'^', include('oidc_provider.urls', namespace='oidc_provider')),
|
||||
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
]
|
||||
|
|
|
@ -3,17 +3,22 @@ from django.utils.translation import ugettext as _
|
|||
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):
|
||||
self.user = user
|
||||
self.userinfo = settings.get('OIDC_USERINFO', import_str=True)(STANDARD_CLAIMS, self.user)
|
||||
self.scopes = scopes
|
||||
|
||||
self.setup()
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def create_response_dic(self):
|
||||
"""
|
||||
Generate the dic that will be jsonify. Checking scopes given vs
|
||||
|
@ -25,7 +30,7 @@ class AbstractScopeClaims(object):
|
|||
|
||||
for scope in self.scopes:
|
||||
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)
|
||||
|
||||
|
@ -39,7 +44,6 @@ class AbstractScopeClaims(object):
|
|||
scopes = []
|
||||
|
||||
for name in self.__class__.__dict__:
|
||||
|
||||
if name.startswith('scope_'):
|
||||
scope = name.split('scope_')[1]
|
||||
scopes.append(scope)
|
||||
|
@ -60,65 +64,91 @@ class AbstractScopeClaims(object):
|
|||
|
||||
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.
|
||||
See: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
try:
|
||||
self.userinfo = settings.get('OIDC_USERINFO',
|
||||
import_str=True).get_by_user(self.user)
|
||||
except:
|
||||
self.userinfo = None
|
||||
|
||||
def scope_profile(self, user):
|
||||
info_profile = (
|
||||
_(u'Basic profile'),
|
||||
_(u'Access to your basic information. Includes names, gender, birthdate and other information.'),
|
||||
)
|
||||
def scope_profile(self):
|
||||
dic = {
|
||||
'name': getattr(self.userinfo, 'name', None),
|
||||
'given_name': getattr(self.userinfo, 'given_name', None),
|
||||
'family_name': getattr(self.userinfo, 'family_name', None),
|
||||
'middle_name': getattr(self.userinfo, 'middle_name', None),
|
||||
'nickname': getattr(self.userinfo, 'nickname', None),
|
||||
'preferred_username': getattr(self.userinfo, 'preferred_username', None),
|
||||
'profile': getattr(self.userinfo, 'profile', None),
|
||||
'picture': getattr(self.userinfo, 'picture', None),
|
||||
'website': getattr(self.userinfo, 'website', None),
|
||||
'gender': getattr(self.userinfo, 'gender', None),
|
||||
'birthdate': getattr(self.userinfo, 'birthdate', None),
|
||||
'zoneinfo': getattr(self.userinfo, 'zoneinfo', None),
|
||||
'locale': getattr(self.userinfo, 'locale', None),
|
||||
'updated_at': getattr(self.userinfo, 'updated_at', None),
|
||||
'name': self.userinfo.get('name'),
|
||||
'given_name': self.userinfo.get('given_name'),
|
||||
'family_name': self.userinfo.get('family_name'),
|
||||
'middle_name': self.userinfo.get('middle_name'),
|
||||
'nickname': self.userinfo.get('nickname'),
|
||||
'preferred_username': self.userinfo.get('preferred_username'),
|
||||
'profile': self.userinfo.get('profile'),
|
||||
'picture': self.userinfo.get('picture'),
|
||||
'website': self.userinfo.get('website'),
|
||||
'gender': self.userinfo.get('gender'),
|
||||
'birthdate': self.userinfo.get('birthdate'),
|
||||
'zoneinfo': self.userinfo.get('zoneinfo'),
|
||||
'locale': self.userinfo.get('locale'),
|
||||
'updated_at': self.userinfo.get('updated_at'),
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
def scope_email(self, user):
|
||||
info_email = (
|
||||
_(u'Email'),
|
||||
_(u'Access to your email address.'),
|
||||
)
|
||||
def scope_email(self):
|
||||
dic = {
|
||||
'email': getattr(self.user, 'email', None),
|
||||
'email_verified': getattr(self.userinfo, 'email_verified', None),
|
||||
'email': self.userinfo.get('email'),
|
||||
'email_verified': self.userinfo.get('email_verified'),
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
def scope_phone(self, user):
|
||||
info_phone = (
|
||||
_(u'Phone number'),
|
||||
_(u'Access to your phone number.'),
|
||||
)
|
||||
def scope_phone(self):
|
||||
dic = {
|
||||
'phone_number': getattr(self.userinfo, 'phone_number', None),
|
||||
'phone_number_verified': getattr(self.userinfo, 'phone_number_verified', None),
|
||||
'phone_number': self.userinfo.get('phone_number'),
|
||||
'phone_number_verified': self.userinfo.get('phone_number_verified'),
|
||||
}
|
||||
|
||||
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 = {
|
||||
'address': {
|
||||
'formatted': getattr(self.userinfo, 'address_formatted', None),
|
||||
'street_address': getattr(self.userinfo, 'address_street_address', None),
|
||||
'locality': getattr(self.userinfo, 'address_locality', None),
|
||||
'region': getattr(self.userinfo, 'address_region', None),
|
||||
'postal_code': getattr(self.userinfo, 'address_postal_code', None),
|
||||
'country': getattr(self.userinfo, 'address_country', None),
|
||||
'formatted': self.userinfo.get('address', {}).get('formatted'),
|
||||
'street_address': self.userinfo.get('address', {}).get('street_address'),
|
||||
'locality': self.userinfo.get('address', {}).get('locality'),
|
||||
'region': self.userinfo.get('address', {}).get('region'),
|
||||
'postal_code': self.userinfo.get('address', {}).get('postal_code'),
|
||||
'country': self.userinfo.get('address', {}).get('country'),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from datetime import timedelta
|
||||
import logging
|
||||
try:
|
||||
from urllib import urlencode
|
||||
|
@ -8,6 +7,7 @@ except ImportError:
|
|||
|
||||
from django.utils import timezone
|
||||
|
||||
from oidc_provider.lib.claims import StandardScopeClaims
|
||||
from oidc_provider.lib.errors import *
|
||||
from oidc_provider.lib.utils.params import *
|
||||
from oidc_provider.lib.utils.token import *
|
||||
|
@ -35,7 +35,7 @@ class AuthorizeEndpoint(object):
|
|||
self.grant_type = None
|
||||
|
||||
# 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):
|
||||
"""
|
||||
|
@ -54,38 +54,50 @@ class AuthorizeEndpoint(object):
|
|||
self.params.response_type = query_dict.get('response_type', '')
|
||||
self.params.scope = query_dict.get('scope', '').split()
|
||||
self.params.state = query_dict.get('state', '')
|
||||
|
||||
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):
|
||||
# Client validation.
|
||||
try:
|
||||
self.client = Client.objects.get(client_id=self.params.client_id)
|
||||
except Client.DoesNotExist:
|
||||
logger.debug('[Authorize] Invalid client identifier: %s', self.params.client_id)
|
||||
raise ClientIdError()
|
||||
|
||||
# Redirect URI validation.
|
||||
if self.is_authentication and not self.params.redirect_uri:
|
||||
logger.debug('[Authorize] Missing redirect uri.')
|
||||
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 = urlunsplit(clean_redirect_uri._replace(query=''))
|
||||
if not (clean_redirect_uri in self.client.redirect_uris):
|
||||
logger.debug('[Authorize] Invalid redirect uri: %s', self.params.redirect_uri)
|
||||
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):
|
||||
uri = urlsplit(self.params.redirect_uri)
|
||||
|
@ -99,8 +111,10 @@ class AuthorizeEndpoint(object):
|
|||
client=self.client,
|
||||
scope=self.params.scope,
|
||||
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()
|
||||
|
||||
query_params['code'] = code.code
|
||||
|
@ -112,8 +126,9 @@ class AuthorizeEndpoint(object):
|
|||
id_token_dic = create_id_token(
|
||||
user=self.request.user,
|
||||
aud=self.client.client_id,
|
||||
nonce=self.params.nonce)
|
||||
query_fragment['id_token'] = encode_id_token(id_token_dic)
|
||||
nonce=self.params.nonce,
|
||||
request=self.request)
|
||||
query_fragment['id_token'] = encode_id_token(id_token_dic, self.client)
|
||||
else:
|
||||
id_token_dic = {}
|
||||
|
||||
|
@ -155,18 +170,24 @@ class AuthorizeEndpoint(object):
|
|||
|
||||
Return None.
|
||||
"""
|
||||
expires_at = timezone.now() + timedelta(
|
||||
date_given = timezone.now()
|
||||
expires_at = date_given + timedelta(
|
||||
days=settings.get('OIDC_SKIP_CONSENT_EXPIRE'))
|
||||
|
||||
uc, created = UserConsent.objects.get_or_create(
|
||||
user=self.request.user,
|
||||
client=self.client,
|
||||
defaults={'expires_at': expires_at})
|
||||
defaults={
|
||||
'expires_at': expires_at,
|
||||
'date_given': date_given,
|
||||
}
|
||||
)
|
||||
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:
|
||||
uc.expires_at = expires_at
|
||||
uc.date_given = date_given
|
||||
|
||||
uc.save()
|
||||
|
||||
|
@ -187,3 +208,19 @@ class AuthorizeEndpoint(object):
|
|||
pass
|
||||
|
||||
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 re
|
||||
try:
|
||||
|
@ -30,14 +31,16 @@ class TokenEndpoint(object):
|
|||
|
||||
self.params.client_id = client_id
|
||||
self.params.client_secret = client_secret
|
||||
self.params.redirect_uri = unquote(
|
||||
self.request.POST.get('redirect_uri', ''))
|
||||
self.params.redirect_uri = unquote(self.request.POST.get('redirect_uri', ''))
|
||||
self.params.grant_type = self.request.POST.get('grant_type', '')
|
||||
self.params.code = self.request.POST.get('code', '')
|
||||
self.params.state = self.request.POST.get('state', '')
|
||||
self.params.scope = self.request.POST.get('scope', '')
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
raise TokenError('invalid_client')
|
||||
|
||||
if not (self.client.client_secret == self.params.client_secret):
|
||||
logger.debug('[Token] Invalid client secret: client %s do not have secret %s',
|
||||
self.client.client_id, self.client.client_secret)
|
||||
raise TokenError('invalid_client')
|
||||
if self.client.client_type == 'confidential':
|
||||
if not (self.client.client_secret == self.params.client_secret):
|
||||
logger.debug('[Token] Invalid client secret: client %s do not have secret %s',
|
||||
self.client.client_id, self.client.client_secret)
|
||||
raise TokenError('invalid_client')
|
||||
|
||||
if self.params.grant_type == 'authorization_code':
|
||||
if not (self.params.redirect_uri in self.client.redirect_uris):
|
||||
|
@ -90,6 +94,19 @@ class TokenEndpoint(object):
|
|||
self.params.redirect_uri)
|
||||
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':
|
||||
if not self.params.refresh_token:
|
||||
logger.debug('[Token] Missing refresh token')
|
||||
|
@ -119,6 +136,7 @@ class TokenEndpoint(object):
|
|||
user=self.code.user,
|
||||
aud=self.client.client_id,
|
||||
nonce=self.code.nonce,
|
||||
request=self.request,
|
||||
)
|
||||
else:
|
||||
id_token_dic = {}
|
||||
|
@ -140,7 +158,7 @@ class TokenEndpoint(object):
|
|||
'refresh_token': token.refresh_token,
|
||||
'token_type': 'bearer',
|
||||
'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
|
||||
|
@ -152,6 +170,7 @@ class TokenEndpoint(object):
|
|||
user=self.token.user,
|
||||
aud=self.client.client_id,
|
||||
nonce=None,
|
||||
request=self.request,
|
||||
)
|
||||
else:
|
||||
id_token_dic = {}
|
||||
|
@ -173,7 +192,7 @@ class TokenEndpoint(object):
|
|||
'refresh_token': token.refresh_token,
|
||||
'token_type': 'bearer',
|
||||
'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
|
||||
|
|
|
@ -13,12 +13,31 @@ def redirect(uri):
|
|||
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
|
||||
appended.
|
||||
"""
|
||||
site_url = settings.get('SITE_URL')
|
||||
site_url = get_site_url(site_url=site_url, request=request)
|
||||
path = reverse('oidc_provider:provider_info') \
|
||||
.split('/.well-known/openid-configuration')[0]
|
||||
issuer = site_url + path
|
||||
|
@ -26,14 +45,12 @@ def get_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.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_by_user(cls, user):
|
||||
return None
|
||||
return claims
|
||||
|
||||
|
||||
def default_sub_generator(user):
|
||||
|
|
|
@ -4,8 +4,8 @@ import uuid
|
|||
|
||||
from Crypto.PublicKey.RSA import importKey
|
||||
from django.utils import timezone
|
||||
from hashlib import md5
|
||||
from jwkest.jwk import RSAKey as jwk_RSAKey
|
||||
from jwkest.jwk import SYMKey
|
||||
from jwkest.jws import JWS
|
||||
|
||||
from oidc_provider.lib.utils.common import get_issuer
|
||||
|
@ -13,7 +13,7 @@ from oidc_provider.models import *
|
|||
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).
|
||||
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()))
|
||||
|
||||
dic = {
|
||||
'iss': get_issuer(),
|
||||
'iss': get_issuer(request=request),
|
||||
'sub': sub,
|
||||
'aud': str(aud),
|
||||
'exp': exp_time,
|
||||
|
@ -55,21 +55,26 @@ def create_id_token(user, aud, nonce):
|
|||
return dic
|
||||
|
||||
|
||||
def encode_id_token(payload):
|
||||
def encode_id_token(payload, client):
|
||||
"""
|
||||
Represent the ID Token as a JSON Web Token (JWT).
|
||||
|
||||
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():
|
||||
keys.append(jwk_RSAKey(key=importKey(rsakey.key), kid=rsakey.kid))
|
||||
|
||||
if not keys:
|
||||
raise Exception('You must add at least one RSA Key.')
|
||||
if not keys:
|
||||
raise Exception('You must add at least one RSA Key.')
|
||||
elif alg == 'HS256':
|
||||
keys = [SYMKey(key=client.client_secret, alg=alg)]
|
||||
else:
|
||||
raise Exception('Unsupported key algorithm.')
|
||||
|
||||
_jws = JWS(payload, alg='RS256')
|
||||
_jws = JWS(payload, alg=alg)
|
||||
|
||||
return _jws.sign_compact(keys)
|
||||
|
||||
|
@ -95,7 +100,8 @@ def create_token(user, client, id_token_dic, scope):
|
|||
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.
|
||||
|
||||
|
@ -104,7 +110,13 @@ def create_code(user, client, scope, nonce, is_authentication):
|
|||
code = Code()
|
||||
code.user = user
|
||||
code.client = client
|
||||
|
||||
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(
|
||||
seconds=settings.get('OIDC_CODE_EXPIRE'))
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
|
||||
CLIENT_TYPE_CHOICES = [
|
||||
('confidential', 'Confidential'),
|
||||
('public', 'Public'),
|
||||
]
|
||||
name = models.CharField(max_length=100, default='', verbose_name=_(u'Name'))
|
||||
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.'))
|
||||
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 = [
|
||||
('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.'))
|
||||
_redirect_uris = models.TextField(default='', verbose_name=_(u'Redirect URIs'), help_text=_(u'Enter each URI on a new line.'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _(u'Client')
|
||||
|
@ -39,7 +45,7 @@ class Client(models.Model):
|
|||
|
||||
def __unicode__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
def redirect_uris():
|
||||
def fget(self):
|
||||
return self._redirect_uris.splitlines()
|
||||
|
@ -55,10 +61,10 @@ class Client(models.Model):
|
|||
|
||||
class BaseCodeTokenModel(models.Model):
|
||||
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||
client = models.ForeignKey(Client)
|
||||
expires_at = models.DateTimeField()
|
||||
_scope = models.TextField(default='')
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_(u'User'))
|
||||
client = models.ForeignKey(Client, verbose_name=_(u'Client'))
|
||||
expires_at = models.DateTimeField(verbose_name=_(u'Expiration Date'))
|
||||
_scope = models.TextField(default='', verbose_name=_(u'Scopes'))
|
||||
|
||||
def scope():
|
||||
def fget(self):
|
||||
|
@ -72,20 +78,22 @@ class BaseCodeTokenModel(models.Model):
|
|||
return timezone.now() >= self.expires_at
|
||||
|
||||
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):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Code(BaseCodeTokenModel):
|
||||
|
||||
code = models.CharField(max_length=255, unique=True)
|
||||
nonce = models.CharField(max_length=255, blank=True, default='')
|
||||
is_authentication = models.BooleanField(default=False)
|
||||
code = models.CharField(max_length=255, unique=True, verbose_name=_(u'Code'))
|
||||
nonce = models.CharField(max_length=255, blank=True, default='', verbose_name=_(u'Nonce'))
|
||||
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:
|
||||
verbose_name = _(u'Authorization Code')
|
||||
|
@ -94,9 +102,9 @@ class Code(BaseCodeTokenModel):
|
|||
|
||||
class Token(BaseCodeTokenModel):
|
||||
|
||||
access_token = models.CharField(max_length=255, unique=True)
|
||||
refresh_token = models.CharField(max_length=255, unique=True, null=True)
|
||||
_id_token = models.TextField()
|
||||
access_token = models.CharField(max_length=255, unique=True, verbose_name=_(u'Access Token'))
|
||||
refresh_token = models.CharField(max_length=255, unique=True, null=True, verbose_name=_(u'Refresh Token'))
|
||||
_id_token = models.TextField(verbose_name=_(u'ID Token'))
|
||||
def id_token():
|
||||
def fget(self):
|
||||
return json.loads(self._id_token)
|
||||
|
@ -112,13 +120,15 @@ class Token(BaseCodeTokenModel):
|
|||
|
||||
class UserConsent(BaseCodeTokenModel):
|
||||
|
||||
date_given = models.DateTimeField(verbose_name=_(u'Date Given'))
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'client')
|
||||
|
||||
|
||||
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:
|
||||
verbose_name = _(u'RSA Key')
|
||||
|
|
|
@ -4,6 +4,9 @@ from django.conf import settings
|
|||
|
||||
|
||||
class DefaultSettings(object):
|
||||
required_attrs = (
|
||||
'LOGIN_URL',
|
||||
)
|
||||
|
||||
@property
|
||||
def LOGIN_URL(self):
|
||||
|
@ -15,7 +18,7 @@ class DefaultSettings(object):
|
|||
@property
|
||||
def SITE_URL(self):
|
||||
"""
|
||||
REQUIRED. The OP server url.
|
||||
OPTIONAL. The OP server url.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
@ -38,9 +41,9 @@ class DefaultSettings(object):
|
|||
def OIDC_EXTRA_SCOPE_CLAIMS(self):
|
||||
"""
|
||||
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
|
||||
def OIDC_IDTOKEN_EXPIRE(self):
|
||||
|
@ -92,10 +95,10 @@ class DefaultSettings(object):
|
|||
@property
|
||||
def OIDC_USERINFO(self):
|
||||
"""
|
||||
OPTIONAL. A string with the location of your class.
|
||||
Used to add extra scopes specific for your app.
|
||||
OPTIONAL. A string with the location of your function.
|
||||
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
|
||||
def OIDC_IDTOKEN_PROCESSING_HOOK(self):
|
||||
|
@ -131,7 +134,7 @@ def get(name, import_str=False):
|
|||
value = getattr(default_settings, name)
|
||||
value = getattr(settings, name)
|
||||
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.')
|
||||
|
||||
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>
|
||||
|
||||
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
||||
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
{{ hidden_inputs }}
|
||||
|
||||
<ul>
|
||||
{% for scope in params.scope %}
|
||||
<li>{{ scope | capfirst }}</li>
|
||||
{% endfor %}
|
||||
{% for scope in scopes %}
|
||||
<li><strong>{{ scope.name }}</strong> <br><i>{{ scope.description }}</i></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<input type="submit" value="Decline" />
|
||||
|
|
|
@ -3,4 +3,6 @@
|
|||
<input name="response_type" type="hidden" value="{{ params.response_type }}" />
|
||||
<input name="scope" type="hidden" value="{{ params.scope | join:' ' }}" />
|
||||
<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',
|
||||
]
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# OIDC Provider settings.
|
||||
|
||||
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_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():
|
||||
|
@ -31,7 +33,7 @@ def create_fake_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:
|
||||
'code', 'id_token' or 'id_token token'.
|
||||
|
@ -40,8 +42,12 @@ def create_fake_client(response_type):
|
|||
"""
|
||||
client = Client()
|
||||
client.name = 'Some Client'
|
||||
client.client_id = '123'
|
||||
client.client_secret = '456'
|
||||
client.client_id = str(random.randint(1, 999999)).zfill(6)
|
||||
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.redirect_uris = ['http://example.com/']
|
||||
|
||||
|
@ -50,17 +56,6 @@ def create_fake_client(response_type):
|
|||
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):
|
||||
"""
|
||||
Check if the code inside the url is valid.
|
||||
|
@ -78,27 +73,16 @@ def is_code_valid(url, user, client):
|
|||
return is_code_ok
|
||||
|
||||
|
||||
class FakeUserInfo(object):
|
||||
def userinfo(claims, user):
|
||||
"""
|
||||
Fake class for setting OIDC_USERINFO.
|
||||
Fake function for setting OIDC_USERINFO.
|
||||
"""
|
||||
|
||||
given_name = 'John'
|
||||
family_name = 'Doe'
|
||||
nickname = 'johndoe'
|
||||
website = 'http://johndoe.com'
|
||||
|
||||
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()
|
||||
claims['given_name'] = 'John'
|
||||
claims['family_name'] = 'Doe'
|
||||
claims['name'] = '{0} {1}'.format(claims['given_name'], claims['family_name'])
|
||||
claims['email'] = user.email
|
||||
claims['address']['country'] = 'Argentina'
|
||||
return claims
|
||||
|
||||
|
||||
def fake_sub_generator(user):
|
||||
|
|
|
@ -6,6 +6,7 @@ import uuid
|
|||
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.management import call_command
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import RequestFactory
|
||||
from django.test import TestCase
|
||||
|
@ -22,10 +23,35 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
|||
"""
|
||||
|
||||
def setUp(self):
|
||||
call_command('creatersakey')
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_fake_user()
|
||||
self.client = create_fake_client(response_type='code')
|
||||
self.client_public = create_fake_client(response_type='code', is_public=True)
|
||||
self.client_implicit = create_fake_client(response_type='id_token token')
|
||||
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):
|
||||
"""
|
||||
|
@ -35,11 +61,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
|||
|
||||
See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
||||
"""
|
||||
url = reverse('oidc_provider:authorize')
|
||||
|
||||
request = self.factory.get(url)
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
response = self._auth_request('get')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
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
|
||||
"""
|
||||
# Create an authorize request with an unsupported response_type.
|
||||
query_str = urlencode({
|
||||
data = {
|
||||
'client_id': self.client.client_id,
|
||||
'response_type': 'something_wrong',
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'scope': 'openid email',
|
||||
'state': self.state,
|
||||
}).replace('+', '%20')
|
||||
}
|
||||
|
||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
||||
|
||||
request = self.factory.get(url)
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
response = self._auth_request('get', data)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
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
|
||||
"""
|
||||
query_str = urlencode({
|
||||
data = {
|
||||
'client_id': self.client.client_id,
|
||||
'response_type': 'code',
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'scope': 'openid email',
|
||||
'state': self.state,
|
||||
}).replace('+', '%20')
|
||||
}
|
||||
|
||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
||||
|
||||
request = self.factory.get(url)
|
||||
request.user = AnonymousUser()
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
response = self._auth_request('get', data)
|
||||
|
||||
# Check if user was redirected to the login view.
|
||||
login_url_exists = settings.get('LOGIN_URL') in response['Location']
|
||||
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):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
query_str = urlencode({
|
||||
data = {
|
||||
'client_id': self.client.client_id,
|
||||
'response_type': 'code',
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'scope': 'openid email',
|
||||
'state': self.state,
|
||||
}).replace('+', '%20')
|
||||
# PKCE parameters.
|
||||
'code_challenge': FAKE_CODE_CHALLENGE,
|
||||
'code_challenge_method': 'S256',
|
||||
}
|
||||
|
||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
||||
|
||||
request = self.factory.get(url)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||
|
||||
# Check if hidden inputs exists in the form,
|
||||
# also if their values are valid.
|
||||
|
@ -140,6 +141,8 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
|||
'client_id': self.client.client_id,
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'response_type': 'code',
|
||||
'code_challenge': FAKE_CODE_CHALLENGE,
|
||||
'code_challenge_method': 'S256',
|
||||
}
|
||||
|
||||
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]
|
||||
by adding them as query parameters to the redirect_uri.
|
||||
"""
|
||||
response_type = 'code'
|
||||
|
||||
url = reverse('oidc_provider:authorize')
|
||||
|
||||
post_data = {
|
||||
data = {
|
||||
'client_id': self.client.client_id,
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'response_type': response_type,
|
||||
'response_type': 'code',
|
||||
'scope': 'openid email',
|
||||
'state': self.state,
|
||||
# PKCE parameters.
|
||||
'code_challenge': FAKE_CODE_CHALLENGE,
|
||||
'code_challenge_method': 'S256',
|
||||
}
|
||||
|
||||
request = self.factory.post(url, data=post_data)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||
|
||||
# Because user doesn't allow app, SHOULD exists an error parameter
|
||||
# in the query.
|
||||
|
@ -185,13 +183,9 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
|||
msg='"access_denied" code is missing in query.')
|
||||
|
||||
# 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)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||
|
||||
is_code_ok = is_code_valid(url=response['Location'],
|
||||
user=self.user,
|
||||
|
@ -210,7 +204,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
|||
list of scopes) and because they might be prompted for the same
|
||||
authorization multiple times, the server skip it.
|
||||
"""
|
||||
post_data = {
|
||||
data = {
|
||||
'client_id': self.client.client_id,
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'response_type': 'code',
|
||||
|
@ -220,34 +214,25 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
|||
}
|
||||
|
||||
request = self.factory.post(reverse('oidc_provider:authorize'),
|
||||
data=post_data)
|
||||
data=data)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
|
||||
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,
|
||||
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'],
|
||||
user=self.user,
|
||||
client=self.client)
|
||||
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
|
||||
|
||||
del post_data['allow']
|
||||
query_str = urlencode(post_data).replace('+', '%20')
|
||||
|
||||
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)
|
||||
del data['allow']
|
||||
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||
|
||||
is_code_ok = is_code_valid(url=response['Location'],
|
||||
user=self.user,
|
||||
|
@ -255,10 +240,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
|||
self.assertEqual(is_code_ok, True, msg='Code returned is invalid or missing.')
|
||||
|
||||
def test_response_uri_is_properly_constructed(self):
|
||||
"""
|
||||
TODO
|
||||
"""
|
||||
post_data = {
|
||||
data = {
|
||||
'client_id': self.client.client_id,
|
||||
'redirect_uri': self.client.default_redirect_uri + "?redirect_state=xyz",
|
||||
'response_type': 'code',
|
||||
|
@ -267,100 +249,85 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
|||
'allow': 'Accept',
|
||||
}
|
||||
|
||||
request = self.factory.post(reverse('oidc_provider:authorize'),
|
||||
data=post_data)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
# TODO
|
||||
|
||||
is_code_ok = is_code_valid(url=response['Location'],
|
||||
user=self.user,
|
||||
client=self.client)
|
||||
self.assertEqual(is_code_ok, True,
|
||||
msg='Code returned is invalid.')
|
||||
|
||||
def test_scope_with_plus(self):
|
||||
def test_public_client_auto_approval(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'
|
||||
|
||||
query_str = urlencode({
|
||||
'client_id': self.client.client_id,
|
||||
data = {
|
||||
'client_id': self.client_public.client_id,
|
||||
'response_type': 'code',
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'scope': scope_test,
|
||||
'redirect_uri': self.client_public.default_redirect_uri,
|
||||
'scope': 'openid email',
|
||||
'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)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
self.assertEqual('Request for Permission' in response.content.decode('utf-8'), True)
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
|
||||
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):
|
||||
def test_implicit_missing_nonce(self):
|
||||
"""
|
||||
The `nonce` parameter is REQUIRED if you use the Implicit Flow.
|
||||
"""
|
||||
query_str = urlencode({
|
||||
'client_id': self.client.client_id,
|
||||
'response_type': self.client.response_type,
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
data = {
|
||||
'client_id': self.client_implicit.client_id,
|
||||
'response_type': self.client_implicit.response_type,
|
||||
'redirect_uri': self.client_implicit.default_redirect_uri,
|
||||
'scope': 'openid email',
|
||||
'state': self.state,
|
||||
}).replace('+', '%20')
|
||||
}
|
||||
|
||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
||||
|
||||
request = self.factory.get(url)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
response = self._auth_request('get', data, is_user_authenticated=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
|
||||
separate requests for authorization and for an access token, the client
|
||||
receives the access token as the result of the authorization request.
|
||||
"""
|
||||
post_data = {
|
||||
'client_id': self.client.client_id,
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'response_type': self.client.response_type,
|
||||
data = {
|
||||
'client_id': self.client_implicit.client_id,
|
||||
'redirect_uri': self.client_implicit.default_redirect_uri,
|
||||
'response_type': self.client_implicit.response_type,
|
||||
'scope': 'openid email',
|
||||
'state': self.state,
|
||||
'nonce': self.nonce,
|
||||
'allow': 'Accept',
|
||||
}
|
||||
|
||||
request = self.factory.post(reverse('oidc_provider:authorize'),
|
||||
data=post_data)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
||||
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||
|
||||
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:
|
||||
from urllib import urlencode
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import RequestFactory, override_settings
|
||||
from django.test import TestCase
|
||||
from jwkest.jwk import KEYS
|
||||
|
@ -23,10 +24,10 @@ class TokenTestCase(TestCase):
|
|||
"""
|
||||
|
||||
def setUp(self):
|
||||
call_command('creatersakey')
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_fake_user()
|
||||
self.client = create_fake_client(response_type='code')
|
||||
create_rsakey()
|
||||
|
||||
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_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 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):
|
||||
"""
|
||||
Test cases for common utils.
|
||||
"""
|
||||
def test_get_issuer(self):
|
||||
issuer = get_issuer()
|
||||
self.assertEqual(issuer, settings.SITE_URL + '/openid')
|
||||
request = Request()
|
||||
|
||||
# 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 django.contrib.auth.views import redirect_to_login, logout
|
||||
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.token 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.models import Client, RSAKey
|
||||
from oidc_provider.models import RESPONSE_TYPE_CHOICES, RSAKey
|
||||
from oidc_provider import settings
|
||||
|
||||
|
||||
|
@ -40,20 +38,31 @@ class AuthorizeView(View):
|
|||
if 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())
|
||||
|
||||
if settings.get('OIDC_SKIP_CONSENT_ENABLE'):
|
||||
# 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())
|
||||
|
||||
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.
|
||||
context = {
|
||||
'params': authorize.params,
|
||||
}
|
||||
hidden_inputs = render_to_string(
|
||||
'oidc_provider/hidden_inputs.html', context)
|
||||
hidden_inputs = render_to_string('oidc_provider/hidden_inputs.html', context)
|
||||
|
||||
# Remove `openid` from scope list
|
||||
# since we don't need to print it.
|
||||
|
@ -64,12 +73,15 @@ class AuthorizeView(View):
|
|||
'client': authorize.client,
|
||||
'hidden_inputs': hidden_inputs,
|
||||
'params': authorize.params,
|
||||
'scopes': authorize.get_scopes_information(),
|
||||
}
|
||||
|
||||
return render(request, 'oidc_provider/authorize.html', context)
|
||||
else:
|
||||
path = request.get_full_path()
|
||||
return redirect_to_login(path)
|
||||
if authorize.params.prompt == 'none':
|
||||
raise AuthorizeError(authorize.params.redirect_uri, 'login_required', authorize.grant_type)
|
||||
|
||||
return redirect_to_login(request.get_full_path())
|
||||
|
||||
except (ClientIdError, RedirectUriError) as error:
|
||||
context = {
|
||||
|
@ -87,15 +99,12 @@ class AuthorizeView(View):
|
|||
return redirect(uri)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
authorize = AuthorizeEndpoint(request)
|
||||
|
||||
allow = True if request.POST.get('allow') else False
|
||||
|
||||
try:
|
||||
authorize.validate_params()
|
||||
|
||||
if not allow:
|
||||
|
||||
if not request.POST.get('allow'):
|
||||
raise AuthorizeError(authorize.params.redirect_uri,
|
||||
'access_denied',
|
||||
authorize.grant_type)
|
||||
|
@ -148,17 +157,16 @@ def userinfo(request, *args, **kwargs):
|
|||
}
|
||||
|
||||
standard_claims = StandardScopeClaims(token.user, token.scope)
|
||||
|
||||
dic.update(standard_claims.create_response_dic())
|
||||
|
||||
extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)(
|
||||
token.user, token.scope)
|
||||
|
||||
dic.update(extra_claims.create_response_dic())
|
||||
if settings.get('OIDC_EXTRA_SCOPE_CLAIMS'):
|
||||
extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)(token.user, token.scope)
|
||||
dic.update(extra_claims.create_response_dic())
|
||||
|
||||
response = JsonResponse(dic, status=200)
|
||||
response['Cache-Control'] = 'no-store'
|
||||
response['Pragma'] = 'no-cache'
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
@ -167,21 +175,20 @@ class ProviderInfoView(View):
|
|||
def get(self, request, *args, **kwargs):
|
||||
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')
|
||||
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]
|
||||
types_supported = [x[0] for x in RESPONSE_TYPE_CHOICES]
|
||||
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
|
||||
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(
|
||||
name='django-oidc-provider',
|
||||
version='0.3.1',
|
||||
version='0.3.6',
|
||||
packages=[
|
||||
'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints',
|
||||
'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/tests/app',
|
||||
|
|
5
tox.ini
5
tox.ini
|
@ -1,7 +1,7 @@
|
|||
[tox]
|
||||
|
||||
envlist=
|
||||
clean,py{27,34}-django{17,18,19},stats
|
||||
clean,py{27,34}-django{17,18,19},py35-django{18,19},stats
|
||||
|
||||
[testenv]
|
||||
|
||||
|
@ -13,8 +13,9 @@ deps =
|
|||
mock
|
||||
|
||||
commands =
|
||||
pip uninstall --yes django-oidc-provider
|
||||
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]
|
||||
|
||||
|
|
Loading…
Reference in a new issue