diff --git a/.travis.yml b/.travis.yml index 56c5e66..d947e99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 . diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a2b927..5bfc26b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,44 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +### [0.3.6] - 2016-07-07 + +##### Changed +- OIDC_USERINFO setting. + +### [0.3.5] - 2016-06-21 + +##### Added +- Field date_given in UserConsent model. +- Verbose names to all model fields. +- Customize scopes names and descriptions on authorize template. + +##### Changed +- OIDC_EXTRA_SCOPE_CLAIMS setting. + +### [0.3.4] - 2016-06-10 + +##### Changed +- Make SITE_URL setting optional. + +##### Fixed +- Missing migration. + +### [0.3.3] - 2016-05-03 + +##### Fixed +- Important bug with PKCE and form submit in Auth Request. + +### [0.3.2] - 2016-04-26 + ##### Added - Choose type of client on creation. +- Implement Proof Key for Code Exchange by OAuth Public Clients. +- Support for prompt parameter. +- Support for different client JWT tokens algorithm. + +##### Fixed +- Not auto-approve requests for non-confidential clients (publics). ### [0.3.1] - 2016-03-09 diff --git a/README.md b/README.md index 80ec4b8..676d50c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/conf.py b/docs/conf.py index cf7b163..bb4f14f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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. diff --git a/docs/images/client_creation.png b/docs/images/client_creation.png new file mode 100644 index 0000000..fac2105 Binary files /dev/null and b/docs/images/client_creation.png differ diff --git a/docs/index.rst b/docs/index.rst index 466355f..43974a5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 `_ +* `Proof Key for Code Exchange by OAuth Public Clients `_ + -------------------------------------------------------------------------------- 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` - diff --git a/docs/sections/claims.rst b/docs/sections/claims.rst index 96e82c8..78160f7 100644 --- a/docs/sections/claims.rst +++ b/docs/sections/claims.rst @@ -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. diff --git a/docs/sections/clients.rst b/docs/sections/clients.rst deleted file mode 100644 index e3f5ab8..0000000 --- a/docs/sections/clients.rst +++ /dev/null @@ -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 `_ - -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() diff --git a/docs/sections/contribute.rst b/docs/sections/contribute.rst index 9839cca..2811551 100644 --- a/docs/sections/contribute.rst +++ b/docs/sections/contribute.rst @@ -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 `_ 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 `_ for this. + +Improve Documentation +===================== + +We use `Sphinx `_ 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. diff --git a/docs/sections/installation.rst b/docs/sections/installation.rst index 61528e3..3c4f811 100644 --- a/docs/sections/installation.rst +++ b/docs/sections/installation.rst @@ -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/' diff --git a/docs/sections/relyingparties.rst b/docs/sections/relyingparties.rst new file mode 100644 index 0000000..f9ed7d3 --- /dev/null +++ b/docs/sections/relyingparties.rst @@ -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 `_ diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index 23f295b..e51194e 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -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 `_ 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. diff --git a/docs/sections/userconsent.rst b/docs/sections/userconsent.rst new file mode 100644 index 0000000..bc74025 --- /dev/null +++ b/docs/sections/userconsent.rst @@ -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 `_. + + >>> from oidc_provider.models import UserConsent + >>> UserConsent.objects.filter(user__email='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. diff --git a/example_project/.gitignore b/example_project/.gitignore index 6ddd3e2..9dd04fe 100644 --- a/example_project/.gitignore +++ b/example_project/.gitignore @@ -1,3 +1,3 @@ *.sqlite3 *.pem - +static/ diff --git a/example_project/README.md b/example_project/README.md index cad83d3..ab1fd6c 100644 --- a/example_project/README.md +++ b/example_project/README.md @@ -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. diff --git a/example_project/provider_app/static/css/custom.css b/example_project/provider_app/static/css/custom.css index e522417..0f6eaa3 100644 --- a/example_project/provider_app/static/css/custom.css +++ b/example_project/provider_app/static/css/custom.css @@ -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; } \ No newline at end of file diff --git a/example_project/provider_app/templates/base.html b/example_project/provider_app/templates/base.html index d0c1cf1..bc5903d 100644 --- a/example_project/provider_app/templates/base.html +++ b/example_project/provider_app/templates/base.html @@ -6,42 +6,46 @@ - - + - OIDC Provider Example + OpenID Provider Example - + + - - {% block content %}{% endblock %} - -
-
-
- View the project on Github. Example by Juan Ignacio Fiorentino. + +
- - +
+ {% block content %}{% endblock %} +
+ + + + + \ No newline at end of file diff --git a/example_project/provider_app/templates/home.html b/example_project/provider_app/templates/home.html index 7ede9e4..ec76385 100644 --- a/example_project/provider_app/templates/home.html +++ b/example_project/provider_app/templates/home.html @@ -4,46 +4,21 @@ {% block content %} -
-
-
- -

Congratulations! It works.
... what's next?

-
-
-
-
-
-
-

Now that you are an OpenID Connect Provider, start by creating your clients here.

-

Also check that you've created at least one server key, do it here.

