Merge branch 'v0.3.x' of https://github.com/juanifioren/django-oidc-provider into feature-jwtalg
This commit is contained in:
commit
9f9df355a3
|
@ -2,10 +2,15 @@ language: python
|
||||||
python:
|
python:
|
||||||
- "2.7"
|
- "2.7"
|
||||||
- "3.4"
|
- "3.4"
|
||||||
|
- "3.5"
|
||||||
env:
|
env:
|
||||||
- DJANGO=1.7
|
- DJANGO=1.7
|
||||||
- DJANGO=1.8
|
- DJANGO=1.8
|
||||||
- DJANGO=1.9
|
- DJANGO=1.9
|
||||||
|
matrix:
|
||||||
|
exclude:
|
||||||
|
- python: "3.5"
|
||||||
|
env: DJANGO=1.7
|
||||||
install:
|
install:
|
||||||
- pip install -q django==$DJANGO
|
- pip install -q django==$DJANGO
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
|
|
|
@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### [Unreleased]
|
### [Unreleased]
|
||||||
|
|
||||||
|
##### Added
|
||||||
|
- Choose type of client on creation.
|
||||||
|
- Implement Proof Key for Code Exchange by OAuth Public Clients.
|
||||||
|
- Support for prompt parameter.
|
||||||
|
|
||||||
|
##### Fixed
|
||||||
|
- Not auto-approve requests for non-confidential clients (publics).
|
||||||
|
|
||||||
### [0.3.1] - 2016-03-09
|
### [0.3.1] - 2016-03-09
|
||||||
|
|
||||||
##### Fixed
|
##### Fixed
|
||||||
|
|
|
@ -53,9 +53,9 @@ author = u'Juan Ignacio Fiorentino'
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = u'0.2'
|
version = u'0.3'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = u'0.2.5'
|
release = u'0.3.x'
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
|
BIN
docs/images/client_creation.png
Normal file
BIN
docs/images/client_creation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
|
@ -3,6 +3,11 @@ Welcome to Django OIDC Provider Documentation!
|
||||||
|
|
||||||
Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. And as a side effect a fair implementation of OAuth2.0 too.
|
Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. And as a side effect a fair implementation of OAuth2.0 too.
|
||||||
|
|
||||||
|
Also implements the following specifications:
|
||||||
|
|
||||||
|
* `OAuth 2.0 for Native Apps <https://tools.ietf.org/html/draft-ietf-oauth-native-apps-01>`_
|
||||||
|
* `Proof Key for Code Exchange by OAuth Public Clients <https://tools.ietf.org/html/rfc7636>`_
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
Before getting started there are some important things that you should know:
|
Before getting started there are some important things that you should know:
|
||||||
|
@ -19,7 +24,7 @@ Contents:
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
sections/installation
|
sections/installation
|
||||||
sections/clients
|
sections/relyingparties
|
||||||
sections/serverkeys
|
sections/serverkeys
|
||||||
sections/templates
|
sections/templates
|
||||||
sections/claims
|
sections/claims
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
.. _clients:
|
|
||||||
|
|
||||||
Clients
|
|
||||||
#######
|
|
||||||
|
|
||||||
Also known as Relying Parties (RP). User and client creation it's up to you. This is because is out of the scope in the core implementation of OIDC.
|
|
||||||
So, there are different ways to create your Clients. By displaying a HTML form or maybe if you have internal thrusted Clients you can create them programatically.
|
|
||||||
|
|
||||||
`Read more about client creation from OAuth2 spec <http://tools.ietf.org/html/rfc6749#section-2>`_
|
|
||||||
|
|
||||||
For your users, the tipical situation is that you provide them a login and a registration page.
|
|
||||||
|
|
||||||
If you want to test the provider without getting to deep into this topics you can:
|
|
||||||
|
|
||||||
Create a user with ``python manage.py createsuperuser`` and clients using Django admin:
|
|
||||||
|
|
||||||
.. image:: http://i64.tinypic.com/2dsfgoy.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
Or also you can create a client programmatically with Django shell ``python manage.py shell``::
|
|
||||||
|
|
||||||
>>> from oidc_provider.models import Client
|
|
||||||
>>> c = Client(name='Some Client', client_id='123', client_secret='456', response_type='code', redirect_uris=['http://example.com/'])
|
|
||||||
>>> c.save()
|
|
43
docs/sections/relyingparties.rst
Normal file
43
docs/sections/relyingparties.rst
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
.. _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.
|
||||||
|
|
||||||
|
Using the admin
|
||||||
|
===============
|
||||||
|
|
||||||
|
We suggest you to use Django admin to easily manage your clients:
|
||||||
|
|
||||||
|
.. image:: ../images/client_creation.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
For re-generating ``client_secret``, when you are in the Client editing view, select "Client type" to be ``public``. Then after saving, select back to be ``confidential`` and save again.
|
||||||
|
|
||||||
|
Custom view
|
||||||
|
===========
|
||||||
|
|
||||||
|
If for some reason you need to create your own view to manage them, you can grab the form class that the admin makes use of. Located in ``oidc_provider.admin.ClientForm``.
|
||||||
|
|
||||||
|
Some built-in logic that comes with it:
|
||||||
|
|
||||||
|
* Automatic ``client_id`` and ``client_secret`` generation.
|
||||||
|
* Empty ``client_secret`` when ``client_type`` is equal to ``public``.
|
||||||
|
|
||||||
|
Programmatically
|
||||||
|
================
|
||||||
|
|
||||||
|
You can create a Client programmatically with Django shell ``python manage.py shell``::
|
||||||
|
|
||||||
|
>>> from oidc_provider.models import Client
|
||||||
|
>>> c = Client(name='Some Client', client_id='123', client_secret='456', response_type='code', redirect_uris=['http://example.com/'])
|
||||||
|
>>> c.save()
|
||||||
|
|
||||||
|
`Read more about client creation from OAuth2 spec <http://tools.ietf.org/html/rfc6749#section-2>`_
|
2
example_project/.gitignore
vendored
2
example_project/.gitignore
vendored
|
@ -1,3 +1,3 @@
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.pem
|
*.pem
|
||||||
|
static/
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Example Project
|
# Example Project
|
||||||
|
|
||||||
![Example Project](http://s12.postimg.org/e4uwlsi0d/Screenshot_from_2016_02_02_13_15_26.png)
|
![Example Project](http://i.imgur.com/IK3OZjx.png)
|
||||||
|
|
||||||
Run your own OIDC provider in a second. This is a Django app with all the necessary things to work with `django-oidc-provider` package.
|
Run your own OIDC provider in a second. This is a Django app with all the necessary things to work with `django-oidc-provider` package.
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,800);
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Open Sans', sans-serif;
|
background-color: #536dfe;
|
||||||
font-weight: 400;
|
display: flex;
|
||||||
height: auto;
|
min-height: 100vh;
|
||||||
padding-top: 10px;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.huge.header {
|
#main-container {
|
||||||
font-size: 46px;
|
flex: 1 0 auto;
|
||||||
font-weight: 800;
|
padding-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui.segment {
|
footer {
|
||||||
font-size: 18px;
|
padding-top: 0px !important;
|
||||||
font-weight: 300;
|
|
||||||
}
|
}
|
|
@ -6,42 +6,46 @@
|
||||||
<head>
|
<head>
|
||||||
|
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
||||||
|
|
||||||
<title>OIDC Provider Example</title>
|
<title>OpenID Provider Example</title>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/semantic.min.css">
|
<link rel="stylesheet" href="http://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/css/materialize.min.css">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/custom.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'css/custom.css' %}">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<div class="ui page grid fixed large blue inverted menu">
|
|
||||||
<a href="{% url 'home' %}" class=" item">django-oidc-provider</a>
|
<div class="navbar-fixed">
|
||||||
<div class="right menu">
|
<nav class="white">
|
||||||
|
<div class="nav-wrapper">
|
||||||
|
<a href="{% url 'home' %}" class="brand-logo center black-text">OpenID Provider</a>
|
||||||
|
<ul id="nav-mobile" class="right hide-on-med-and-down">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a href="#" class="item">{{ user.email }}</a>
|
|
||||||
{% if user.is_superuser %}
|
{% if user.is_superuser %}
|
||||||
<a href="{% url 'admin:index' %}" class="item">Admin</a>
|
<li><a href="{% url 'admin:index' %}" class="black-text">{% trans 'Admin' %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{% url 'logout' %}" class="item"><i class="remove icon"></i></a>
|
<li><a href="{% url 'logout' %}" class="black-text">X</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'login' %}" class="item">Login</a>
|
<li><a href="{% url 'login' %}" class="black-text"><i class="material-icons left">input</i> {% trans 'Login' %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="main-container" class="container">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
<div class="ui stackable page grid">
|
|
||||||
<div class="column">
|
|
||||||
<div class="ui right aligned secondary segment">
|
|
||||||
View the project <a href="https://github.com/juanifioren/django-oidc-provider" target="_BLANK">on Github</a>. Example by <a href="https://github.com/juanifioren" target="_BLANK">Juan Ignacio Fiorentino</a>.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
<footer class="page-footer white">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.1.6/semantic.min.js"></script>
|
<div class="footer-copyright">
|
||||||
|
<div class="container black-text"><a href="https://www.linkedin.com/in/juanifioren" target="_BLANK">Created by Juan Ignacio Fiorentino</a><a class="right" href="https://github.com/juanifioren/django-oidc-provider" target="_BLANK">View on Github</a></div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/js/materialize.min.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -4,46 +4,21 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="ui stackable page grid">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="center aligned column">
|
<div class="col s12 m10 offset-m1">
|
||||||
<i class="thumbs outline up massive icon"></i>
|
<div class="card hoverable">
|
||||||
<h1 class="ui huge header">Congratulations! It works. <div class="sub header">... what's next?</div></h1>
|
<div class="card-content">
|
||||||
</div>
|
<h1 class="center-align flow-text indigo-text text-lighten-1">Example of an OpenID Connect 1.0 Provider. Built with the <a href="https://www.djangoproject.com/" target="_BLANK"><u>Django Framework</u></a> and <a href="https://github.com/juanifioren/django-oidc-provider" target="_BLANK"><u>django-oidc-provider</u></a> package.</h1>
|
||||||
</div>
|
<p class="flow-text">Start by creating your clients <a href="{% url 'admin:index' %}oidc_provider/client/">here</a>.</p>
|
||||||
<div class="row">
|
<p class="flow-text">Also check that you've created at least one server key, do it <a href="{% url 'admin:index' %}oidc_provider/rsakey/">here</a>.</p>
|
||||||
<div class="column">
|
<div class="collection with-header">
|
||||||
<div class="ui segments">
|
<div class="collection-header"><h4>Server Endpoints</h4></div>
|
||||||
<div class="ui segment">
|
<a href="{% url 'oidc_provider:provider_info' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:provider_info' %}</a>
|
||||||
<p>Now that you are an OpenID Connect Provider, start by creating your clients <a href="{% url 'admin:index' %}oidc_provider/client/" target="_BLANK">here</a>.</p>
|
<a href="{% url 'oidc_provider:jwks' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:jwks' %}</a>
|
||||||
<p>Also check that you've created at least one server key, do it <a href="{% url 'admin:index' %}oidc_provider/rsakey/" target="_BLANK">here</a>.</p>
|
<a href="{% url 'oidc_provider:authorize' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:authorize' %}</a>
|
||||||
<h2 class="ui header">Server Endpoints</h2>
|
<a href="{% url 'oidc_provider:token' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:token' %}</a>
|
||||||
<div class="ui list">
|
<a href="{% url 'oidc_provider:userinfo' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:userinfo' %}</a>
|
||||||
<div class="item">
|
<a href="{% url 'oidc_provider:logout' %}" class="collection-item indigo-text text-lighten-1">{% url 'oidc_provider:logout' %}</a>
|
||||||
<a href="{% url 'oidc_provider:provider_info' %}" class="header">{% url 'oidc_provider:provider_info' %}</a>
|
|
||||||
<div class="description">The configuration information of the provider. <a href="http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:jwks' %}" class="header">{% url 'oidc_provider:jwks' %}</a>
|
|
||||||
<div class="description">JavaScript Object Notation (JSON) data structure that represents a cryptographic key.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:authorize' %}" class="header">{% url 'oidc_provider:authorize' %}</a>
|
|
||||||
<div class="description">This endpoint performs Authentication of the End-User. <a href="http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:token' %}" class="header">{% url 'oidc_provider:token' %}</a>
|
|
||||||
<div class="description">Used to obtain an Access Token, an ID Token, and optionally a Refresh Token. <a href="http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:userinfo' %}" class="header">{% url 'oidc_provider:userinfo' %}</a>
|
|
||||||
<div class="description">OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User. <a href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<a href="{% url 'oidc_provider:logout' %}" class="header">{% url 'oidc_provider:logout' %}</a>
|
|
||||||
<div class="description">Used to notify the OP that the End-User has logged out of the site. <a href="http://openid.net/specs/openid-connect-session-1_0.html#RPLogout" target="_BLANK">Read more</a>.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,30 +1,40 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="ui page grid">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="six wide centered column">
|
<div class="col s12 m8 offset-m2 l6 offset-l3">
|
||||||
|
<div class="card hoverable">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="row">
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<div class="ui negative message">
|
<h5 class="red-text text-darken-1 flow-text center-align">Your username and password didn't match. Please try again.</h5>
|
||||||
<p>Your username and password didn't match. Please try again.</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form class="ui form segment" method="post" action="{% url 'login' %}">
|
<form class="col s12" method="post" action="{% url 'login' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="next" value="{{ next }}">
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
<div class="field">
|
<div class="row">
|
||||||
<label>Username</label>
|
<div class="input-field col s12">
|
||||||
<input type="text" name="username">
|
<i class="material-icons prefix">account_circle</i>
|
||||||
|
<input id="username" name="username" type="text" class="validate">
|
||||||
|
<label for="username">{% trans 'Username' %}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label>Password</label>
|
|
||||||
<input type="password" name="password">
|
|
||||||
</div>
|
</div>
|
||||||
<input class="ui submit big primary fluid button" type="submit" value="Enter" />
|
<div class="row">
|
||||||
|
<div class="input-field col s12">
|
||||||
|
<i class="material-icons prefix">lock</i>
|
||||||
|
<input id="password" name="password" type="password" class="validate">
|
||||||
|
<label for="password">{% trans 'Password' %}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input class="waves-effect waves-light btn-large right green" type="submit" value="{% trans 'Enter' %}" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -2,27 +2,26 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="ui page grid">
|
<div class="row">
|
||||||
<div class="nine wide centered column">
|
<div class="col s12 m8 offset-m2 l6 offset-l3">
|
||||||
<div class="ui segment">
|
<h4 class="grey-text text-lighten-3">Request for Permission</h4>
|
||||||
<h1 class="ui dividing header">Request for Permission</h1>
|
<div class="card hoverable">
|
||||||
<p>Client <i>{{ client.name }}</i> would like to access this information of you.</p>
|
<div class="card-content">
|
||||||
|
<p class="flow-text">Client <i>{{ client.name }}</i> would like to access this information of you.</p>
|
||||||
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ hidden_inputs }}
|
{{ hidden_inputs }}
|
||||||
<div class="ui bulleted list">
|
<ul class="collection">
|
||||||
{% for scope in params.scope %}
|
{% for scope in params.scope %}
|
||||||
<div class="item">{{ scope | capfirst }}</div>
|
<li class="collection-item">{{ scope | capfirst }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</ul>
|
||||||
<div class="ui fluid large buttons">
|
<input class="waves-effect waves-light btn grey" type="submit" value="Cancel" />
|
||||||
<input class="ui button" type="submit" value="Cancel" />
|
<input name="allow" class="waves-effect waves-light btn green right" type="submit" value="Authorize" />
|
||||||
<div class="or"></div>
|
|
||||||
<input name="allow" class="positive ui button" type="submit" value="Authorize" />
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -2,12 +2,11 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="ui page grid">
|
<div class="row">
|
||||||
<div class="nine wide centered column">
|
<div class="col s12 m8 offset-m2 l6 offset-l3">
|
||||||
<div class="ui icon negative large message">
|
<div class="card hoverable">
|
||||||
<i class="meh icon"></i>
|
<div class="card-content">
|
||||||
<div class="content">
|
<h4 class="center-align red-text text-lighten-1">{{ error }}</h4>
|
||||||
<div class="header">{{ error }}</div>
|
|
||||||
<p>{{ description }}</p>
|
<p>{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ urlpatterns = [
|
||||||
url(r'^accounts/login/$', auth_views.login, { 'template_name': 'login.html' }, name='login'),
|
url(r'^accounts/login/$', auth_views.login, { 'template_name': 'login.html' }, name='login'),
|
||||||
url(r'^accounts/logout/$', auth_views.logout, { 'next_page': '/' }, name='logout'),
|
url(r'^accounts/logout/$', auth_views.logout, { 'next_page': '/' }, name='logout'),
|
||||||
|
|
||||||
url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
|
url(r'^', include('oidc_provider.urls', namespace='oidc_provider')),
|
||||||
|
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -30,10 +30,19 @@ class ClientForm(ModelForm):
|
||||||
|
|
||||||
def clean_client_secret(self):
|
def clean_client_secret(self):
|
||||||
instance = getattr(self, 'instance', None)
|
instance = getattr(self, 'instance', None)
|
||||||
|
|
||||||
|
secret = ''
|
||||||
|
|
||||||
if instance and instance.pk:
|
if instance and instance.pk:
|
||||||
return instance.client_secret
|
if (self.cleaned_data['client_type'] == 'confidential') and not instance.client_secret:
|
||||||
|
secret = md5(uuid4().hex.encode()).hexdigest()
|
||||||
|
elif (self.cleaned_data['client_type'] == 'confidential') and instance.client_secret:
|
||||||
|
secret = instance.client_secret
|
||||||
else:
|
else:
|
||||||
return md5(uuid4().hex.encode()).hexdigest()
|
if (instance.client_type == 'confidential'):
|
||||||
|
secret = md5(uuid4().hex.encode()).hexdigest()
|
||||||
|
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Client)
|
@admin.register(Client)
|
||||||
|
|
|
@ -55,37 +55,50 @@ class AuthorizeEndpoint(object):
|
||||||
self.params.scope = query_dict.get('scope', '').split()
|
self.params.scope = query_dict.get('scope', '').split()
|
||||||
self.params.state = query_dict.get('state', '')
|
self.params.state = query_dict.get('state', '')
|
||||||
self.params.nonce = query_dict.get('nonce', '')
|
self.params.nonce = query_dict.get('nonce', '')
|
||||||
|
self.params.prompt = query_dict.get('prompt', '')
|
||||||
|
|
||||||
|
# PKCE parameters.
|
||||||
|
self.params.code_challenge = query_dict.get('code_challenge')
|
||||||
|
self.params.code_challenge_method = query_dict.get('code_challenge_method')
|
||||||
|
|
||||||
def validate_params(self):
|
def validate_params(self):
|
||||||
|
# Client validation.
|
||||||
try:
|
try:
|
||||||
self.client = Client.objects.get(client_id=self.params.client_id)
|
self.client = Client.objects.get(client_id=self.params.client_id)
|
||||||
except Client.DoesNotExist:
|
except Client.DoesNotExist:
|
||||||
logger.debug('[Authorize] Invalid client identifier: %s', self.params.client_id)
|
logger.debug('[Authorize] Invalid client identifier: %s', self.params.client_id)
|
||||||
raise ClientIdError()
|
raise ClientIdError()
|
||||||
|
|
||||||
|
# Redirect URI validation.
|
||||||
if self.is_authentication and not self.params.redirect_uri:
|
if self.is_authentication and not self.params.redirect_uri:
|
||||||
logger.debug('[Authorize] Missing redirect uri.')
|
logger.debug('[Authorize] Missing redirect uri.')
|
||||||
raise RedirectUriError()
|
raise RedirectUriError()
|
||||||
|
|
||||||
if not self.grant_type:
|
|
||||||
logger.debug('[Authorize] Invalid response type: %s', self.params.response_type)
|
|
||||||
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type',
|
|
||||||
self.grant_type)
|
|
||||||
|
|
||||||
if self.is_authentication and self.grant_type == 'implicit' and not self.params.nonce:
|
|
||||||
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
|
||||||
self.grant_type)
|
|
||||||
|
|
||||||
if self.is_authentication and self.params.response_type != self.client.response_type:
|
|
||||||
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
|
||||||
self.grant_type)
|
|
||||||
|
|
||||||
clean_redirect_uri = urlsplit(self.params.redirect_uri)
|
clean_redirect_uri = urlsplit(self.params.redirect_uri)
|
||||||
clean_redirect_uri = urlunsplit(clean_redirect_uri._replace(query=''))
|
clean_redirect_uri = urlunsplit(clean_redirect_uri._replace(query=''))
|
||||||
if not (clean_redirect_uri in self.client.redirect_uris):
|
if not (clean_redirect_uri in self.client.redirect_uris):
|
||||||
logger.debug('[Authorize] Invalid redirect uri: %s', self.params.redirect_uri)
|
logger.debug('[Authorize] Invalid redirect uri: %s', self.params.redirect_uri)
|
||||||
raise RedirectUriError()
|
raise RedirectUriError()
|
||||||
|
|
||||||
|
# Grant type validation.
|
||||||
|
if not self.grant_type:
|
||||||
|
logger.debug('[Authorize] Invalid response type: %s', self.params.response_type)
|
||||||
|
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type',
|
||||||
|
self.grant_type)
|
||||||
|
|
||||||
|
# Nonce parameter validation.
|
||||||
|
if self.is_authentication and self.grant_type == 'implicit' and not self.params.nonce:
|
||||||
|
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
||||||
|
self.grant_type)
|
||||||
|
|
||||||
|
# Response type parameter validation.
|
||||||
|
if self.is_authentication and self.params.response_type != self.client.response_type:
|
||||||
|
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
||||||
|
self.grant_type)
|
||||||
|
|
||||||
|
# PKCE validation of the transformation method.
|
||||||
|
if self.params.code_challenge:
|
||||||
|
if not (self.params.code_challenge_method in ['plain', 'S256']):
|
||||||
|
raise AuthorizeError(self.params.redirect_uri, 'invalid_request', self.grant_type)
|
||||||
|
|
||||||
def create_response_uri(self):
|
def create_response_uri(self):
|
||||||
uri = urlsplit(self.params.redirect_uri)
|
uri = urlsplit(self.params.redirect_uri)
|
||||||
|
@ -99,7 +112,9 @@ class AuthorizeEndpoint(object):
|
||||||
client=self.client,
|
client=self.client,
|
||||||
scope=self.params.scope,
|
scope=self.params.scope,
|
||||||
nonce=self.params.nonce,
|
nonce=self.params.nonce,
|
||||||
is_authentication=self.is_authentication)
|
is_authentication=self.is_authentication,
|
||||||
|
code_challenge=self.params.code_challenge,
|
||||||
|
code_challenge_method=self.params.code_challenge_method)
|
||||||
|
|
||||||
code.save()
|
code.save()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from base64 import b64decode
|
from base64 import b64decode, urlsafe_b64decode, urlsafe_b64encode
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
try:
|
try:
|
||||||
|
@ -6,7 +7,9 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
from oidc_provider.lib.errors import *
|
from oidc_provider.lib.errors import *
|
||||||
from oidc_provider.lib.utils.params import *
|
from oidc_provider.lib.utils.params import *
|
||||||
|
@ -30,14 +33,16 @@ class TokenEndpoint(object):
|
||||||
|
|
||||||
self.params.client_id = client_id
|
self.params.client_id = client_id
|
||||||
self.params.client_secret = client_secret
|
self.params.client_secret = client_secret
|
||||||
self.params.redirect_uri = unquote(
|
self.params.redirect_uri = unquote(self.request.POST.get('redirect_uri', ''))
|
||||||
self.request.POST.get('redirect_uri', ''))
|
|
||||||
self.params.grant_type = self.request.POST.get('grant_type', '')
|
self.params.grant_type = self.request.POST.get('grant_type', '')
|
||||||
self.params.code = self.request.POST.get('code', '')
|
self.params.code = self.request.POST.get('code', '')
|
||||||
self.params.state = self.request.POST.get('state', '')
|
self.params.state = self.request.POST.get('state', '')
|
||||||
self.params.scope = self.request.POST.get('scope', '')
|
self.params.scope = self.request.POST.get('scope', '')
|
||||||
self.params.refresh_token = self.request.POST.get('refresh_token', '')
|
self.params.refresh_token = self.request.POST.get('refresh_token', '')
|
||||||
|
|
||||||
|
# PKCE parameters.
|
||||||
|
self.params.code_verifier = self.request.POST.get('code_verifier')
|
||||||
|
|
||||||
def _extract_client_auth(self):
|
def _extract_client_auth(self):
|
||||||
"""
|
"""
|
||||||
Get client credentials using HTTP Basic Authentication method.
|
Get client credentials using HTTP Basic Authentication method.
|
||||||
|
@ -68,6 +73,7 @@ class TokenEndpoint(object):
|
||||||
logger.debug('[Token] Client does not exist: %s', self.params.client_id)
|
logger.debug('[Token] Client does not exist: %s', self.params.client_id)
|
||||||
raise TokenError('invalid_client')
|
raise TokenError('invalid_client')
|
||||||
|
|
||||||
|
if self.client.client_type == 'confidential':
|
||||||
if not (self.client.client_secret == self.params.client_secret):
|
if not (self.client.client_secret == self.params.client_secret):
|
||||||
logger.debug('[Token] Invalid client secret: client %s do not have secret %s',
|
logger.debug('[Token] Invalid client secret: client %s do not have secret %s',
|
||||||
self.client.client_id, self.client.client_secret)
|
self.client.client_id, self.client.client_secret)
|
||||||
|
@ -90,6 +96,19 @@ class TokenEndpoint(object):
|
||||||
self.params.redirect_uri)
|
self.params.redirect_uri)
|
||||||
raise TokenError('invalid_grant')
|
raise TokenError('invalid_grant')
|
||||||
|
|
||||||
|
# Validate PKCE parameters.
|
||||||
|
if self.params.code_verifier:
|
||||||
|
if self.code.code_challenge_method == 'S256':
|
||||||
|
new_code_challenge = urlsafe_b64encode(
|
||||||
|
hashlib.sha256(self.params.code_verifier.encode('ascii')).digest()
|
||||||
|
).decode('utf-8').replace('=', '')
|
||||||
|
else:
|
||||||
|
new_code_challenge = self.params.code_verifier
|
||||||
|
|
||||||
|
# TODO: We should explain the error.
|
||||||
|
if not (new_code_challenge == self.code.code_challenge):
|
||||||
|
raise TokenError('invalid_grant')
|
||||||
|
|
||||||
elif self.params.grant_type == 'refresh_token':
|
elif self.params.grant_type == 'refresh_token':
|
||||||
if not self.params.refresh_token:
|
if not self.params.refresh_token:
|
||||||
logger.debug('[Token] Missing refresh token')
|
logger.debug('[Token] Missing refresh token')
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -101,7 +102,8 @@ def create_token(user, client, id_token_dic, scope):
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
def create_code(user, client, scope, nonce, is_authentication):
|
def create_code(user, client, scope, nonce, is_authentication,
|
||||||
|
code_challenge=None, code_challenge_method=None):
|
||||||
"""
|
"""
|
||||||
Create and populate a Code object.
|
Create and populate a Code object.
|
||||||
|
|
||||||
|
@ -110,7 +112,13 @@ def create_code(user, client, scope, nonce, is_authentication):
|
||||||
code = Code()
|
code = Code()
|
||||||
code.user = user
|
code.user = user
|
||||||
code.client = client
|
code.client = client
|
||||||
|
|
||||||
code.code = uuid.uuid4().hex
|
code.code = uuid.uuid4().hex
|
||||||
|
|
||||||
|
if code_challenge and code_challenge_method:
|
||||||
|
code.code_challenge = code_challenge
|
||||||
|
code.code_challenge_method = code_challenge_method
|
||||||
|
|
||||||
code.expires_at = timezone.now() + timedelta(
|
code.expires_at = timezone.now() + timedelta(
|
||||||
seconds=settings.get('OIDC_CODE_EXPIRE'))
|
seconds=settings.get('OIDC_CODE_EXPIRE'))
|
||||||
code.scope = scope
|
code.scope = scope
|
||||||
|
|
20
oidc_provider/migrations/0011_client_client_type.py
Normal file
20
oidc_provider/migrations/0011_client_client_type.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2016-04-04 19:56
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oidc_provider', '0010_code_is_authentication'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='client',
|
||||||
|
name='client_type',
|
||||||
|
field=models.CharField(choices=[(b'confidential', b'Confidential'), (b'public', b'Public')], default=b'confidential', help_text='<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.', max_length=30),
|
||||||
|
),
|
||||||
|
]
|
20
oidc_provider/migrations/0012_auto_20160405_2041.py
Normal file
20
oidc_provider/migrations/0012_auto_20160405_2041.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2016-04-05 20:41
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oidc_provider', '0011_client_client_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='client',
|
||||||
|
name='client_secret',
|
||||||
|
field=models.CharField(blank=True, default=b'', max_length=255),
|
||||||
|
),
|
||||||
|
]
|
25
oidc_provider/migrations/0013_auto_20160407_1912.py
Normal file
25
oidc_provider/migrations/0013_auto_20160407_1912.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2016-04-07 19:12
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oidc_provider', '0012_auto_20160405_2041'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='code',
|
||||||
|
name='code_challenge',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='code',
|
||||||
|
name='code_challenge_method',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,22 +8,28 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
class Client(models.Model):
|
CLIENT_TYPE_CHOICES = [
|
||||||
|
('confidential', 'Confidential'),
|
||||||
|
('public', 'Public'),
|
||||||
|
]
|
||||||
|
|
||||||
RESPONSE_TYPE_CHOICES = [
|
RESPONSE_TYPE_CHOICES = [
|
||||||
('code', 'code (Authorization Code Flow)'),
|
('code', 'code (Authorization Code Flow)'),
|
||||||
('id_token', 'id_token (Implicit Flow)'),
|
('id_token', 'id_token (Implicit Flow)'),
|
||||||
('id_token token', 'id_token token (Implicit Flow)'),
|
('id_token token', 'id_token token (Implicit Flow)'),
|
||||||
]
|
]
|
||||||
|
|
||||||
JWT_ALGS = [
|
JWT_ALGS = [
|
||||||
('HS256', 'HS256'),
|
('HS256', 'HS256'),
|
||||||
('RS256', 'RS256'),
|
('RS256', 'RS256'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
class Client(models.Model):
|
||||||
|
|
||||||
name = models.CharField(max_length=100, default='')
|
name = models.CharField(max_length=100, default='')
|
||||||
|
client_type = models.CharField(max_length=30, choices=CLIENT_TYPE_CHOICES, default='confidential', help_text=_(u'<b>Confidential</b> clients are capable of maintaining the confidentiality of their credentials. <b>Public</b> clients are incapable.'))
|
||||||
client_id = models.CharField(max_length=255, unique=True)
|
client_id = models.CharField(max_length=255, unique=True)
|
||||||
client_secret = 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)
|
response_type = models.CharField(max_length=30, choices=RESPONSE_TYPE_CHOICES)
|
||||||
jwt_alg = models.CharField(max_length=10, choices=JWT_ALGS, default='RS256', verbose_name=_(u'JWT Algorithm'))
|
jwt_alg = models.CharField(max_length=10, choices=JWT_ALGS, default='RS256', verbose_name=_(u'JWT Algorithm'))
|
||||||
date_created = models.DateField(auto_now_add=True)
|
date_created = models.DateField(auto_now_add=True)
|
||||||
|
@ -86,6 +92,8 @@ class Code(BaseCodeTokenModel):
|
||||||
code = models.CharField(max_length=255, unique=True)
|
code = models.CharField(max_length=255, unique=True)
|
||||||
nonce = models.CharField(max_length=255, blank=True, default='')
|
nonce = models.CharField(max_length=255, blank=True, default='')
|
||||||
is_authentication = models.BooleanField(default=False)
|
is_authentication = models.BooleanField(default=False)
|
||||||
|
code_challenge = models.CharField(max_length=255, null=True)
|
||||||
|
code_challenge_method = models.CharField(max_length=255, null=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _(u'Authorization Code')
|
verbose_name = _(u'Authorization Code')
|
||||||
|
|
|
@ -4,3 +4,5 @@
|
||||||
<input name="scope" type="hidden" value="{{ params.scope | join:' ' }}" />
|
<input name="scope" type="hidden" value="{{ params.scope | join:' ' }}" />
|
||||||
<input name="state" type="hidden" value="{{ params.state }}" />
|
<input name="state" type="hidden" value="{{ params.state }}" />
|
||||||
<input name="nonce" type="hidden" value="{{ params.nonce }}" />
|
<input name="nonce" type="hidden" value="{{ params.nonce }}" />
|
||||||
|
<input name="code_challenge" type="hidden" value="{{ params.code_challenge }}" />
|
||||||
|
<input name="code_challenge_method" type="hidden" value="{{ params.code_challenge_method }}" />
|
||||||
|
|
|
@ -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-----
|
|
|
@ -13,6 +13,8 @@ from oidc_provider.models import *
|
||||||
|
|
||||||
FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d'
|
FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d'
|
||||||
FAKE_RANDOM_STRING = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
FAKE_RANDOM_STRING = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
|
||||||
|
FAKE_CODE_CHALLENGE = 'YlYXEqXuRm-Xgi2BOUiK50JW1KsGTX6F1TDnZSC8VTg'
|
||||||
|
FAKE_CODE_VERIFIER = 'SmxGa0XueyNh5bDgTcSrqzAh2_FmXEqU8kDT6CuXicw'
|
||||||
|
|
||||||
|
|
||||||
def create_fake_user():
|
def create_fake_user():
|
||||||
|
@ -31,7 +33,7 @@ def create_fake_user():
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def create_fake_client(response_type):
|
def create_fake_client(response_type, is_public=False):
|
||||||
"""
|
"""
|
||||||
Create a test client, response_type argument MUST be:
|
Create a test client, response_type argument MUST be:
|
||||||
'code', 'id_token' or 'id_token token'.
|
'code', 'id_token' or 'id_token token'.
|
||||||
|
@ -40,8 +42,12 @@ def create_fake_client(response_type):
|
||||||
"""
|
"""
|
||||||
client = Client()
|
client = Client()
|
||||||
client.name = 'Some Client'
|
client.name = 'Some Client'
|
||||||
client.client_id = '123'
|
client.client_id = str(random.randint(1, 999999)).zfill(6)
|
||||||
client.client_secret = '456'
|
if is_public:
|
||||||
|
client.client_type = 'public'
|
||||||
|
client.client_secret = ''
|
||||||
|
else:
|
||||||
|
client.client_secret = str(random.randint(1, 999999)).zfill(6)
|
||||||
client.response_type = response_type
|
client.response_type = response_type
|
||||||
client.redirect_uris = ['http://example.com/']
|
client.redirect_uris = ['http://example.com/']
|
||||||
|
|
||||||
|
@ -50,17 +56,6 @@ def create_fake_client(response_type):
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
def create_rsakey():
|
|
||||||
"""
|
|
||||||
Generate and save a sample RSA Key.
|
|
||||||
"""
|
|
||||||
fullpath = os.path.abspath(os.path.dirname(__file__)) + '/RSAKEY.pem'
|
|
||||||
|
|
||||||
with open(fullpath, 'r') as f:
|
|
||||||
key = f.read()
|
|
||||||
RSAKey(key=key).save()
|
|
||||||
|
|
||||||
|
|
||||||
def is_code_valid(url, user, client):
|
def is_code_valid(url, user, client):
|
||||||
"""
|
"""
|
||||||
Check if the code inside the url is valid.
|
Check if the code inside the url is valid.
|
||||||
|
|
|
@ -6,6 +6,7 @@ import uuid
|
||||||
|
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.core.management import call_command
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
@ -22,10 +23,35 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
call_command('creatersakey')
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.user = create_fake_user()
|
self.user = create_fake_user()
|
||||||
self.client = create_fake_client(response_type='code')
|
self.client = create_fake_client(response_type='code')
|
||||||
|
self.client_public = create_fake_client(response_type='code', is_public=True)
|
||||||
|
self.client_implicit = create_fake_client(response_type='id_token token')
|
||||||
self.state = uuid.uuid4().hex
|
self.state = uuid.uuid4().hex
|
||||||
|
self.nonce = uuid.uuid4().hex
|
||||||
|
|
||||||
|
def _auth_request(self, method, data={}, is_user_authenticated=False):
|
||||||
|
url = reverse('oidc_provider:authorize')
|
||||||
|
|
||||||
|
if method.lower() == 'get':
|
||||||
|
query_str = urlencode(data).replace('+', '%20')
|
||||||
|
if query_str:
|
||||||
|
url += '?' + query_str
|
||||||
|
request = self.factory.get(url)
|
||||||
|
elif method.lower() == 'post':
|
||||||
|
request = self.factory.post(url, data=data)
|
||||||
|
else:
|
||||||
|
raise Exception('Method unsupported for an Authorization Request.')
|
||||||
|
|
||||||
|
# Simulate that the user is logged.
|
||||||
|
request.user = self.user if is_user_authenticated else AnonymousUser()
|
||||||
|
|
||||||
|
response = AuthorizeView.as_view()(request)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def test_missing_parameters(self):
|
def test_missing_parameters(self):
|
||||||
"""
|
"""
|
||||||
|
@ -35,11 +61,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
|
|
||||||
See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
See: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
||||||
"""
|
"""
|
||||||
url = reverse('oidc_provider:authorize')
|
response = self._auth_request('get')
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(bool(response.content), True)
|
self.assertEqual(bool(response.content), True)
|
||||||
|
@ -52,19 +74,15 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||||
"""
|
"""
|
||||||
# Create an authorize request with an unsupported response_type.
|
# Create an authorize request with an unsupported response_type.
|
||||||
query_str = urlencode({
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'response_type': 'something_wrong',
|
'response_type': 'something_wrong',
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
}).replace('+', '%20')
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
response = self._auth_request('get', data)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.has_header('Location'), True)
|
self.assertEqual(response.has_header('Location'), True)
|
||||||
|
@ -80,34 +98,20 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
|
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#Authenticates
|
See: http://openid.net/specs/openid-connect-core-1_0.html#Authenticates
|
||||||
"""
|
"""
|
||||||
query_str = urlencode({
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
}).replace('+', '%20')
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
response = self._auth_request('get', data)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
request.user = AnonymousUser()
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
# Check if user was redirected to the login view.
|
# Check if user was redirected to the login view.
|
||||||
login_url_exists = settings.get('LOGIN_URL') in response['Location']
|
login_url_exists = settings.get('LOGIN_URL') in response['Location']
|
||||||
self.assertEqual(login_url_exists, True)
|
self.assertEqual(login_url_exists, True)
|
||||||
|
|
||||||
# Check if the login will redirect to a valid url.
|
|
||||||
try:
|
|
||||||
next_value = response['Location'].split(REDIRECT_FIELD_NAME + '=')[1]
|
|
||||||
next_url = unquote(next_value)
|
|
||||||
is_next_ok = next_url == url
|
|
||||||
except:
|
|
||||||
is_next_ok = False
|
|
||||||
self.assertEqual(is_next_ok, True)
|
|
||||||
|
|
||||||
def test_user_consent_inputs(self):
|
def test_user_consent_inputs(self):
|
||||||
"""
|
"""
|
||||||
Once the End-User is authenticated, the Authorization Server MUST
|
Once the End-User is authenticated, the Authorization Server MUST
|
||||||
|
@ -116,21 +120,18 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
|
|
||||||
See: http://openid.net/specs/openid-connect-core-1_0.html#Consent
|
See: http://openid.net/specs/openid-connect-core-1_0.html#Consent
|
||||||
"""
|
"""
|
||||||
query_str = urlencode({
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
}).replace('+', '%20')
|
# PKCE parameters.
|
||||||
|
'code_challenge': FAKE_CODE_CHALLENGE,
|
||||||
|
'code_challenge_method': 'S256',
|
||||||
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
# Check if hidden inputs exists in the form,
|
# Check if hidden inputs exists in the form,
|
||||||
# also if their values are valid.
|
# also if their values are valid.
|
||||||
|
@ -140,6 +141,8 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
|
'code_challenge': FAKE_CODE_CHALLENGE,
|
||||||
|
'code_challenge_method': 'S256',
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value in iter(to_check.items()):
|
for key, value in iter(to_check.items()):
|
||||||
|
@ -159,23 +162,18 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
the parameters defined in Section 4.1.2 of OAuth 2.0 [RFC6749]
|
the parameters defined in Section 4.1.2 of OAuth 2.0 [RFC6749]
|
||||||
by adding them as query parameters to the redirect_uri.
|
by adding them as query parameters to the redirect_uri.
|
||||||
"""
|
"""
|
||||||
response_type = 'code'
|
data = {
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize')
|
|
||||||
|
|
||||||
post_data = {
|
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'response_type': response_type,
|
'response_type': 'code',
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
|
# PKCE parameters.
|
||||||
|
'code_challenge': FAKE_CODE_CHALLENGE,
|
||||||
|
'code_challenge_method': 'S256',
|
||||||
}
|
}
|
||||||
|
|
||||||
request = self.factory.post(url, data=post_data)
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
# Because user doesn't allow app, SHOULD exists an error parameter
|
# Because user doesn't allow app, SHOULD exists an error parameter
|
||||||
# in the query.
|
# in the query.
|
||||||
|
@ -185,13 +183,9 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
msg='"access_denied" code is missing in query.')
|
msg='"access_denied" code is missing in query.')
|
||||||
|
|
||||||
# Simulate user authorization.
|
# Simulate user authorization.
|
||||||
post_data['allow'] = 'Accept' # Should be the value of the button.
|
data['allow'] = 'Accept' # Will be the value of the button.
|
||||||
|
|
||||||
request = self.factory.post(url, data=post_data)
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
is_code_ok = is_code_valid(url=response['Location'],
|
is_code_ok = is_code_valid(url=response['Location'],
|
||||||
user=self.user,
|
user=self.user,
|
||||||
|
@ -210,7 +204,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
list of scopes) and because they might be prompted for the same
|
list of scopes) and because they might be prompted for the same
|
||||||
authorization multiple times, the server skip it.
|
authorization multiple times, the server skip it.
|
||||||
"""
|
"""
|
||||||
post_data = {
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
|
@ -220,34 +214,25 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
request = self.factory.post(reverse('oidc_provider:authorize'),
|
request = self.factory.post(reverse('oidc_provider:authorize'),
|
||||||
data=post_data)
|
data=data)
|
||||||
# Simulate that the user is logged.
|
# Simulate that the user is logged.
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
|
|
||||||
with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True):
|
with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True):
|
||||||
response = AuthorizeView.as_view()(request)
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
|
|
||||||
self.assertEqual('code' in response['Location'], True,
|
self.assertEqual('code' in response['Location'], True,
|
||||||
msg='Code is missing in the returned url.')
|
msg='Code is missing in the returned url.')
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
|
|
||||||
is_code_ok = is_code_valid(url=response['Location'],
|
is_code_ok = is_code_valid(url=response['Location'],
|
||||||
user=self.user,
|
user=self.user,
|
||||||
client=self.client)
|
client=self.client)
|
||||||
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
|
self.assertEqual(is_code_ok, True, msg='Code returned is invalid.')
|
||||||
|
|
||||||
del post_data['allow']
|
del data['allow']
|
||||||
query_str = urlencode(post_data).replace('+', '%20')
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
# Ensure user consent skip is enabled.
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
is_code_ok = is_code_valid(url=response['Location'],
|
is_code_ok = is_code_valid(url=response['Location'],
|
||||||
user=self.user,
|
user=self.user,
|
||||||
|
@ -255,10 +240,7 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
self.assertEqual(is_code_ok, True, msg='Code returned is invalid or missing.')
|
self.assertEqual(is_code_ok, True, msg='Code returned is invalid or missing.')
|
||||||
|
|
||||||
def test_response_uri_is_properly_constructed(self):
|
def test_response_uri_is_properly_constructed(self):
|
||||||
"""
|
data = {
|
||||||
TODO
|
|
||||||
"""
|
|
||||||
post_data = {
|
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri + "?redirect_state=xyz",
|
'redirect_uri': self.client.default_redirect_uri + "?redirect_state=xyz",
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
|
@ -267,100 +249,85 @@ class AuthorizationCodeFlowTestCase(TestCase):
|
||||||
'allow': 'Accept',
|
'allow': 'Accept',
|
||||||
}
|
}
|
||||||
|
|
||||||
request = self.factory.post(reverse('oidc_provider:authorize'),
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
data=post_data)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
# TODO
|
||||||
|
|
||||||
is_code_ok = is_code_valid(url=response['Location'],
|
def test_public_client_auto_approval(self):
|
||||||
user=self.user,
|
|
||||||
client=self.client)
|
|
||||||
self.assertEqual(is_code_ok, True,
|
|
||||||
msg='Code returned is invalid.')
|
|
||||||
|
|
||||||
def test_scope_with_plus(self):
|
|
||||||
"""
|
"""
|
||||||
In query string, scope use `+` instead of the space url-encoded.
|
It's recommended not auto-approving requests for non-confidential clients.
|
||||||
"""
|
"""
|
||||||
scope_test = 'openid email profile'
|
data = {
|
||||||
|
'client_id': self.client_public.client_id,
|
||||||
query_str = urlencode({
|
|
||||||
'client_id': self.client.client_id,
|
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client_public.default_redirect_uri,
|
||||||
'scope': scope_test,
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
})
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
with self.settings(OIDC_SKIP_CONSENT_ALWAYS=True):
|
||||||
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
self.assertEqual('Request for Permission' in response.content.decode('utf-8'), True)
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
def test_implicit_missing_nonce(self):
|
||||||
|
|
||||||
self.assertEqual(scope_test in response.content.decode('utf-8'), True)
|
|
||||||
|
|
||||||
|
|
||||||
class ImplicitFlowTestCase(TestCase):
|
|
||||||
"""
|
|
||||||
Test cases for Authorize Endpoint using Implicit Grant Flow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
self.user = create_fake_user()
|
|
||||||
self.client = create_fake_client(response_type='id_token token')
|
|
||||||
self.state = uuid.uuid4().hex
|
|
||||||
self.nonce = uuid.uuid4().hex
|
|
||||||
create_rsakey()
|
|
||||||
|
|
||||||
def test_missing_nonce(self):
|
|
||||||
"""
|
"""
|
||||||
The `nonce` parameter is REQUIRED if you use the Implicit Flow.
|
The `nonce` parameter is REQUIRED if you use the Implicit Flow.
|
||||||
"""
|
"""
|
||||||
query_str = urlencode({
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client_implicit.client_id,
|
||||||
'response_type': self.client.response_type,
|
'response_type': self.client_implicit.response_type,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client_implicit.default_redirect_uri,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
}).replace('+', '%20')
|
}
|
||||||
|
|
||||||
url = reverse('oidc_provider:authorize') + '?' + query_str
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||||
|
|
||||||
request = self.factory.get(url)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
self.assertEqual('#error=invalid_request' in response['Location'], True)
|
self.assertEqual('#error=invalid_request' in response['Location'], True)
|
||||||
|
|
||||||
def test_access_token_response(self):
|
def test_implicit_access_token_response(self):
|
||||||
"""
|
"""
|
||||||
Unlike the Authorization Code flow, in which the client makes
|
Unlike the Authorization Code flow, in which the client makes
|
||||||
separate requests for authorization and for an access token, the client
|
separate requests for authorization and for an access token, the client
|
||||||
receives the access token as the result of the authorization request.
|
receives the access token as the result of the authorization request.
|
||||||
"""
|
"""
|
||||||
post_data = {
|
data = {
|
||||||
'client_id': self.client.client_id,
|
'client_id': self.client_implicit.client_id,
|
||||||
'redirect_uri': self.client.default_redirect_uri,
|
'redirect_uri': self.client_implicit.default_redirect_uri,
|
||||||
'response_type': self.client.response_type,
|
'response_type': self.client_implicit.response_type,
|
||||||
'scope': 'openid email',
|
'scope': 'openid email',
|
||||||
'state': self.state,
|
'state': self.state,
|
||||||
'nonce': self.nonce,
|
'nonce': self.nonce,
|
||||||
'allow': 'Accept',
|
'allow': 'Accept',
|
||||||
}
|
}
|
||||||
|
|
||||||
request = self.factory.post(reverse('oidc_provider:authorize'),
|
response = self._auth_request('post', data, is_user_authenticated=True)
|
||||||
data=post_data)
|
|
||||||
# Simulate that the user is logged.
|
|
||||||
request.user = self.user
|
|
||||||
|
|
||||||
response = AuthorizeView.as_view()(request)
|
|
||||||
|
|
||||||
self.assertEqual('access_token' in response['Location'], True)
|
self.assertEqual('access_token' in response['Location'], True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prompt_parameter(self):
|
||||||
|
"""
|
||||||
|
Specifies whether the Authorization Server prompts the End-User for reauthentication and consent.
|
||||||
|
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
'client_id': self.client.client_id,
|
||||||
|
'response_type': self.client.response_type,
|
||||||
|
'redirect_uri': self.client.default_redirect_uri,
|
||||||
|
'scope': 'openid email',
|
||||||
|
'state': self.state,
|
||||||
|
}
|
||||||
|
|
||||||
|
data['prompt'] = 'none'
|
||||||
|
|
||||||
|
response = self._auth_request('get', data)
|
||||||
|
|
||||||
|
# An error is returned if an End-User is not already authenticated.
|
||||||
|
self.assertEqual('login_required' in response['Location'], True)
|
||||||
|
|
||||||
|
response = self._auth_request('get', data, is_user_authenticated=True)
|
||||||
|
|
||||||
|
# An error is returned if the Client does not have pre-configured consent for the requested Claims.
|
||||||
|
self.assertEqual('interaction_required' in response['Location'], True)
|
||||||
|
|
|
@ -4,6 +4,7 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
from django.test import RequestFactory, override_settings
|
from django.test import RequestFactory, override_settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from jwkest.jwk import KEYS
|
from jwkest.jwk import KEYS
|
||||||
|
@ -23,10 +24,10 @@ class TokenTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
call_command('creatersakey')
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.user = create_fake_user()
|
self.user = create_fake_user()
|
||||||
self.client = create_fake_client(response_type='code')
|
self.client = create_fake_client(response_type='code')
|
||||||
create_rsakey()
|
|
||||||
|
|
||||||
def _auth_code_post_data(self, code):
|
def _auth_code_post_data(self, code):
|
||||||
"""
|
"""
|
||||||
|
@ -445,3 +446,22 @@ class TokenTestCase(TestCase):
|
||||||
|
|
||||||
self.assertEqual(id_token.get('test_idtoken_processing_hook2'), FAKE_RANDOM_STRING)
|
self.assertEqual(id_token.get('test_idtoken_processing_hook2'), FAKE_RANDOM_STRING)
|
||||||
self.assertEqual(id_token.get('test_idtoken_processing_hook_user_email2'), self.user.email)
|
self.assertEqual(id_token.get('test_idtoken_processing_hook_user_email2'), self.user.email)
|
||||||
|
|
||||||
|
def test_pkce_parameters(self):
|
||||||
|
"""
|
||||||
|
Test Proof Key for Code Exchange by OAuth Public Clients.
|
||||||
|
https://tools.ietf.org/html/rfc7636
|
||||||
|
"""
|
||||||
|
code = create_code(user=self.user, client=self.client,
|
||||||
|
scope=['openid', 'email'], nonce=FAKE_NONCE, is_authentication=True,
|
||||||
|
code_challenge=FAKE_CODE_CHALLENGE, code_challenge_method='S256')
|
||||||
|
code.save()
|
||||||
|
|
||||||
|
post_data = self._auth_code_post_data(code=code.code)
|
||||||
|
|
||||||
|
# Add parameters.
|
||||||
|
post_data['code_verifier'] = FAKE_CODE_VERIFIER
|
||||||
|
|
||||||
|
response = self._post_request(post_data)
|
||||||
|
|
||||||
|
response_dic = json.loads(response.content.decode('utf-8'))
|
||||||
|
|
|
@ -40,12 +40,14 @@ class AuthorizeView(View):
|
||||||
if hook_resp:
|
if hook_resp:
|
||||||
return hook_resp
|
return hook_resp
|
||||||
|
|
||||||
if settings.get('OIDC_SKIP_CONSENT_ALWAYS'):
|
if settings.get('OIDC_SKIP_CONSENT_ALWAYS') and not (authorize.client.client_type == 'public') \
|
||||||
|
and not (authorize.params.prompt == 'consent'):
|
||||||
return redirect(authorize.create_response_uri())
|
return redirect(authorize.create_response_uri())
|
||||||
|
|
||||||
if settings.get('OIDC_SKIP_CONSENT_ENABLE'):
|
if settings.get('OIDC_SKIP_CONSENT_ENABLE'):
|
||||||
# Check if user previously give consent.
|
# Check if user previously give consent.
|
||||||
if authorize.client_has_user_consent():
|
if authorize.client_has_user_consent() and not (authorize.client.client_type == 'public') \
|
||||||
|
and not (authorize.params.prompt == 'consent'):
|
||||||
return redirect(authorize.create_response_uri())
|
return redirect(authorize.create_response_uri())
|
||||||
|
|
||||||
# Generate hidden inputs for the form.
|
# Generate hidden inputs for the form.
|
||||||
|
@ -66,10 +68,22 @@ class AuthorizeView(View):
|
||||||
'params': authorize.params,
|
'params': authorize.params,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
return render(request, 'oidc_provider/authorize.html', context)
|
return render(request, 'oidc_provider/authorize.html', context)
|
||||||
else:
|
else:
|
||||||
path = request.get_full_path()
|
if authorize.params.prompt == 'none':
|
||||||
return redirect_to_login(path)
|
raise AuthorizeError(authorize.params.redirect_uri, 'login_required', authorize.grant_type)
|
||||||
|
|
||||||
|
return redirect_to_login(request.get_full_path())
|
||||||
|
|
||||||
except (ClientIdError, RedirectUriError) as error:
|
except (ClientIdError, RedirectUriError) as error:
|
||||||
context = {
|
context = {
|
||||||
|
@ -87,15 +101,12 @@ class AuthorizeView(View):
|
||||||
return redirect(uri)
|
return redirect(uri)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
authorize = AuthorizeEndpoint(request)
|
authorize = AuthorizeEndpoint(request)
|
||||||
|
|
||||||
allow = True if request.POST.get('allow') else False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
authorize.validate_params()
|
authorize.validate_params()
|
||||||
|
|
||||||
if not allow:
|
if not request.POST.get('allow'):
|
||||||
raise AuthorizeError(authorize.params.redirect_uri,
|
raise AuthorizeError(authorize.params.redirect_uri,
|
||||||
'access_denied',
|
'access_denied',
|
||||||
authorize.grant_type)
|
authorize.grant_type)
|
||||||
|
|
Loading…
Reference in a new issue