Merge pull request #5 from juanifioren/v0.3.x

updating
This commit is contained in:
Wojciech Bartosiak 2016-07-29 22:54:37 +02:00 committed by GitHub
commit c70dab13a8
48 changed files with 1472 additions and 597 deletions

View file

@ -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 .

View file

@ -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

View file

@ -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.

View file

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -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`

View file

@ -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.

View file

@ -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()

View file

@ -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.

View file

@ -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/'

View 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>`_

View file

@ -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.

View 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.

View file

@ -1,3 +1,3 @@
*.sqlite3
*.pem
static/

View file

@ -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.

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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)),
]

View file

@ -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'),
}
}

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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

Binary file not shown.

View 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 ""

Binary file not shown.

View 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"

View 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),
),
]

View 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'),
),
]

View 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=''),
),
]

View 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'),
),
]

View file

@ -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')

View file

@ -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

View file

@ -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" />

View file

@ -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 %}

View file

@ -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-----

View file

@ -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'

View file

@ -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):

View file

@ -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)

View file

@ -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'))

View file

@ -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')

View file

@ -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']

View file

@ -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',

View file

@ -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]