-

Server Endpoints

-
-
- {% url 'oidc_provider:provider_info' %} -
The configuration information of the provider. Read more.
-
-
- {% url 'oidc_provider:jwks' %} -
JavaScript Object Notation (JSON) data structure that represents a cryptographic key.
-
-
- {% url 'oidc_provider:authorize' %} -
This endpoint performs Authentication of the End-User. Read more.
-
-
- {% url 'oidc_provider:token' %} -
Used to obtain an Access Token, an ID Token, and optionally a Refresh Token. Read more.
-
-
- {% url 'oidc_provider:userinfo' %} -
OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User. Read more.
-
-
- {% url 'oidc_provider:logout' %} -
Used to notify the OP that the End-User has logged out of the site. Read more.
-
-
+
+
+
+
+

Example of an OpenID Connect 1.0 Provider. Built with the Django Framework and django-oidc-provider package.

+

Start by creating your clients here.

+

Also check that you've created at least one server key, do it here.

+
diff --git a/example_project/provider_app/templates/login.html b/example_project/provider_app/templates/login.html index 8f3b7a9..e9708ca 100644 --- a/example_project/provider_app/templates/login.html +++ b/example_project/provider_app/templates/login.html @@ -1,28 +1,38 @@ {% extends 'base.html' %} +{% load i18n %} + {% block content %} -
-
-
- {% if form.errors %} -
-

Your username and password didn't match. Please try again.

+
+
+
+
+
+ {% if form.errors %} +
Your username and password didn't match. Please try again.
+ {% endif %} +
+ {% csrf_token %} + +
+
+ account_circle + + +
+
+
+
+ lock + + +
+
+ +
+
- {% endif %} -
- {% csrf_token %} - -
- - -
-
- - -
- -
diff --git a/example_project/provider_app/templates/oidc_provider/authorize.html b/example_project/provider_app/templates/oidc_provider/authorize.html index 6df2ed5..3e2a8b3 100644 --- a/example_project/provider_app/templates/oidc_provider/authorize.html +++ b/example_project/provider_app/templates/oidc_provider/authorize.html @@ -2,25 +2,24 @@ {% block content %} -
-
-
-

Request for Permission

-

Client {{ client.name }} would like to access this information of you.

-
- {% csrf_token %} - {{ hidden_inputs }} -
- {% for scope in params.scope %} -
{{ scope | capfirst }}
- {% endfor %} -
-
- -
- -
-
+
+
+

Request for Permission

+
+
+

Client {{ client.name }} would like to access this information of you.

+
+ {% csrf_token %} + {{ hidden_inputs }} +
    + {% for scope in params.scope %} +
  • {{ scope | capfirst }}
  • + {% endfor %} +
+ + +
+
diff --git a/example_project/provider_app/templates/oidc_provider/error.html b/example_project/provider_app/templates/oidc_provider/error.html index 1dfc227..31a221c 100644 --- a/example_project/provider_app/templates/oidc_provider/error.html +++ b/example_project/provider_app/templates/oidc_provider/error.html @@ -2,12 +2,11 @@ {% block content %} -
-
-
- -
-
{{ error }}
+
+
+
+
+

{{ error }}

{{ description }}

diff --git a/example_project/provider_app/urls.py b/example_project/provider_app/urls.py index 1757fa8..12e6abf 100644 --- a/example_project/provider_app/urls.py +++ b/example_project/provider_app/urls.py @@ -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)), ] diff --git a/oidc_provider/lib/claims.py b/oidc_provider/lib/claims.py index d71a95d..4330ee3 100644 --- a/oidc_provider/lib/claims.py +++ b/oidc_provider/lib/claims.py @@ -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'), } } diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 56972a4..d2d1951 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -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 diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index a981eee..200cb2c 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -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 diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py index 9d37f4f..6b625e3 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -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): diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index e512326..fc0880d 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -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 diff --git a/oidc_provider/locale/es/LC_MESSAGES/django.mo b/oidc_provider/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 0000000..4ddd099 Binary files /dev/null and b/oidc_provider/locale/es/LC_MESSAGES/django.mo differ diff --git a/oidc_provider/locale/es/LC_MESSAGES/django.po b/oidc_provider/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000..c9b0257 --- /dev/null +++ b/oidc_provider/locale/es/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \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 "" +"Confidential clients are capable of maintaining the confidentiality " +"of their credentials. Public 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 "" diff --git a/oidc_provider/locale/fr/LC_MESSAGES/django.mo b/oidc_provider/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000..1d3f2e8 Binary files /dev/null and b/oidc_provider/locale/fr/LC_MESSAGES/django.mo differ diff --git a/oidc_provider/locale/fr/LC_MESSAGES/django.po b/oidc_provider/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..2b940bb --- /dev/null +++ b/oidc_provider/locale/fr/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \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 "" +"Confidential clients are capable of maintaining the confidentiality " +"of their credentials. Public clients are incapable." +msgstr "" +"Confidentiel les clients sont capable de maintenir la confidentialité " +" des paramètres de connexion. Public 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" diff --git a/oidc_provider/migrations/0013_auto_20160407_1912.py b/oidc_provider/migrations/0013_auto_20160407_1912.py new file mode 100644 index 0000000..19cb444 --- /dev/null +++ b/oidc_provider/migrations/0013_auto_20160407_1912.py @@ -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), + ), + ] diff --git a/oidc_provider/migrations/0014_client_jwt_alg.py b/oidc_provider/migrations/0014_client_jwt_alg.py new file mode 100644 index 0000000..d2b096c --- /dev/null +++ b/oidc_provider/migrations/0014_client_jwt_alg.py @@ -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'), + ), + ] diff --git a/oidc_provider/migrations/0015_change_client_code.py b/oidc_provider/migrations/0015_change_client_code.py new file mode 100644 index 0000000..bfffd57 --- /dev/null +++ b/oidc_provider/migrations/0015_change_client_code.py @@ -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='Confidential clients are capable of maintaining the confidentiality of their credentials. Public 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=''), + ), + ] diff --git a/oidc_provider/migrations/0016_userconsent_and_verbosenames.py b/oidc_provider/migrations/0016_userconsent_and_verbosenames.py new file mode 100644 index 0000000..afd043e --- /dev/null +++ b/oidc_provider/migrations/0016_userconsent_and_verbosenames.py @@ -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='Confidential clients are capable of maintaining the confidentiality of their credentials. Public 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'), + ), + ] diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 8d9ad39..09b36a2 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -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'Confidential clients are capable of maintaining the confidentiality of their credentials. Public 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'Confidential clients are capable of maintaining the confidentiality of their credentials. Public 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') diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index b7890d1..8e16aab 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -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 diff --git a/oidc_provider/templates/oidc_provider/authorize.html b/oidc_provider/templates/oidc_provider/authorize.html index 95591b4..8d5e08d 100644 --- a/oidc_provider/templates/oidc_provider/authorize.html +++ b/oidc_provider/templates/oidc_provider/authorize.html @@ -3,15 +3,15 @@

Client {{ client.name }} would like to access this information of you ...

- + {% csrf_token %} {{ hidden_inputs }}
    - {% for scope in params.scope %} -
  • {{ scope | capfirst }}
  • - {% endfor %} + {% for scope in scopes %} +
  • {{ scope.name }}
    {{ scope.description }}
  • + {% endfor %}
diff --git a/oidc_provider/templates/oidc_provider/hidden_inputs.html b/oidc_provider/templates/oidc_provider/hidden_inputs.html index 59c7035..596414e 100644 --- a/oidc_provider/templates/oidc_provider/hidden_inputs.html +++ b/oidc_provider/templates/oidc_provider/hidden_inputs.html @@ -3,4 +3,6 @@ - \ No newline at end of file +{% if params.nonce %}{% endif %} +{% if params.code_challenge %}{% endif %} +{% if params.code_challenge_method %}{% endif %} diff --git a/oidc_provider/tests/app/RSAKEY.pem b/oidc_provider/tests/app/RSAKEY.pem deleted file mode 100644 index bcad3a0..0000000 --- a/oidc_provider/tests/app/RSAKEY.pem +++ /dev/null @@ -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----- \ No newline at end of file diff --git a/oidc_provider/tests/app/settings.py b/oidc_provider/tests/app/settings.py index ef5f92b..113f43c 100644 --- a/oidc_provider/tests/app/settings.py +++ b/oidc_provider/tests/app/settings.py @@ -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' diff --git a/oidc_provider/tests/app/utils.py b/oidc_provider/tests/app/utils.py index bd3989d..616b130 100644 --- a/oidc_provider/tests/app/utils.py +++ b/oidc_provider/tests/app/utils.py @@ -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): diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index fc92fcc..dd22800 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -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) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index b17408d..0873b23 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -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')) diff --git a/oidc_provider/tests/test_utils.py b/oidc_provider/tests/test_utils.py index 0357014..32bdf8d 100644 --- a/oidc_provider/tests/test_utils.py +++ b/oidc_provider/tests/test_utils.py @@ -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') diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 01f5d1b..a22ca17 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -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'] diff --git a/setup.py b/setup.py index cb1a60e..6fd3783 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='django-oidc-provider', - version='0.3.1', + version='0.3.6', packages=[ 'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints', 'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/tests/app', diff --git a/tox.ini b/tox.ini index 92540ff..48d8589 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist= - clean,py{27,34}-django{17,18,19},stats + clean,py{27,34}-django{17,18,19},py35-django{18,19},stats [testenv] @@ -13,8 +13,9 @@ deps = mock commands = + pip uninstall --yes django-oidc-provider pip install -e . - coverage run --omit=.tox/*,oidc_provider/tests/* {envbindir}/django-admin.py test oidc_provider --settings=oidc_provider.tests.app.settings + coverage run --omit=.tox/*,oidc_provider/tests/* {envbindir}/django-admin.py test {posargs:oidc_provider} --settings=oidc_provider.tests.app.settings [testenv:clean]