updating library...
This commit is contained in:
commit
c48832b8cd
54 changed files with 1969 additions and 562 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,3 +1,7 @@
|
|||
__pycache__/
|
||||
build/
|
||||
dist/
|
||||
*.py[cod]
|
||||
*.egg-info
|
||||
*.egg-info
|
||||
.ropeproject
|
||||
src/
|
56
CHANGELOG.md
Normal file
56
CHANGELOG.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
# CHANGELOG
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
### [Unreleased]
|
||||
|
||||
#### Added
|
||||
- Better naming for models in the admin.
|
||||
|
||||
### [0.0.5] - 2015-05-09
|
||||
|
||||
#### Added
|
||||
- Support for Django 1.8.
|
||||
|
||||
#### Fixed
|
||||
- Validation of scope in UserInfo endpoint.
|
||||
|
||||
### [0.0.4] - 2015-04-22
|
||||
|
||||
#### Added
|
||||
- Initial migrations.
|
||||
|
||||
##### Fixed
|
||||
- Important bug with id_token when using implicit flow.
|
||||
- Validate Code expiration in Auth Code Flow.
|
||||
- Validate Access Token expiration in UserInfo endpoint.
|
||||
|
||||
### [0.0.3] - 2015-04-15
|
||||
|
||||
##### Added
|
||||
- Normalize gender field in UserInfo.
|
||||
|
||||
##### Changed
|
||||
- Make address_formatted a property inside UserInfo.
|
||||
|
||||
##### Fixed
|
||||
- Important bug in claims response.
|
||||
|
||||
### [0.0.2] - 2015-03-26
|
||||
|
||||
##### Added
|
||||
- Setting OIDC_AFTER_USERLOGIN_HOOK.
|
||||
|
||||
##### Fixed
|
||||
- Tests failing because an incorrect tag in one template.
|
||||
|
||||
### [0.0.1] - 2015-03-13
|
||||
|
||||
##### Added
|
||||
- Provider Configuration Information endpoint.
|
||||
- Setting OIDC_IDTOKEN_SUB_GENERATOR.
|
||||
|
||||
##### Changed
|
||||
- Now use setup in OIDC_EXTRA_SCOPE_CLAIMS setting.
|
||||
|
||||
### [0.0.0] - 2015-02-26
|
279
DOC.md
Normal file
279
DOC.md
Normal file
|
@ -0,0 +1,279 @@
|
|||
# Welcome to the Docs!
|
||||
|
||||
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.
|
||||
|
||||
|
||||
**This project is still in DEVELOPMENT and is rapidly changing. DO NOT USE IT FOR PRODUCTION SITES, unless you know what you do.**
|
||||
|
||||
****************************************
|
||||
|
||||
Before getting started there are some important things that you should know:
|
||||
* Although OpenID was built on top of OAuth2, this isn't an OAuth2 server. Maybe in a future it will be.
|
||||
* 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.
|
||||
|
||||
# Table Of Contents
|
||||
|
||||
- [Requirements](#requirements)
|
||||
- [Installation](#installation)
|
||||
- [Settings](#settings)
|
||||
- [SITE_URL](#site_url)
|
||||
- [LOGIN_URL](#login_url)
|
||||
- [OIDC_AFTER_USERLOGIN_HOOK](#oidc_after_userlogin_hook)
|
||||
- [OIDC_CODE_EXPIRE](#oidc_code_expire)
|
||||
- [OIDC_EXTRA_SCOPE_CLAIMS](#oidc_extra_scope_claims)
|
||||
- [OIDC_IDTOKEN_EXPIRE](#oidc_idtoken_expire)
|
||||
- [OIDC_IDTOKEN_SUB_GENERATOR](#oidc_idtoken_sub_generator)
|
||||
- [OIDC_TOKEN_EXPIRE](#oidc_token_expire)
|
||||
- [Users And Clients](#users-and-clients)
|
||||
- [Templates](#templates)
|
||||
- [Server Endpoints](#server-endpoints)
|
||||
- [Running Tests](#running-tests)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 2.7.*.
|
||||
- Django 1.7.*.
|
||||
|
||||
## Installation
|
||||
|
||||
If you want to get started fast see our [Example Project](https://github.com/juanifioren/django-oidc-provider/tree/master/example_project) folder.
|
||||
|
||||
Install the package using pip.
|
||||
|
||||
```bash
|
||||
pip install django-oidc-provider
|
||||
# Or latest code from repo.
|
||||
pip install git+https://github.com/juanifioren/django-oidc-provider.git#egg=oidc_provider
|
||||
# Or if working from a local repo
|
||||
pip install git+./#egg=oidc_provider
|
||||
```
|
||||
|
||||
Add it to your apps.
|
||||
|
||||
```python
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'oidc_provider',
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
Add the provider urls.
|
||||
|
||||
```python
|
||||
urlpatterns = patterns('',
|
||||
# ...
|
||||
url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
Add required variables to your project settings.
|
||||
|
||||
##### SITE_URL
|
||||
REQUIRED. The OP server url. For example `http://localhost:8000`.
|
||||
|
||||
##### LOGIN_URL
|
||||
REQUIRED. Used to log the user in. [Read more in Django docs](https://docs.djangoproject.com/en/1.7/ref/settings/#login-url).
|
||||
|
||||
Default is `/accounts/login/`.
|
||||
|
||||
##### OIDC_AFTER_USERLOGIN_HOOK
|
||||
OPTIONAL. Provide a way to plug into the process after the user has logged in, typically to perform some business logic.
|
||||
|
||||
Default is:
|
||||
```python
|
||||
def default_hook_func(request, user, client):
|
||||
return None
|
||||
```
|
||||
|
||||
Return `None` if you want to continue with the flow.
|
||||
|
||||
The typical situation will be checking some state of the user or maybe redirect him somewhere.
|
||||
With request you have access to all OIDC parameters. Remember that if you redirect the user to another place then you need to take him back to the authorize endpoint (use `request.get_full_path()` as the value for a "next" parameter).
|
||||
|
||||
##### OIDC_CODE_EXPIRE
|
||||
OPTIONAL. Expressed in seconds.
|
||||
|
||||
Default is `60*10`.
|
||||
|
||||
##### OIDC_EXTRA_SCOPE_CLAIMS
|
||||
OPTIONAL. Used to add extra scopes specific for your app. This class MUST inherit ``AbstractScopeClaims``.
|
||||
|
||||
OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens.
|
||||
|
||||
[Here](http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) you have the standard scopes defined by the protocol.
|
||||
|
||||
Check out an example of how to implement it:
|
||||
|
||||
```python
|
||||
from oidc_provider.lib.claims import AbstractScopeClaims
|
||||
|
||||
class MyAppScopeClaims(AbstractScopeClaims):
|
||||
|
||||
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()
|
||||
|
||||
def scope_books(self, user):
|
||||
|
||||
# Here you can search books for this user.
|
||||
|
||||
dic = {
|
||||
'books_readed': books_readed_count,
|
||||
}
|
||||
|
||||
return dic
|
||||
```
|
||||
|
||||
See how we create our own scopes using the convention:
|
||||
|
||||
``def scope_<SCOPE_NAME>(self, user):``
|
||||
|
||||
If a field is empty or ``None`` will be cleaned from the response.
|
||||
|
||||
##### OIDC_IDTOKEN_EXPIRE
|
||||
OPTIONAL. Expressed in seconds.
|
||||
|
||||
Default is `60*10`.
|
||||
|
||||
##### OIDC_IDTOKEN_SUB_GENERATOR
|
||||
OPTIONAL. Subject Identifier. A locally unique and never reassigned identifier within the Issuer for the End-User, which is intended to be consumed by the Client.
|
||||
|
||||
Is just a function that receives a `user` object. Returns a unique string for the given user.
|
||||
|
||||
Default is:
|
||||
```python
|
||||
def default_sub_generator(user):
|
||||
|
||||
return user.id
|
||||
```
|
||||
|
||||
##### OIDC_TOKEN_EXPIRE
|
||||
OPTIONAL. Token object expiration after been created. Expressed in seconds.
|
||||
|
||||
Default is `60*60`.
|
||||
|
||||
## Users And Clients
|
||||
|
||||
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](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 then create a Client with django shell: ``python manage.py shell``.
|
||||
|
||||
```python
|
||||
>>> 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()
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
Add your own templates files inside a folder named ``templates/oidc_provider/``.
|
||||
You can copy the sample html here and edit them with your own styles.
|
||||
|
||||
**authorize.html**
|
||||
|
||||
```html
|
||||
<h1>Request for Permission</h1>
|
||||
|
||||
<p>Client <strong>{{ client.name }}</strong> would like to access this information of you ...</p>
|
||||
|
||||
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
{{ hidden_inputs }}
|
||||
|
||||
<ul>
|
||||
{% for scope in params.scope %}
|
||||
<li>{{ scope | capfirst }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<input name="allow" type="submit" value="Authorize" />
|
||||
|
||||
</form>
|
||||
```
|
||||
|
||||
**error.html**
|
||||
|
||||
```html
|
||||
<h3>{{ error }}</h3>
|
||||
<p>{{ description }}</p>
|
||||
```
|
||||
|
||||
## Server Endpoints
|
||||
|
||||
**/authorize endpoint**
|
||||
|
||||
Example of an OpenID Authentication Request using the ``Authorization Code`` flow.
|
||||
|
||||
```curl
|
||||
GET /openid/authorize?client_id=123&redirect_uri=http%3A%2F%2Fexample.com%2F&response_type=code&scope=openid%20profile%20email&state=abcdefgh HTTP/1.1
|
||||
Host: localhost:8000
|
||||
Cache-Control: no-cache
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
After the user accepts and authorizes the client application, the server redirects to:
|
||||
|
||||
```curl
|
||||
http://example.com/?code=5fb3b172913448acadce6b011af1e75e&state=abcdefgh
|
||||
```
|
||||
|
||||
The ``code`` param will be use it to obtain access token.
|
||||
|
||||
**/token endpoint**
|
||||
|
||||
```curl
|
||||
POST /openid/token/ HTTP/1.1
|
||||
Host: localhost:8000
|
||||
Cache-Control: no-cache
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
client_id=123&client_secret=456&redirect_uri=http%253A%252F%252Fexample.com%252F&grant_type=authorization_code&code=[CODE]&state=abcdefgh
|
||||
```
|
||||
|
||||
**/userinfo endpoint**
|
||||
|
||||
```curl
|
||||
POST /openid/userinfo/ HTTP/1.1
|
||||
Host: localhost:8000
|
||||
Authorization: Bearer [ACCESS_TOKEN]
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
You need a Django project properly configured with the package. Then just run tests as normal.
|
||||
|
||||
```bash
|
||||
$ python manage.py test oidc_provider
|
||||
```
|
||||
|
||||
This provider was tested (and fully works) with these OIDC Clients:
|
||||
- [Drupal OpenID Connect](https://www.drupal.org/project/openid_connect)
|
||||
- [Passport OpenID Connect](https://github.com/jaredhanson/passport-openidconnect) (for NodeJS)
|
|
@ -1,3 +1,3 @@
|
|||
include LICENSE
|
||||
include README.rst
|
||||
recursive-include openid_provider/templates *
|
||||
recursive-include oidc_provider/templates *
|
136
README.rst
136
README.rst
|
@ -1,133 +1,19 @@
|
|||
|
||||
.. image:: http://s1.postimg.org/qcm2dtr6n/title.png
|
||||
####################################################
|
||||
Django OIDC Provider
|
||||
####################
|
||||
|
||||
**This project is in ALFA version and is rapidly changing. DO NOT USE IT FOR PRODUCTION SITES.**
|
||||
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.
|
||||
|
||||
Important things that you should know:
|
||||
Read docs for more info.
|
||||
|
||||
- Although OpenID was built on top of OAuth2, this isn't an OAuth2 server. Maybe in a future it will be.
|
||||
- This cover ``authorization_code`` flow and ``implicit`` flow, NO support for ``hybrid`` flow at this moment.
|
||||
- Only support for requesting Claims using Scope Values.
|
||||
https://github.com/juanifioren/django-oidc-provider/blob/master/DOC.md
|
||||
|
||||
See changelog here.
|
||||
|
||||
https://github.com/juanifioren/django-oidc-provider/blob/master/CHANGELOG.md
|
||||
|
||||
************
|
||||
Installation
|
||||
Contributing
|
||||
************
|
||||
|
||||
Install the package using pip.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pip install https://github.com/juanifioren/django-openid-provider/archive/master.zip
|
||||
|
||||
|
||||
Add it to your apps.
|
||||
|
||||
.. code:: python
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'openid_provider',
|
||||
# ...
|
||||
)
|
||||
|
||||
Add the provider urls.
|
||||
|
||||
.. code:: python
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# ...
|
||||
url(r'^openid/', include('openid_provider.urls', namespace='openid_provider')),
|
||||
# ...
|
||||
)
|
||||
|
||||
********
|
||||
Settings
|
||||
********
|
||||
|
||||
Add required variables to your project settings.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# REQUIRED.
|
||||
|
||||
# Your server provider url.
|
||||
SITE_URL = 'http://localhost:8000'
|
||||
|
||||
# Used to log the user in.
|
||||
# See: https://docs.djangoproject.com/en/1.7/ref/settings/#login-url
|
||||
LOGIN_URL = '/accounts/login/'
|
||||
|
||||
# OPTIONAL.
|
||||
|
||||
DOP_CODE_EXPIRE = 60*10 # 10 min.
|
||||
DOP_IDTOKEN_EXPIRE = 60*10, # 10 min.
|
||||
DOP_TOKEN_EXPIRE = 60*60 # 1 hour.
|
||||
|
||||
|
||||
********************
|
||||
Create User & Client
|
||||
********************
|
||||
|
||||
First of all, we need to create a user: ``python manage.py createsuperuser``.
|
||||
|
||||
Then let's create a Client. Start django shell: ``python manage.py shell``.
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> from openid_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()
|
||||
|
||||
*******************
|
||||
/authorize endpoint
|
||||
*******************
|
||||
|
||||
Example of an OpenID Authentication Request using the ´´Authorization Code´´ flow.
|
||||
|
||||
.. code:: curl
|
||||
|
||||
GET /openid/authorize?client_id=123&redirect_uri=http%3A%2F%2Fexample.com%2F&response_type=code&scope=openid%20profile%20email&state=abcdefgh HTTP/1.1
|
||||
Host: localhost:8000
|
||||
Cache-Control: no-cache
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
****
|
||||
Code
|
||||
****
|
||||
|
||||
After the user accepts and authorizes the client application, the server redirects to:
|
||||
|
||||
.. code:: curl
|
||||
|
||||
http://example.com/?code=5fb3b172913448acadce6b011af1e75e&state=abcdefgh
|
||||
|
||||
We extract the ``code`` param and use it to obtain access token.
|
||||
|
||||
***************
|
||||
/token endpoint
|
||||
***************
|
||||
|
||||
.. code:: curl
|
||||
|
||||
POST /openid/token/ HTTP/1.1
|
||||
Host: localhost:8000
|
||||
Cache-Control: no-cache
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
client_id=123&client_secret=456&redirect_uri=http%253A%252F%252Fexample.com%252F&grant_type=authorization_code&code=[CODE]&state=abcdefgh
|
||||
|
||||
******************
|
||||
/userinfo endpoint
|
||||
******************
|
||||
|
||||
.. code:: curl
|
||||
|
||||
POST /openid/userinfo/ HTTP/1.1
|
||||
Host: localhost:8000
|
||||
Authorization: Bearer [ACCESS_TOKEN]
|
||||
We love contributions, so please feel free to fix bugs, improve things, provide documentation. Just submit a Pull Request.
|
||||
|
|
1
example_project/.gitignore
vendored
Normal file
1
example_project/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.sqlite3
|
27
example_project/README.md
Normal file
27
example_project/README.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Example Project
|
||||
|
||||
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.
|
||||
|
||||
## Setup & Running
|
||||
|
||||
Setup project environment with [virtualenv](https://virtualenv.pypa.io) and [pip](https://pip.pypa.io).
|
||||
|
||||
```bash
|
||||
$ virtualenv project_env
|
||||
$ source project_env/bin/activate
|
||||
|
||||
$ git clone https://github.com/juanifioren/django-oidc-provider.git
|
||||
$ cd django-oidc-provider/example_project
|
||||
$ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Run your provider.
|
||||
|
||||
```bash
|
||||
$ python manage.py migrate
|
||||
$ python manage.py runserver
|
||||
```
|
||||
|
||||
Open your browser and go to `http://localhost:8000`. Voilà!
|
||||
|
||||
[<img title="django-oidc-provider" src="http://i.imgur.com/h4gv4s1.gif" width="100%" />](https://github.com/juanifioren/meteor-coffee-boilerplate)
|
10
example_project/manage.py
Executable file
10
example_project/manage.py
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "provider_app.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
78
example_project/provider_app/settings.py
Normal file
78
example_project/provider_app/settings.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'waw%j=vza!vc1^eyosw%#_!gg96%zb7sp*+!owkutue4i(sm91'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
TEMPLATE_DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'provider_app',
|
||||
'oidc_provider',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'provider_app.urls'
|
||||
|
||||
WSGI_APPLICATION = 'provider_app.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
|
||||
# OIDC Provider settings.
|
||||
|
||||
SITE_URL = 'http://localhost:8000'
|
24
example_project/provider_app/templates/accounts/login.html
Normal file
24
example_project/provider_app/templates/accounts/login.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default" style="width:400px;margin:0 auto 25px auto;">
|
||||
<div class="panel-body">
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger" role="alert">Username and password are incorrect.</div>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="username" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" name="password" placeholder="Password">
|
||||
</div>
|
||||
<input type="submit" class="btn btn-success btn-lg btn-block" value="Enter">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
12
example_project/provider_app/templates/accounts/logout.html
Normal file
12
example_project/provider_app/templates/accounts/logout.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default" style="width:400px;margin:0 auto 25px auto;">
|
||||
<div class="panel-body">
|
||||
<h1>Bye!</h1>
|
||||
<p>Thanks for spending some quality time with the web site today.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
50
example_project/provider_app/templates/base.html
Normal file
50
example_project/provider_app/templates/base.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OpenID Provider</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.4/cerulean/bootstrap.min.css" rel="stylesheet">
|
||||
<style type="text/css">body{padding-top:20px;padding-bottom:20px}.footer,.header,.marketing{padding-right:15px;padding-left:15px}.header{padding-bottom:20px;border-bottom:1px solid #e5e5e5}.header h3{margin-top:0;margin-bottom:0;line-height:40px}.footer{padding-top:19px;color:#777;border-top:1px solid #e5e5e5}@media (min-width:768px){.container{max-width:730px}}.container-narrow>hr{margin:30px 0}.jumbotron{border-bottom:1px solid #e5e5e5}.jumbotron .btn{padding:14px 24px;font-size:21px}.marketing{margin:40px 0}.marketing p+h4{margin-top:28px}@media screen and (min-width:768px){.footer,.header,.marketing{padding-right:0;padding-left:0}.header{margin-bottom:30px}.jumbotron{border-bottom:0}}</style>
|
||||
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div class="header clearfix">
|
||||
<nav>
|
||||
<ul class="nav nav-pills pull-right">
|
||||
<li role="presentation"><a href="{% url 'home' %}">Home</a></li>
|
||||
{% if user.is_authenticated %}
|
||||
<li role="presentation"><a href="#">{{ user.email }}</a></li>
|
||||
<li role="presentation"><a href="{% url 'logout' %}">Logout</a></li>
|
||||
{% else %}
|
||||
<li role="presentation"><a href="{% url 'login' %}">Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
<h3 class="text-muted">django-oidc-provider</h3>
|
||||
</div>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<footer class="footer">
|
||||
<p>Developed by <a href="http://github.com/juanifioren" target="_BLANK">Juan Ignacio Fiorentino</a>.</p>
|
||||
</footer>
|
||||
|
||||
</div> <!-- /container -->
|
||||
|
||||
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
11
example_project/provider_app/templates/home.html
Normal file
11
example_project/provider_app/templates/home.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="jumbotron">
|
||||
<h1>Welcome!</h1>
|
||||
<p class="lead">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.</p>
|
||||
<p><a class="btn btn-lg btn-success" href="https://github.com/juanifioren/django-oidc-provider" role="button">View on Github</a></p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,30 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Request for Permission</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>Client <strong>{{ client.name }}</strong> would like to access this information of you ...</p>
|
||||
|
||||
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
{{ hidden_inputs }}
|
||||
|
||||
<ul>
|
||||
{% for scope in params.scope %}
|
||||
<li>{{ scope | capfirst }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<input name="allow" class="btn btn-primary btn-lg btn-block" type="submit" value="Authorize" />
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,14 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{{ error }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
15
example_project/provider_app/urls.py
Normal file
15
example_project/provider_app/urls.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django.contrib.auth import views as auth_views
|
||||
from django.conf.urls import patterns, include, url
|
||||
from django.contrib import admin
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'),
|
||||
url(r'^accounts/login/$', auth_views.login, {'template_name': 'accounts/login.html'}, name='login'),
|
||||
url(r'^accounts/logout/$', auth_views.logout, {'template_name': 'accounts/logout.html'}, name='logout'),
|
||||
|
||||
url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
|
||||
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
)
|
5
example_project/provider_app/wsgi.py
Normal file
5
example_project/provider_app/wsgi.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
import os
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "provider_app.settings")
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
application = get_wsgi_application()
|
2
example_project/requirements.txt
Normal file
2
example_project/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
django==1.8
|
||||
django-oidc-provider==0.0.5
|
9
oidc_provider/admin.py
Normal file
9
oidc_provider/admin.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from oidc_provider.models import Client, Code, Token, UserInfo
|
||||
|
||||
|
||||
admin.site.register(Client)
|
||||
admin.site.register(Code)
|
||||
admin.site.register(Token)
|
||||
admin.site.register(UserInfo)
|
124
oidc_provider/lib/claims.py
Normal file
124
oidc_provider/lib/claims.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
from oidc_provider.models import UserInfo
|
||||
|
||||
|
||||
class AbstractScopeClaims(object):
|
||||
|
||||
def __init__(self, user, scopes):
|
||||
self.user = 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
|
||||
registered.
|
||||
|
||||
Returns a dic.
|
||||
"""
|
||||
dic = {}
|
||||
|
||||
for scope in self.scopes:
|
||||
|
||||
if scope in self._scopes_registered():
|
||||
dic.update(getattr(self, 'scope_' + scope)(self.user))
|
||||
|
||||
dic = self._clean_dic(dic)
|
||||
|
||||
return dic
|
||||
|
||||
def _scopes_registered(self):
|
||||
"""
|
||||
Return a list that contains all the scopes registered
|
||||
in the class.
|
||||
"""
|
||||
scopes = []
|
||||
|
||||
for name in self.__class__.__dict__:
|
||||
|
||||
if name.startswith('scope_'):
|
||||
scope = name.split('scope_')[1]
|
||||
scopes.append(scope)
|
||||
|
||||
return scopes
|
||||
|
||||
def _clean_dic(self, dic):
|
||||
"""
|
||||
Clean recursively all empty or None values inside a dict.
|
||||
"""
|
||||
aux_dic = dic.copy()
|
||||
for key, value in dic.iteritems():
|
||||
|
||||
if not value:
|
||||
del aux_dic[key]
|
||||
elif type(value) is dict:
|
||||
aux_dic[key] = self._clean_dic(value)
|
||||
|
||||
return aux_dic
|
||||
|
||||
class StandardScopeClaims(AbstractScopeClaims):
|
||||
"""
|
||||
Based on OpenID Standard Claims.
|
||||
See: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
try:
|
||||
self.userinfo = UserInfo.objects.get(user=self.user)
|
||||
except UserInfo.DoesNotExist:
|
||||
# Create an empty model object.
|
||||
self.userinfo = UserInfo()
|
||||
|
||||
def scope_profile(self, user):
|
||||
dic = {
|
||||
'name': self.userinfo.name,
|
||||
'given_name': self.userinfo.given_name,
|
||||
'family_name': self.userinfo.family_name,
|
||||
'middle_name': self.userinfo.middle_name,
|
||||
'nickname': self.userinfo.nickname,
|
||||
'preferred_username': self.userinfo.preferred_username,
|
||||
'profile': self.userinfo.profile,
|
||||
'picture': self.userinfo.picture,
|
||||
'website': self.userinfo.website,
|
||||
'gender': self.userinfo.gender,
|
||||
'birthdate': self.userinfo.birthdate,
|
||||
'zoneinfo': self.userinfo.zoneinfo,
|
||||
'locale': self.userinfo.locale,
|
||||
'updated_at': self.userinfo.updated_at,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
def scope_email(self, user):
|
||||
dic = {
|
||||
'email': self.user.email,
|
||||
'email_verified': self.userinfo.email_verified,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
def scope_phone(self, user):
|
||||
dic = {
|
||||
'phone_number': self.userinfo.phone_number,
|
||||
'phone_number_verified': self.userinfo.phone_number_verified,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
def scope_address(self, user):
|
||||
dic = {
|
||||
'address': {
|
||||
'formatted': self.userinfo.address_formatted,
|
||||
'street_address': self.userinfo.address_street_address,
|
||||
'locality': self.userinfo.address_locality,
|
||||
'region': self.userinfo.address_region,
|
||||
'postal_code': self.userinfo.address_postal_code,
|
||||
'country': self.userinfo.address_country,
|
||||
}
|
||||
}
|
||||
|
||||
return dic
|
|
@ -1,16 +1,13 @@
|
|||
import uuid
|
||||
|
||||
from datetime import timedelta
|
||||
import uuid
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from openid_provider import settings
|
||||
|
||||
from ..errors import *
|
||||
from ..utils.params import *
|
||||
from ..utils.token import *
|
||||
|
||||
from openid_provider.models import *
|
||||
from oidc_provider.lib.errors import *
|
||||
from oidc_provider.lib.utils.params import *
|
||||
from oidc_provider.lib.utils.token import *
|
||||
from oidc_provider.models import *
|
||||
from oidc_provider import settings
|
||||
|
||||
|
||||
class AuthorizeEndpoint(object):
|
||||
|
@ -23,7 +20,8 @@ class AuthorizeEndpoint(object):
|
|||
|
||||
# Because in this endpoint we handle both GET
|
||||
# and POST request.
|
||||
self.query_dict = (self.request.POST if self.request.method == 'POST' else self.request.GET)
|
||||
self.query_dict = (self.request.POST if self.request.method == 'POST'
|
||||
else self.request.GET)
|
||||
|
||||
self._extract_params()
|
||||
|
||||
|
@ -94,26 +92,22 @@ class AuthorizeEndpoint(object):
|
|||
|
||||
try:
|
||||
self.validate_params()
|
||||
|
||||
if self.grant_type == 'authorization_code':
|
||||
|
||||
code = Code()
|
||||
code.user = self.request.user
|
||||
code.client = self.client
|
||||
code.code = uuid.uuid4().hex
|
||||
code.expires_at = timezone.now() + timedelta(
|
||||
seconds=settings.get('DOP_CODE_EXPIRE'))
|
||||
code.scope = self.params.scope
|
||||
if self.grant_type == 'authorization_code':
|
||||
code = create_code(
|
||||
user=self.request.user,
|
||||
client=self.client,
|
||||
scope=self.params.scope)
|
||||
|
||||
code.save()
|
||||
|
||||
# Create the response uri.
|
||||
uri = self.params.redirect_uri + '?code={0}'.format(code.code)
|
||||
|
||||
else: # Implicit Flow
|
||||
|
||||
id_token_dic = create_id_token_dic(
|
||||
self.request.user,
|
||||
settings.get('SITE_URL'),
|
||||
self.client.client_id)
|
||||
id_token_dic = create_id_token(
|
||||
user=self.request.user,
|
||||
aud=self.client.client_id)
|
||||
|
||||
token = create_token(
|
||||
user=self.request.user,
|
||||
|
@ -127,13 +121,18 @@ class AuthorizeEndpoint(object):
|
|||
id_token = encode_id_token(
|
||||
id_token_dic, self.client.client_secret)
|
||||
|
||||
# TODO: Check if response_type is 'id_token token' then
|
||||
# Create the response uri.
|
||||
uri = self.params.redirect_uri + \
|
||||
'#token_type={0}&id_token={1}&expires_in={2}'.format(
|
||||
'bearer',
|
||||
id_token,
|
||||
60 * 10,
|
||||
)
|
||||
|
||||
# Check if response_type is 'id_token token' then
|
||||
# add access_token to the fragment.
|
||||
uri = self.params.redirect_uri + '#token_type={0}&id_token={1}&expires_in={2}'.format(
|
||||
'bearer',
|
||||
id_token,
|
||||
60 * 10
|
||||
)
|
||||
if self.params.response_type == 'id_token token':
|
||||
uri += '&access_token={0}'.format(token.access_token)
|
||||
except:
|
||||
raise AuthorizeError(
|
||||
self.params.redirect_uri,
|
||||
|
@ -141,8 +140,6 @@ class AuthorizeEndpoint(object):
|
|||
self.grant_type)
|
||||
|
||||
# Add state if present.
|
||||
uri = uri + \
|
||||
('&state={0}'.format(self.params.state)
|
||||
if self.params.state else '')
|
||||
uri += ('&state={0}'.format(self.params.state) if self.params.state else '')
|
||||
|
||||
return uri
|
31
oidc_provider/lib/endpoints/discovery.py
Normal file
31
oidc_provider/lib/endpoints/discovery.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
|
||||
from oidc_provider import settings
|
||||
from oidc_provider.lib.utils.common import get_issuer
|
||||
|
||||
|
||||
class ProviderInfoEndpoint(object):
|
||||
|
||||
@classmethod
|
||||
def create_response_dic(cls):
|
||||
dic = {}
|
||||
|
||||
dic['issuer'] = get_issuer()
|
||||
|
||||
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')
|
||||
|
||||
from oidc_provider.models import Client
|
||||
types_supported = [x[0] for x in Client.RESPONSE_TYPE_CHOICES]
|
||||
dic['response_types_supported'] = types_supported
|
||||
|
||||
# TODO:
|
||||
#dic['jwks_uri'] = None
|
||||
|
||||
# See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
|
||||
dic['subject_types_supported'] = ['public']
|
||||
|
||||
return dic
|
|
@ -1,30 +1,22 @@
|
|||
import urllib
|
||||
|
||||
try: # JsonResponse is only available in Django > 1.7
|
||||
from django.http import JsonResponse
|
||||
except ImportError:
|
||||
from ..utils.http import JsonResponse
|
||||
|
||||
from openid_provider import settings
|
||||
from django.http import JsonResponse
|
||||
|
||||
from ..utils.http import JsonResponse
|
||||
from ..errors import *
|
||||
from ..utils.params import *
|
||||
from ..utils.token import *
|
||||
|
||||
from openid_provider.models import *
|
||||
from oidc_provider.lib.errors import *
|
||||
from oidc_provider.lib.utils.params import *
|
||||
from oidc_provider.lib.utils.token import *
|
||||
from oidc_provider.models import *
|
||||
from oidc_provider import settings
|
||||
|
||||
|
||||
class TokenEndpoint(object):
|
||||
|
||||
def __init__(self, request):
|
||||
|
||||
self.request = request
|
||||
self.params = Params()
|
||||
self._extract_params()
|
||||
|
||||
def _extract_params(self):
|
||||
|
||||
query_dict = self.request.POST
|
||||
|
||||
self.params.client_id = query_dict.get('client_id', '')
|
||||
|
@ -36,7 +28,6 @@ class TokenEndpoint(object):
|
|||
self.params.state = query_dict.get('state', '')
|
||||
|
||||
def validate_params(self):
|
||||
|
||||
if not (self.params.grant_type == 'authorization_code'):
|
||||
raise TokenError('unsupported_grant_type')
|
||||
|
||||
|
@ -51,8 +42,8 @@ class TokenEndpoint(object):
|
|||
|
||||
self.code = Code.objects.get(code=self.params.code)
|
||||
|
||||
if not (self.code.client == self.client) and \
|
||||
not self.code.has_expired():
|
||||
if not (self.code.client == self.client) \
|
||||
or self.code.has_expired():
|
||||
raise TokenError('invalid_grant')
|
||||
|
||||
except Client.DoesNotExist:
|
||||
|
@ -62,11 +53,9 @@ class TokenEndpoint(object):
|
|||
raise TokenError('invalid_grant')
|
||||
|
||||
def create_response_dic(self):
|
||||
|
||||
id_token_dic = create_id_token_dic(
|
||||
self.code.user,
|
||||
settings.get('SITE_URL'),
|
||||
self.client.client_id)
|
||||
id_token_dic = create_id_token(
|
||||
user=self.code.user,
|
||||
aud=self.client.client_id)
|
||||
|
||||
token = create_token(
|
||||
user=self.code.user,
|
||||
|
@ -85,14 +74,14 @@ class TokenEndpoint(object):
|
|||
dic = {
|
||||
'access_token': token.access_token,
|
||||
'token_type': 'bearer',
|
||||
'expires_in': settings.get('DOP_TOKEN_EXPIRE'),
|
||||
'expires_in': settings.get('OIDC_TOKEN_EXPIRE'),
|
||||
'id_token': id_token,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
@classmethod
|
||||
def response(self, dic, status=200):
|
||||
def response(cls, dic, status=200):
|
||||
"""
|
||||
Create and return a response object.
|
||||
"""
|
|
@ -1,27 +1,23 @@
|
|||
import re
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.http import JsonResponse
|
||||
|
||||
try: # JsonResponse is only available in Django > 1.7
|
||||
from django.http import JsonResponse
|
||||
except ImportError:
|
||||
from ..utils.http import JsonResponse
|
||||
|
||||
from ..errors import *
|
||||
from ..scopes import *
|
||||
from ..utils.params import *
|
||||
from openid_provider.models import *
|
||||
from oidc_provider.lib.errors import *
|
||||
from oidc_provider.lib.claims import *
|
||||
from oidc_provider.lib.utils.params import *
|
||||
from oidc_provider.models import *
|
||||
from oidc_provider import settings
|
||||
|
||||
|
||||
class UserInfoEndpoint(object):
|
||||
|
||||
def __init__(self, request):
|
||||
|
||||
self.request = request
|
||||
self.params = Params()
|
||||
self._extract_params()
|
||||
|
||||
def _extract_params(self):
|
||||
|
||||
# TODO: Maybe add other ways of passing access token
|
||||
# http://tools.ietf.org/html/rfc6750#section-2
|
||||
self.params.access_token = self._get_access_token()
|
||||
|
@ -43,10 +39,15 @@ class UserInfoEndpoint(object):
|
|||
return access_token
|
||||
|
||||
def validate_params(self):
|
||||
|
||||
try:
|
||||
self.token = Token.objects.get(access_token=self.params.access_token)
|
||||
|
||||
if self.token.has_expired():
|
||||
raise UserInfoError('invalid_token')
|
||||
|
||||
if not ('openid' in self.token.scope):
|
||||
raise UserInfoError('insufficient_scope')
|
||||
|
||||
except Token.DoesNotExist:
|
||||
raise UserInfoError('invalid_token')
|
||||
|
||||
|
@ -61,15 +62,19 @@ class UserInfoEndpoint(object):
|
|||
'sub': self.token.id_token.get('sub'),
|
||||
}
|
||||
|
||||
standard_claims = StandardClaims(self.token.user, self.token.scope)
|
||||
|
||||
standard_claims = StandardScopeClaims(self.token.user, self.token.scope)
|
||||
|
||||
dic.update(standard_claims.create_response_dic())
|
||||
|
||||
extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS')(
|
||||
self.token.user, self.token.scope)
|
||||
|
||||
dic.update(extra_claims.create_response_dic())
|
||||
|
||||
return dic
|
||||
|
||||
@classmethod
|
||||
def response(self, dic):
|
||||
|
||||
def response(cls, dic):
|
||||
response = JsonResponse(dic, status=200)
|
||||
response['Cache-Control'] = 'no-store'
|
||||
response['Pragma'] = 'no-cache'
|
||||
|
@ -77,8 +82,7 @@ class UserInfoEndpoint(object):
|
|||
return response
|
||||
|
||||
@classmethod
|
||||
def error_response(self, code, description, status):
|
||||
|
||||
def error_response(cls, code, description, status):
|
||||
response = HttpResponse(status=status)
|
||||
response['WWW-Authenticate'] = 'error="{0}", error_description="{1}"'.format(code, description)
|
||||
|
|
@ -20,39 +20,54 @@ class AuthorizeError(Exception):
|
|||
# https://tools.ietf.org/html/rfc6749#section-4.1.2.1
|
||||
'invalid_request': 'The request is otherwise malformed',
|
||||
|
||||
'unauthorized_client': 'The client is not authorized to request an authorization code using this method',
|
||||
'unauthorized_client': 'The client is not authorized to request an '
|
||||
'authorization code using this method',
|
||||
|
||||
'access_denied': 'The resource owner or authorization server denied the request',
|
||||
'access_denied': 'The resource owner or authorization server denied '
|
||||
'the request',
|
||||
|
||||
'unsupported_response_type': 'The authorization server does not support obtaining an authorization code using '
|
||||
'this method',
|
||||
'unsupported_response_type': 'The authorization server does not '
|
||||
'support obtaining an authorization code '
|
||||
'using this method',
|
||||
|
||||
'invalid_scope': 'The requested scope is invalid, unknown, or malformed',
|
||||
'invalid_scope': 'The requested scope is invalid, unknown, or '
|
||||
'malformed',
|
||||
|
||||
'server_error': 'The authorization server encountered an error',
|
||||
|
||||
'temporarily_unavailable': 'The authorization server is currently unable to handle the request due to a '
|
||||
'temporary overloading or maintenance of the server',
|
||||
'temporarily_unavailable': 'The authorization server is currently '
|
||||
'unable to handle the request due to a '
|
||||
'temporary overloading or maintenance of '
|
||||
'the server',
|
||||
|
||||
# OpenID errors.
|
||||
# http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||
'interaction_required': 'The Authorization Server requires End-User interaction of some form to proceed',
|
||||
'interaction_required': 'The Authorization Server requires End-User '
|
||||
'interaction of some form to proceed',
|
||||
|
||||
'login_required': 'The Authorization Server requires End-User authentication',
|
||||
'login_required': 'The Authorization Server requires End-User '
|
||||
'authentication',
|
||||
|
||||
'account_selection_required': 'The End-User is required to select a session at the Authorization Server',
|
||||
'account_selection_required': 'The End-User is required to select a '
|
||||
'session at the Authorization Server',
|
||||
|
||||
'consent_required': 'The Authorization Server requires End-User consent',
|
||||
'consent_required': 'The Authorization Server requires End-User'
|
||||
'consent',
|
||||
|
||||
'invalid_request_uri': 'The request_uri in the Authorization Request returns an error or contains invalid data',
|
||||
'invalid_request_uri': 'The request_uri in the Authorization Request '
|
||||
'returns an error or contains invalid data',
|
||||
|
||||
'invalid_request_object': 'The request parameter contains an invalid Request Object',
|
||||
'invalid_request_object': 'The request parameter contains an invalid '
|
||||
'Request Object',
|
||||
|
||||
'request_not_supported': 'The provider does not support use of the request parameter',
|
||||
'request_not_supported': 'The provider does not support use of the '
|
||||
'request parameter',
|
||||
|
||||
'request_uri_not_supported': 'The provider does not support use of the request_uri parameter',
|
||||
'request_uri_not_supported': 'The provider does not support use of the '
|
||||
'request_uri parameter',
|
||||
|
||||
'registration_not_supported': 'The provider does not support use of the registration parameter',
|
||||
'registration_not_supported': 'The provider does not support use of '
|
||||
'the registration parameter',
|
||||
}
|
||||
|
||||
def __init__(self, redirect_uri, error, grant_type):
|
||||
|
@ -66,7 +81,8 @@ class AuthorizeError(Exception):
|
|||
|
||||
description = urllib.quote(self.description)
|
||||
|
||||
# See: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
|
||||
# See:
|
||||
# http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
|
||||
hash_or_question = '#' if self.grant_type == 'implicit' else '?'
|
||||
|
||||
uri = '{0}{1}error={2}&error_description={3}'.format(
|
||||
|
@ -92,18 +108,23 @@ class TokenError(Exception):
|
|||
# https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
'invalid_request': 'The request is otherwise malformed',
|
||||
|
||||
'invalid_client': 'Client authentication failed (e.g., unknown client, no client authentication included, '
|
||||
'or unsupported authentication method)',
|
||||
'invalid_client': 'Client authentication failed (e.g., unknown client, '
|
||||
'no client authentication included, or unsupported '
|
||||
'authentication method)',
|
||||
|
||||
'invalid_grant': 'The provided authorization grant or refresh token is invalid, expired, revoked, does not '
|
||||
'match the redirection URI used in the authorization request, or was issued to another client',
|
||||
'invalid_grant': 'The provided authorization grant or refresh token is '
|
||||
'invalid, expired, revoked, does not match the '
|
||||
'redirection URI used in the authorization request, '
|
||||
'or was issued to another client',
|
||||
|
||||
'unauthorized_client': 'The authenticated client is not authorized to use this authorization grant type',
|
||||
'unauthorized_client': 'The authenticated client is not authorized to '
|
||||
'use this authorization grant type',
|
||||
|
||||
'unsupported_grant_type': 'The authorization grant type is not supported by the authorization server',
|
||||
'unsupported_grant_type': 'The authorization grant type is not '
|
||||
'supported by the authorization server',
|
||||
|
||||
'invalid_scope': 'The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the '
|
||||
'resource owner',
|
||||
'invalid_scope': 'The requested scope is invalid, unknown, malformed, '
|
||||
'or exceeds the scope granted by the resource owner',
|
||||
}
|
||||
|
||||
def __init__(self, error):
|
||||
|
@ -129,10 +150,12 @@ class UserInfoError(Exception):
|
|||
'The request is otherwise malformed', 400
|
||||
),
|
||||
'invalid_token': (
|
||||
'The access token provided is expired, revoked, malformed, or invalid for other reasons', 401
|
||||
'The access token provided is expired, revoked, malformed, '
|
||||
'or invalid for other reasons', 401
|
||||
),
|
||||
'insufficient_scope': (
|
||||
'The request requires higher privileges than provided by the access token', 403
|
||||
'The request requires higher privileges than provided by '
|
||||
'the access token', 403
|
||||
),
|
||||
}
|
||||
|
16
oidc_provider/lib/utils/common.py
Normal file
16
oidc_provider/lib/utils/common.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
|
||||
from oidc_provider import settings
|
||||
|
||||
|
||||
def get_issuer():
|
||||
"""
|
||||
Construct the issuer full url. Basically is the site url with some path
|
||||
appended.
|
||||
"""
|
||||
site_url = settings.get('SITE_URL')
|
||||
path = reverse('oidc_provider:provider_info') \
|
||||
.split('/.well-known/openid-configuration/')[0]
|
||||
issuer = site_url + path
|
||||
|
||||
return issuer
|
|
@ -1,38 +1,43 @@
|
|||
from datetime import timedelta
|
||||
import time
|
||||
import jwt
|
||||
import uuid
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from openid_provider import settings
|
||||
from django.utils import timezone
|
||||
from openid_provider.models import *
|
||||
import jwt
|
||||
|
||||
from oidc_provider.lib.utils.common import get_issuer
|
||||
from oidc_provider.models import *
|
||||
from oidc_provider import settings
|
||||
|
||||
|
||||
def create_id_token_dic(user, iss, aud):
|
||||
def create_id_token(user, aud):
|
||||
"""
|
||||
Receives a user object, iss (issuer) and aud (audience).
|
||||
Then creates the id_token dic.
|
||||
Receives a user object and aud (audience).
|
||||
Then creates the id_token dictionary.
|
||||
See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
||||
|
||||
Return a dic.
|
||||
"""
|
||||
expires_in = settings.get('DOP_IDTOKEN_EXPIRE')
|
||||
sub = settings.get('OIDC_IDTOKEN_SUB_GENERATOR')(
|
||||
user=user)
|
||||
|
||||
expires_in = settings.get('OIDC_IDTOKEN_EXPIRE')
|
||||
|
||||
now = timezone.now()
|
||||
|
||||
# Convert datetimes into timestamps.
|
||||
iat_time = time.mktime(now.timetuple())
|
||||
exp_time = time.mktime((now + timedelta(seconds=expires_in)).timetuple())
|
||||
user_auth_time = time.mktime(user.last_login.timetuple())
|
||||
|
||||
user_auth_time = user.last_login or user.date_joined
|
||||
auth_time = time.mktime(user_auth_time.timetuple())
|
||||
|
||||
dic = {
|
||||
'iss': iss,
|
||||
'sub': user.id,
|
||||
'iss': get_issuer(),
|
||||
'sub': sub,
|
||||
'aud': aud,
|
||||
'exp': exp_time,
|
||||
'iat': iat_time,
|
||||
'auth_time': user_auth_time,
|
||||
'auth_time': auth_time,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
@ -64,7 +69,24 @@ def create_token(user, client, id_token_dic, scope):
|
|||
|
||||
token.refresh_token = uuid.uuid4().hex
|
||||
token.expires_at = timezone.now() + timedelta(
|
||||
seconds=settings.get('DOP_TOKEN_EXPIRE'))
|
||||
seconds=settings.get('OIDC_TOKEN_EXPIRE'))
|
||||
token.scope = scope
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def create_code(user, client, scope):
|
||||
"""
|
||||
Create and populate a Code object.
|
||||
|
||||
Return a Code object.
|
||||
"""
|
||||
code = Code()
|
||||
code.user = user
|
||||
code.client = client
|
||||
code.code = uuid.uuid4().hex
|
||||
code.expires_at = timezone.now() + timedelta(
|
||||
seconds=settings.get('OIDC_CODE_EXPIRE'))
|
||||
code.scope = scope
|
||||
|
||||
return code
|
101
oidc_provider/migrations/0001_initial.py
Normal file
101
oidc_provider/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Client',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(default=b'', max_length=100)),
|
||||
('client_id', models.CharField(unique=True, max_length=255)),
|
||||
('client_secret', models.CharField(unique=True, max_length=255)),
|
||||
('response_type', models.CharField(max_length=30, 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)')])),
|
||||
('_redirect_uris', models.TextField(default=b'')),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Code',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('_scope', models.TextField(default=b'')),
|
||||
('code', models.CharField(unique=True, max_length=255)),
|
||||
('client', models.ForeignKey(to='oidc_provider.Client')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Token',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('_scope', models.TextField(default=b'')),
|
||||
('access_token', models.CharField(unique=True, max_length=255)),
|
||||
('_id_token', models.TextField()),
|
||||
('client', models.ForeignKey(to='oidc_provider.Client')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserInfo',
|
||||
fields=[
|
||||
('user', models.OneToOneField(primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
('given_name', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('family_name', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('middle_name', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('nickname', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('gender', models.CharField(max_length=100, null=True, choices=[(b'F', b'Female'), (b'M', b'Male')])),
|
||||
('birthdate', models.DateField(null=True)),
|
||||
('zoneinfo', models.CharField(default=b'', max_length=100, null=True, blank=True)),
|
||||
('preferred_username', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('profile', models.URLField(default=b'', null=True, blank=True)),
|
||||
('picture', models.URLField(default=b'', null=True, blank=True)),
|
||||
('website', models.URLField(default=b'', null=True, blank=True)),
|
||||
('email_verified', models.NullBooleanField(default=False)),
|
||||
('locale', models.CharField(max_length=100, null=True, blank=True)),
|
||||
('phone_number', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('phone_number_verified', models.NullBooleanField(default=False)),
|
||||
('address_street_address', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('address_locality', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('address_region', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('address_postal_code', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('address_country', models.CharField(max_length=255, null=True, blank=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True, null=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='token',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='code',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
0
oidc_provider/migrations/__init__.py
Normal file
0
oidc_provider/migrations/__init__.py
Normal file
143
oidc_provider/models.py
Normal file
143
oidc_provider/models.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
import json
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class Client(models.Model):
|
||||
|
||||
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_id = models.CharField(max_length=255, unique=True)
|
||||
client_secret = models.CharField(max_length=255, unique=True)
|
||||
response_type = models.CharField(max_length=30,
|
||||
choices=RESPONSE_TYPE_CHOICES)
|
||||
|
||||
_redirect_uris = models.TextField(default='')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __unicode__(self):
|
||||
return self.__str__()
|
||||
|
||||
def redirect_uris():
|
||||
def fget(self):
|
||||
return self._redirect_uris.splitlines()
|
||||
def fset(self, value):
|
||||
self._redirect_uris = '\n'.join(value)
|
||||
return locals()
|
||||
redirect_uris = property(**redirect_uris())
|
||||
|
||||
@property
|
||||
def default_redirect_uri(self):
|
||||
return self.redirect_uris[0] if self.redirect_uris else ''
|
||||
|
||||
|
||||
class BaseCodeTokenModel(models.Model):
|
||||
|
||||
user = models.ForeignKey(User)
|
||||
client = models.ForeignKey(Client)
|
||||
expires_at = models.DateTimeField()
|
||||
_scope = models.TextField(default='')
|
||||
|
||||
def scope():
|
||||
def fget(self):
|
||||
return self._scope.split()
|
||||
def fset(self, value):
|
||||
self._scope = ' '.join(value)
|
||||
return locals()
|
||||
scope = property(**scope())
|
||||
|
||||
def has_expired(self):
|
||||
return timezone.now() >= self.expires_at
|
||||
|
||||
def __str__(self):
|
||||
return "%s - %s (%s)" % (self.client, self.user, self.expires_at)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.__str__()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Code(BaseCodeTokenModel):
|
||||
|
||||
code = models.CharField(max_length=255, unique=True)
|
||||
|
||||
|
||||
class Token(BaseCodeTokenModel):
|
||||
|
||||
access_token = models.CharField(max_length=255, unique=True)
|
||||
_id_token = models.TextField()
|
||||
def id_token():
|
||||
def fget(self):
|
||||
return json.loads(self._id_token)
|
||||
def fset(self, value):
|
||||
self._id_token = json.dumps(value)
|
||||
return locals()
|
||||
id_token = property(**id_token())
|
||||
|
||||
|
||||
class UserInfo(models.Model):
|
||||
|
||||
GENDER_CHOICES = [
|
||||
('F', 'Female'),
|
||||
('M', 'Male'),
|
||||
]
|
||||
|
||||
user = models.OneToOneField(User, 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)
|
||||
middle_name = models.CharField(max_length=255, blank=True, null=True)
|
||||
nickname = 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)
|
||||
zoneinfo = models.CharField(max_length=100, default='', blank=True,
|
||||
null=True)
|
||||
locale = models.CharField(max_length=100, default='', blank=True, null=True)
|
||||
preferred_username = models.CharField(max_length=255, blank=True, null=True)
|
||||
profile = models.URLField(default='', null=True, blank=True)
|
||||
picture = models.URLField(default='', null=True, blank=True)
|
||||
website = models.URLField(default='', null=True, blank=True)
|
||||
email_verified = models.NullBooleanField(default=False)
|
||||
locale = models.CharField(max_length=100, blank=True, null=True)
|
||||
phone_number = models.CharField(max_length=255, blank=True, null=True)
|
||||
phone_number_verified = models.NullBooleanField(default=False)
|
||||
address_street_address = models.CharField(max_length=255, blank=True,
|
||||
null=True)
|
||||
address_locality = models.CharField(max_length=255, blank=True, null=True)
|
||||
address_region = models.CharField(max_length=255, blank=True, null=True)
|
||||
address_postal_code = models.CharField(max_length=255, blank=True,
|
||||
null=True)
|
||||
address_country = models.CharField(max_length=255, blank=True, null=True)
|
||||
updated_at = models.DateTimeField(auto_now=True, null=True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
name = ''
|
||||
if self.given_name:
|
||||
name = self.given_name
|
||||
if self.family_name:
|
||||
name = name + ' ' + self.family_name
|
||||
|
||||
return name
|
||||
|
||||
@property
|
||||
def address_formatted(self):
|
||||
formatted = ', '.join([
|
||||
self.address_street_address,
|
||||
self.address_locality,
|
||||
self.address_country])
|
||||
|
||||
if formatted.startswith(', '):
|
||||
formatted = formatted[2:]
|
||||
if formatted.endswith(', '):
|
||||
formatted = formatted[:-2]
|
83
oidc_provider/settings.py
Normal file
83
oidc_provider/settings.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
from django.conf import settings
|
||||
|
||||
|
||||
class DefaultSettings(object):
|
||||
|
||||
@property
|
||||
def LOGIN_URL(self):
|
||||
"""
|
||||
REQUIRED.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def SITE_URL(self):
|
||||
"""
|
||||
REQUIRED.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def OIDC_AFTER_USERLOGIN_HOOK(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
def default_hook_func(request, user, client):
|
||||
return None
|
||||
|
||||
return default_hook_func
|
||||
|
||||
@property
|
||||
def OIDC_CODE_EXPIRE(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
return 60*10
|
||||
|
||||
@property
|
||||
def OIDC_EXTRA_SCOPE_CLAIMS(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
from oidc_provider.lib.claims import AbstractScopeClaims
|
||||
|
||||
return AbstractScopeClaims
|
||||
|
||||
@property
|
||||
def OIDC_IDTOKEN_EXPIRE(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
return 60*10
|
||||
|
||||
@property
|
||||
def OIDC_IDTOKEN_SUB_GENERATOR(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
def default_sub_generator(user):
|
||||
return user.id
|
||||
|
||||
return default_sub_generator
|
||||
|
||||
@property
|
||||
def OIDC_TOKEN_EXPIRE(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
return 60*60
|
||||
|
||||
default_settings = DefaultSettings()
|
||||
|
||||
def get(name):
|
||||
'''
|
||||
Helper function to use inside the package.
|
||||
'''
|
||||
try:
|
||||
value = getattr(default_settings, name)
|
||||
value = getattr(settings, name)
|
||||
except AttributeError:
|
||||
if value == None:
|
||||
raise Exception('You must set ' + name + ' in your settings.')
|
||||
|
||||
return value
|
20
oidc_provider/templates/oidc_provider/authorize.html
Normal file
20
oidc_provider/templates/oidc_provider/authorize.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<h1>Request for Permission</h1>
|
||||
|
||||
<p>Client <strong>{{ client.name }}</strong> would like to access this information of you ...</p>
|
||||
|
||||
<form method="post" action="{% url 'oidc_provider:authorize' %}">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
{{ hidden_inputs }}
|
||||
|
||||
<ul>
|
||||
{% for scope in params.scope %}
|
||||
<li>{{ scope | capfirst }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<input type="submit" value="Decline" />
|
||||
<input name="allow" type="submit" value="Authorize" />
|
||||
|
||||
</form>
|
5
oidc_provider/templates/oidc_provider/hidden_inputs.html
Normal file
5
oidc_provider/templates/oidc_provider/hidden_inputs.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<input name="client_id" type="hidden" value="{{ params.client_id }}" />
|
||||
<input name="redirect_uri" type="hidden" value="{{ params.redirect_uri }}" />
|
||||
<input name="response_type" type="hidden" value="{{ params.response_type }}" />
|
||||
<input name="scope" type="hidden" value="{{ params.scope | join:' ' }}" />
|
||||
<input name="state" type="hidden" value="{{ params.state }}" />
|
0
oidc_provider/tests/__init__.py
Normal file
0
oidc_provider/tests/__init__.py
Normal file
241
oidc_provider/tests/test_authorize_endpoint.py
Normal file
241
oidc_provider/tests/test_authorize_endpoint.py
Normal file
|
@ -0,0 +1,241 @@
|
|||
import urllib
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import RequestFactory
|
||||
from django.test import TestCase
|
||||
|
||||
from oidc_provider import settings
|
||||
from oidc_provider.models import *
|
||||
from oidc_provider.tests.utils import *
|
||||
from oidc_provider.views import *
|
||||
|
||||
|
||||
class AuthorizationCodeFlowTestCase(TestCase):
|
||||
"""
|
||||
Test cases for Authorize Endpoint using Authorization Code Flow.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_fake_user()
|
||||
self.client = create_fake_client(response_type='code')
|
||||
self.state = uuid.uuid4().hex
|
||||
|
||||
def test_missing_parameters(self):
|
||||
"""
|
||||
If the request fails due to a missing, invalid, or mismatching
|
||||
redirection URI, or if the client identifier is missing or invalid,
|
||||
the authorization server SHOULD inform the resource owner of the error.
|
||||
|
||||
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)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(bool(response.content), True)
|
||||
|
||||
def test_invalid_response_type(self):
|
||||
"""
|
||||
The OP informs the RP by using the Error Response parameters defined
|
||||
in Section 4.1.2.1 of OAuth 2.0.
|
||||
|
||||
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
||||
"""
|
||||
# Create an authorize request with an unsupported response_type.
|
||||
query_str = urllib.urlencode({
|
||||
'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)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.has_header('Location'), True)
|
||||
|
||||
# Should be an 'error' component in query.
|
||||
query_exists = 'error=' in response['Location']
|
||||
self.assertEqual(query_exists, True)
|
||||
|
||||
def test_user_not_logged(self):
|
||||
"""
|
||||
The Authorization Server attempts to Authenticate the End-User by
|
||||
redirecting to the login view.
|
||||
|
||||
See: http://openid.net/specs/openid-connect-core-1_0.html#Authenticates
|
||||
"""
|
||||
query_str = urllib.urlencode({
|
||||
'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)
|
||||
|
||||
# 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 = urllib.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
|
||||
obtain an authorization decision before releasing information to
|
||||
the Client.
|
||||
|
||||
See: http://openid.net/specs/openid-connect-core-1_0.html#Consent
|
||||
"""
|
||||
query_str = urllib.urlencode({
|
||||
'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)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
|
||||
# Remove the hook, because we want to test default behaviour.
|
||||
OIDC_AFTER_USERLOGIN_HOOK = settings.default_settings.OIDC_AFTER_USERLOGIN_HOOK
|
||||
with self.settings(
|
||||
OIDC_AFTER_USERLOGIN_HOOK=OIDC_AFTER_USERLOGIN_HOOK):
|
||||
response = AuthorizeView.as_view()(request)
|
||||
|
||||
# Check if hidden inputs exists in the form,
|
||||
# also if their values are valid.
|
||||
input_html = '<input name="{0}" type="hidden" value="{1}" />'
|
||||
|
||||
to_check = {
|
||||
'client_id': self.client.client_id,
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'response_type': 'code',
|
||||
}
|
||||
|
||||
for key, value in to_check.iteritems():
|
||||
is_input_ok = input_html.format(key, value) in response.content
|
||||
self.assertEqual(is_input_ok, True,
|
||||
msg='Hidden input for "'+key+'" fails.')
|
||||
|
||||
def test_user_consent_response(self):
|
||||
"""
|
||||
First,
|
||||
if the user denied the consent we must ensure that
|
||||
the error response parameters are added to the query component
|
||||
of the Redirection URI.
|
||||
|
||||
Second,
|
||||
if the user allow the RP then the server MUST return
|
||||
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 = {
|
||||
'client_id': self.client.client_id,
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'response_type': response_type,
|
||||
'scope': 'openid email',
|
||||
'state': self.state,
|
||||
}
|
||||
|
||||
request = self.factory.post(url, data=post_data)
|
||||
# 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
|
||||
# in the query.
|
||||
self.assertEqual('error=' in response['Location'], True,
|
||||
msg='error param is missing in query.')
|
||||
self.assertEqual('access_denied' in response['Location'], True,
|
||||
msg='"access_denied" code is missing in query.')
|
||||
|
||||
# Simulate user authorization.
|
||||
post_data['allow'] = 'Accept' # Should 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)
|
||||
|
||||
# Validate the code returned by the OP.
|
||||
code = (response['Location'].split('code='))[1].split('&')[0]
|
||||
try:
|
||||
code = Code.objects.get(code=code)
|
||||
is_code_ok = (code.client == self.client) and \
|
||||
(code.user == self.user)
|
||||
except:
|
||||
is_code_ok = False
|
||||
self.assertEqual(is_code_ok, True,
|
||||
msg='Code returned is invalid.')
|
||||
|
||||
# Check if the state is returned.
|
||||
state = (response['Location'].split('state='))[1].split('&')[0]
|
||||
self.assertEqual(state == self.state, True,
|
||||
msg='State change or is missing.')
|
||||
|
||||
|
||||
class AuthorizationImplicitFlowTestCase(TestCase):
|
||||
"""
|
||||
Test cases for Authorize Endpoint using Implicit 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
|
||||
|
||||
# TODO
|
||||
def test_something(self):
|
||||
query_str = urllib.urlencode({
|
||||
'client_id': self.client.client_id,
|
||||
'response_type': 'id_token token',
|
||||
'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)
|
||||
# Simulate that the user is logged.
|
||||
request.user = self.user
|
||||
|
||||
response = AuthorizeView.as_view()(request)
|
26
oidc_provider/tests/test_provider_info_endpoint.py
Normal file
26
oidc_provider/tests/test_provider_info_endpoint.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.test import RequestFactory
|
||||
from django.test import TestCase
|
||||
|
||||
from oidc_provider.views import *
|
||||
|
||||
|
||||
class ProviderInfoTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_response(self):
|
||||
"""
|
||||
See if the endpoint is returning the corresponding
|
||||
server information by checking status, content type, etc.
|
||||
"""
|
||||
url = reverse('oidc_provider:provider_info')
|
||||
|
||||
request = self.factory.get(url)
|
||||
|
||||
response = ProviderInfoView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'] == 'application/json', True)
|
||||
self.assertEqual(bool(response.content), True)
|
122
oidc_provider/tests/test_token_endpoint.py
Normal file
122
oidc_provider/tests/test_token_endpoint.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
import json
|
||||
from urllib import urlencode
|
||||
import uuid
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import RequestFactory
|
||||
from django.test import TestCase
|
||||
|
||||
from oidc_provider.lib.utils.token import *
|
||||
from oidc_provider.tests.utils import *
|
||||
from oidc_provider.views import *
|
||||
|
||||
|
||||
class TokenTestCase(TestCase):
|
||||
"""
|
||||
To obtain an Access Token and an ID Token, the RP Client sends a
|
||||
Token Request to the Token Endpoint to obtain a Token Response
|
||||
when using the Authorization Code Flow.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_fake_user()
|
||||
self.client = create_fake_client(response_type='code')
|
||||
self.state = uuid.uuid4().hex
|
||||
|
||||
def _post_request(self, post_data):
|
||||
"""
|
||||
Makes a request to the token endpoint by sending the
|
||||
`post_data` parameters using the 'application/x-www-form-urlencoded'
|
||||
format.
|
||||
"""
|
||||
url = reverse('oidc_provider:token')
|
||||
|
||||
request = self.factory.post(url,
|
||||
data=urlencode(post_data),
|
||||
content_type='application/x-www-form-urlencoded')
|
||||
|
||||
response = TokenView.as_view()(request)
|
||||
|
||||
return response
|
||||
|
||||
def _create_code(self):
|
||||
"""
|
||||
Generate a valid grant code.
|
||||
"""
|
||||
code = create_code(
|
||||
user=self.user,
|
||||
client=self.client,
|
||||
scope=['openid', 'email'])
|
||||
code.save()
|
||||
|
||||
return code
|
||||
|
||||
def test_request_methods(self):
|
||||
"""
|
||||
Client sends an HTTP POST request to the Token Endpoint. Other request
|
||||
methods MUST NOT be allowed.
|
||||
"""
|
||||
url = reverse('oidc_provider:token')
|
||||
|
||||
requests = [
|
||||
self.factory.get(url),
|
||||
self.factory.put(url),
|
||||
self.factory.delete(url),
|
||||
]
|
||||
|
||||
for request in requests:
|
||||
response = TokenView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code == 405, True,
|
||||
msg=request.method+' request does not return a 405 status.')
|
||||
|
||||
request = self.factory.post(url)
|
||||
|
||||
response = TokenView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code == 400, True,
|
||||
msg=request.method+' request does not return a 400 status.')
|
||||
|
||||
def test_client_authentication(self):
|
||||
"""
|
||||
The authorization server support including the
|
||||
client credentials in the request-body using the `client_id` and
|
||||
`client_secret`parameters.
|
||||
|
||||
See: http://tools.ietf.org/html/rfc6749#section-2.3.1
|
||||
"""
|
||||
code = self._create_code()
|
||||
|
||||
# Test a valid request to the token endpoint.
|
||||
post_data = {
|
||||
'client_id': self.client.client_id,
|
||||
'client_secret': self.client.client_secret,
|
||||
'redirect_uri': self.client.default_redirect_uri,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code.code,
|
||||
'state': self.state,
|
||||
}
|
||||
response = self._post_request(post_data)
|
||||
response_dic = json.loads(response.content)
|
||||
|
||||
self.assertEqual('access_token' in response_dic, True,
|
||||
msg='"access_token" key is missing in response.')
|
||||
self.assertEqual('error' in response_dic, False,
|
||||
msg='"error" key should not exists in response.')
|
||||
|
||||
# Now, test with an invalid client_id.
|
||||
invalid_data = post_data.copy()
|
||||
invalid_data['client_id'] = self.client.client_id * 2 # Fake id.
|
||||
|
||||
# Create another grant code.
|
||||
code = self._create_code()
|
||||
invalid_data['code'] = code.code
|
||||
|
||||
response = self._post_request(invalid_data)
|
||||
response_dic = json.loads(response.content)
|
||||
|
||||
self.assertEqual('error' in response_dic, True,
|
||||
msg='"error" key should exists in response.')
|
||||
self.assertEqual(response_dic.get('error') == 'invalid_client', True,
|
||||
msg='"error" key value should be "invalid_client".')
|
94
oidc_provider/tests/test_userinfo_endpoint.py
Normal file
94
oidc_provider/tests/test_userinfo_endpoint.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import RequestFactory
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from oidc_provider.lib.utils.token import *
|
||||
from oidc_provider.models import *
|
||||
from oidc_provider.tests.utils import *
|
||||
from oidc_provider.views import userinfo
|
||||
|
||||
|
||||
class UserInfoTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.user = create_fake_user()
|
||||
self.client = create_fake_client(response_type='code')
|
||||
|
||||
def _create_token(self):
|
||||
"""
|
||||
Generate a valid token.
|
||||
"""
|
||||
id_token_dic = create_id_token(self.user, self.client.client_id)
|
||||
|
||||
token = create_token(
|
||||
user=self.user,
|
||||
client=self.client,
|
||||
id_token_dic=id_token_dic,
|
||||
scope=['openid', 'email'])
|
||||
token.save()
|
||||
|
||||
return token
|
||||
|
||||
def _post_request(self, access_token):
|
||||
"""
|
||||
Makes a request to the userinfo endpoint by sending the
|
||||
`post_data` parameters using the 'multipart/form-data'
|
||||
format.
|
||||
"""
|
||||
url = reverse('oidc_provider:userinfo')
|
||||
|
||||
request = self.factory.post(url,
|
||||
data={},
|
||||
content_type='multipart/form-data')
|
||||
|
||||
request.META['HTTP_AUTHORIZATION'] = 'Bearer ' + access_token
|
||||
|
||||
response = userinfo(request)
|
||||
|
||||
return response
|
||||
|
||||
def test_response_with_valid_token(self):
|
||||
token = self._create_token()
|
||||
|
||||
# Test a valid request to the userinfo endpoint.
|
||||
response = self._post_request(token.access_token)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(bool(response.content), True)
|
||||
|
||||
def test_response_with_expired_token(self):
|
||||
token = self._create_token()
|
||||
|
||||
# Make token expired.
|
||||
token.expires_at = timezone.now() - timedelta(hours=1)
|
||||
token.save()
|
||||
|
||||
response = self._post_request(token.access_token)
|
||||
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
try:
|
||||
is_header_field_ok = 'invalid_token' in response['WWW-Authenticate']
|
||||
except KeyError:
|
||||
is_header_field_ok = False
|
||||
self.assertEqual(is_header_field_ok, True)
|
||||
|
||||
def test_response_with_invalid_scope(self):
|
||||
token = self._create_token()
|
||||
|
||||
token.scope = ['otherone']
|
||||
token.save()
|
||||
|
||||
response = self._post_request(token.access_token)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
try:
|
||||
is_header_field_ok = 'insufficient_scope' in response['WWW-Authenticate']
|
||||
except KeyError:
|
||||
is_header_field_ok = False
|
||||
self.assertEqual(is_header_field_ok, True)
|
36
oidc_provider/tests/utils.py
Normal file
36
oidc_provider/tests/utils.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from django.contrib.auth.models import User
|
||||
from oidc_provider.models import *
|
||||
|
||||
|
||||
def create_fake_user():
|
||||
"""
|
||||
Create a test user.
|
||||
|
||||
Return a User object.
|
||||
"""
|
||||
user = User()
|
||||
user.username = 'johndoe'
|
||||
user.email = 'johndoe@example.com'
|
||||
user.set_password('1234')
|
||||
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
def create_fake_client(response_type):
|
||||
"""
|
||||
Create a test client, response_type argument MUST be:
|
||||
'code', 'id_token' or 'id_token token'.
|
||||
|
||||
Return a Client object.
|
||||
"""
|
||||
client = Client()
|
||||
client.name = 'Some Client'
|
||||
client.client_id = '123'
|
||||
client.client_secret = '456'
|
||||
client.response_type = response_type
|
||||
client.redirect_uris = ['http://example.com/']
|
||||
|
||||
client.save()
|
||||
|
||||
return client
|
14
oidc_provider/urls.py
Normal file
14
oidc_provider/urls.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.conf.urls import patterns, include, url
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from oidc_provider.views import *
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
||||
url(r'^authorize/$', AuthorizeView.as_view(), name='authorize'),
|
||||
url(r'^token/$', csrf_exempt(TokenView.as_view()), name='token'),
|
||||
url(r'^userinfo/$', csrf_exempt(userinfo), name='userinfo'),
|
||||
|
||||
url(r'^\.well-known/openid-configuration/$', ProviderInfoView.as_view(), name='provider_info'),
|
||||
|
||||
)
|
|
@ -1,21 +1,15 @@
|
|||
import urllib
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.template.loader import render_to_string
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic import View
|
||||
|
||||
from .lib.errors import *
|
||||
from .lib.endpoints.authorize import *
|
||||
from .lib.endpoints.token import *
|
||||
from .lib.endpoints.userinfo import *
|
||||
|
||||
from openid_provider import settings
|
||||
from oidc_provider.lib.endpoints.authorize import *
|
||||
from oidc_provider.lib.endpoints.discovery import *
|
||||
from oidc_provider.lib.endpoints.token import *
|
||||
from oidc_provider.lib.endpoints.userinfo import *
|
||||
from oidc_provider.lib.errors import *
|
||||
|
||||
|
||||
class AuthorizeView(View):
|
||||
|
@ -28,20 +22,34 @@ class AuthorizeView(View):
|
|||
authorize.validate_params()
|
||||
|
||||
if request.user.is_authenticated():
|
||||
# Check if there's a hook setted.
|
||||
hook_resp = settings.get('OIDC_AFTER_USERLOGIN_HOOK')(
|
||||
request=request, user=request.user,
|
||||
client=authorize.client)
|
||||
if hook_resp:
|
||||
return hook_resp
|
||||
|
||||
# This is for printing scopes in the form.
|
||||
authorize.params.scope_str = ' '.join(authorize.params.scope)
|
||||
|
||||
# Generate hidden inputs for the form.
|
||||
context = {
|
||||
'params': authorize.params,
|
||||
}
|
||||
hidden_inputs = render_to_string(
|
||||
'oidc_provider/hidden_inputs.html', context)
|
||||
|
||||
# Remove `openid` from scope list
|
||||
# since we don't need to print it.
|
||||
authorize.params.scope.remove('openid')
|
||||
|
||||
context = {
|
||||
'client': authorize.client,
|
||||
'hidden_inputs': hidden_inputs,
|
||||
'params': authorize.params,
|
||||
}
|
||||
|
||||
return render(request, 'openid_provider/authorize.html', context)
|
||||
return render(request, 'oidc_provider/authorize.html', context)
|
||||
else:
|
||||
path = request.get_full_path()
|
||||
return redirect_to_login(
|
||||
path, settings.get('LOGIN_URL'), REDIRECT_FIELD_NAME)
|
||||
return redirect_to_login(path)
|
||||
|
||||
except (ClientIdError, RedirectUriError) as error:
|
||||
context = {
|
||||
|
@ -49,7 +57,7 @@ class AuthorizeView(View):
|
|||
'description': error.description,
|
||||
}
|
||||
|
||||
return render(request, 'openid_provider/error.html', context)
|
||||
return render(request, 'oidc_provider/error.html', context)
|
||||
|
||||
except (AuthorizeError) as error:
|
||||
uri = error.create_uri(
|
||||
|
@ -70,7 +78,9 @@ class AuthorizeView(View):
|
|||
return HttpResponseRedirect(uri)
|
||||
|
||||
except (AuthorizeError) as error:
|
||||
uri = error.create_uri(authorize.params.redirect_uri, authorize.params.state)
|
||||
uri = error.create_uri(
|
||||
authorize.params.redirect_uri,
|
||||
authorize.params.state)
|
||||
|
||||
return HttpResponseRedirect(uri)
|
||||
|
||||
|
@ -91,6 +101,7 @@ class TokenView(View):
|
|||
except (TokenError) as error:
|
||||
return TokenEndpoint.response(error.create_dict(), status=400)
|
||||
|
||||
|
||||
@require_http_methods(['GET', 'POST'])
|
||||
def userinfo(request):
|
||||
|
||||
|
@ -108,3 +119,12 @@ def userinfo(request):
|
|||
error.code,
|
||||
error.description,
|
||||
error.status)
|
||||
|
||||
|
||||
class ProviderInfoView(View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
dic = ProviderInfoEndpoint.create_response_dic()
|
||||
|
||||
return JsonResponse(dic)
|
|
@ -1,116 +0,0 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openid_provider.models import UserInfo
|
||||
|
||||
|
||||
# Standard Claims
|
||||
# http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
|
||||
class StandardClaims(object):
|
||||
|
||||
__model__ = UserInfo
|
||||
|
||||
def __init__(self, user, scopes):
|
||||
self.user = user
|
||||
self.scopes = scopes
|
||||
|
||||
try:
|
||||
self.model = self.__model__.objects.get(user=self.user)
|
||||
except self.__model__.DoesNotExist:
|
||||
self.model = self.__model__()
|
||||
|
||||
def create_response_dic(self):
|
||||
|
||||
dic = {}
|
||||
|
||||
for scope in self.scopes:
|
||||
|
||||
if scope in self._scopes_registered():
|
||||
dic.update(getattr(self, 'scope_' + scope))
|
||||
|
||||
dic = self._clean_dic(dic)
|
||||
|
||||
return dic
|
||||
|
||||
def _scopes_registered(self):
|
||||
"""
|
||||
Return a list that contains all the scopes registered
|
||||
in the class.
|
||||
"""
|
||||
scopes = []
|
||||
|
||||
for name in self.__class__.__dict__:
|
||||
|
||||
if name.startswith('scope_'):
|
||||
scope = name.split('scope_')[1]
|
||||
scopes.append(scope)
|
||||
|
||||
return scopes
|
||||
|
||||
def _clean_dic(self, dic):
|
||||
"""
|
||||
Clean recursively all empty or None values inside a dict.
|
||||
"""
|
||||
aux_dic = dic.copy()
|
||||
for key, value in dic.iteritems():
|
||||
|
||||
if not value:
|
||||
del aux_dic[key]
|
||||
elif type(value) is dict:
|
||||
aux_dic[key] = clean_dic(value)
|
||||
|
||||
return aux_dic
|
||||
|
||||
@property
|
||||
def scope_profile(self):
|
||||
dic = {
|
||||
'name': self.model.name,
|
||||
'given_name': self.model.given_name,
|
||||
'family_name': self.model.family_name,
|
||||
'middle_name': self.model.middle_name,
|
||||
'nickname': self.model.nickname,
|
||||
'preferred_username': self.model.preferred_username,
|
||||
'profile': self.model.profile,
|
||||
'picture': self.model.picture,
|
||||
'website': self.model.website,
|
||||
'gender': self.model.gender,
|
||||
'birthdate': self.model.birthdate,
|
||||
'zoneinfo': self.model.zoneinfo,
|
||||
'locale': self.model.locale,
|
||||
'updated_at': self.model.updated_at,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
@property
|
||||
def scope_email(self):
|
||||
dic = {
|
||||
'email': self.user.email,
|
||||
'email_verified': self.model.email_verified,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
@property
|
||||
def scope_phone(self):
|
||||
dic = {
|
||||
'phone_number': self.model.phone_number,
|
||||
'phone_number_verified': self.model.phone_number_verified,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
@property
|
||||
def scope_address(self):
|
||||
dic = {
|
||||
'address': {
|
||||
'formatted': self.model.address_formatted,
|
||||
'street_address': self.model.address_street_address,
|
||||
'locality': self.model.address_locality,
|
||||
'region': self.model.address_region,
|
||||
'postal_code': self.model.address_postal_code,
|
||||
'country': self.model.address_country,
|
||||
}
|
||||
}
|
||||
|
||||
return dic
|
|
@ -1,120 +0,0 @@
|
|||
import json
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class Client(models.Model):
|
||||
|
||||
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_id = models.CharField(max_length=255, unique=True)
|
||||
client_secret = models.CharField(max_length=255, unique=True)
|
||||
response_type = models.CharField(max_length=30, choices=RESPONSE_TYPE_CHOICES)
|
||||
|
||||
_redirect_uris = models.TextField(default='')
|
||||
|
||||
def redirect_uris():
|
||||
def fget(self):
|
||||
return self._redirect_uris.splitlines()
|
||||
def fset(self, value):
|
||||
self._redirect_uris = '\n'.join(value)
|
||||
return locals()
|
||||
redirect_uris = property(**redirect_uris())
|
||||
|
||||
@property
|
||||
def default_redirect_uri(self):
|
||||
return self.redirect_uris[0] if self.redirect_uris else ''
|
||||
|
||||
|
||||
class Code(models.Model):
|
||||
|
||||
user = models.ForeignKey(User)
|
||||
client = models.ForeignKey(Client)
|
||||
code = models.CharField(max_length=255, unique=True)
|
||||
expires_at = models.DateTimeField()
|
||||
|
||||
_scope = models.TextField(default='')
|
||||
|
||||
def scope():
|
||||
def fget(self):
|
||||
return self._scope.split()
|
||||
def fset(self, value):
|
||||
self._scope = ' '.join(value)
|
||||
return locals()
|
||||
scope = property(**scope())
|
||||
|
||||
def has_expired(self):
|
||||
return timezone.now() >= self.expires_at
|
||||
|
||||
|
||||
class Token(models.Model):
|
||||
|
||||
user = models.ForeignKey(User)
|
||||
client = models.ForeignKey(Client)
|
||||
access_token = models.CharField(max_length=255, unique=True)
|
||||
expires_at = models.DateTimeField()
|
||||
|
||||
_scope = models.TextField(default='')
|
||||
|
||||
def scope():
|
||||
def fget(self):
|
||||
return self._scope.split()
|
||||
def fset(self, value):
|
||||
self._scope = ' '.join(value)
|
||||
return locals()
|
||||
scope = property(**scope())
|
||||
|
||||
_id_token = models.TextField()
|
||||
|
||||
def id_token():
|
||||
def fget(self):
|
||||
return json.loads(self._id_token)
|
||||
def fset(self, value):
|
||||
self._id_token = json.dumps(value)
|
||||
return locals()
|
||||
id_token = property(**id_token())
|
||||
|
||||
|
||||
class UserInfo(models.Model):
|
||||
|
||||
user = models.OneToOneField(User, primary_key=True)
|
||||
|
||||
given_name = models.CharField(max_length=255, default='')
|
||||
family_name = models.CharField(max_length=255, default='')
|
||||
middle_name = models.CharField(max_length=255, default='')
|
||||
nickname = models.CharField(max_length=255, default='')
|
||||
preferred_username = models.CharField(max_length=255, default='')
|
||||
profile = models.URLField(default='')
|
||||
picture = models.URLField(default='')
|
||||
website = models.URLField(default='')
|
||||
email_verified = models.BooleanField(default=False)
|
||||
gender = models.CharField(max_length=100, default='')
|
||||
birthdate = models.DateField()
|
||||
zoneinfo = models.CharField(max_length=100, default='')
|
||||
locale = models.CharField(max_length=100, default='')
|
||||
phone_number = models.CharField(max_length=255, default='')
|
||||
phone_number_verified = models.BooleanField(default=False)
|
||||
address_formatted = models.CharField(max_length=255, default='')
|
||||
address_street_address = models.CharField(max_length=255, default='')
|
||||
address_locality = models.CharField(max_length=255, default='')
|
||||
address_region = models.CharField(max_length=255, default='')
|
||||
address_postal_code = models.CharField(max_length=255, default='')
|
||||
address_country = models.CharField(max_length=255, default='')
|
||||
updated_at = models.DateTimeField()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
name = ''
|
||||
if self.given_name:
|
||||
name = self.given_name
|
||||
if self.family_name:
|
||||
name = name + ' ' + self.family_name
|
||||
|
||||
return name
|
|
@ -1,27 +1,83 @@
|
|||
from django.conf import settings
|
||||
|
||||
|
||||
# Here goes all the package default settings.
|
||||
default_settings = {
|
||||
'DOP_CODE_EXPIRE': 60 * 10, # 10 min.
|
||||
'DOP_IDTOKEN_EXPIRE': 60 * 10, # 10 min.
|
||||
'DOP_TOKEN_EXPIRE': 60 * 60, # 1 hour.
|
||||
'LOGIN_URL': None,
|
||||
'SITE_URL': None,
|
||||
}
|
||||
class DefaultSettings(object):
|
||||
|
||||
@property
|
||||
def LOGIN_URL(self):
|
||||
"""
|
||||
REQUIRED.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def SITE_URL(self):
|
||||
"""
|
||||
REQUIRED.
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def OIDC_AFTER_USERLOGIN_HOOK(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
def default_hook_func(request, user, client):
|
||||
return None
|
||||
|
||||
return default_hook_func
|
||||
|
||||
@property
|
||||
def OIDC_CODE_EXPIRE(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
return 60*10
|
||||
|
||||
@property
|
||||
def OIDC_EXTRA_SCOPE_CLAIMS(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
from oidc_provider.lib.claims import AbstractScopeClaims
|
||||
|
||||
return AbstractScopeClaims
|
||||
|
||||
@property
|
||||
def OIDC_IDTOKEN_EXPIRE(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
return 60*10
|
||||
|
||||
@property
|
||||
def OIDC_IDTOKEN_SUB_GENERATOR(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
def default_sub_generator(user):
|
||||
return user.id
|
||||
|
||||
return default_sub_generator
|
||||
|
||||
@property
|
||||
def OIDC_TOKEN_EXPIRE(self):
|
||||
"""
|
||||
OPTIONAL.
|
||||
"""
|
||||
return 60*60
|
||||
|
||||
default_settings = DefaultSettings()
|
||||
|
||||
def get(name):
|
||||
"""
|
||||
'''
|
||||
Helper function to use inside the package.
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
'''
|
||||
try:
|
||||
value = default_settings[name]
|
||||
value = getattr(default_settings, name)
|
||||
value = getattr(settings, name)
|
||||
except AttributeError:
|
||||
if value == None:
|
||||
raise Exception('You must set ' + name + ' in your settings.')
|
||||
|
||||
return value
|
||||
return value
|
|
@ -1,28 +0,0 @@
|
|||
<h1>Request for Permission</h1>
|
||||
|
||||
<p>Client <strong>{{ client.name }}</strong> would like to access this information of you ...</p>
|
||||
|
||||
<form method="post" action="{% url 'openid_provider:authorize' %}">
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
<input name="client_id" type="hidden" value="{{ params.client_id }}" />
|
||||
<input name="redirect_uri" type="hidden" value="{{ params.redirect_uri }}" />
|
||||
<input name="response_type" type="hidden" value="{{ params.response_type }}" />
|
||||
<input name="scope" type="hidden" value="{{ params.scope_str }}" />
|
||||
<input name="state" type="hidden" value="{{ params.state }}" />
|
||||
|
||||
<ul>
|
||||
{% for scope in params.scope %}
|
||||
{% if scope != 'openid' %}
|
||||
<li>{{ scope | capfirst }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<input type="submit" value="Decline" />
|
||||
<input name="allow" type="submit" value="Authorize" />
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -1,12 +0,0 @@
|
|||
from django.conf.urls import patterns, include, url
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from openid_provider.views import *
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^authorize/$', AuthorizeView.as_view(), name='authorize'),
|
||||
url(r'^token/$', csrf_exempt(TokenView.as_view()), name='token'),
|
||||
url(r'^userinfo/$', csrf_exempt(userinfo), name='userinfo'),
|
||||
|
||||
)
|
17
setup.py
17
setup.py
|
@ -8,14 +8,17 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
|
|||
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
||||
|
||||
setup(
|
||||
name='django-openid-provider',
|
||||
version='0.1',
|
||||
packages=['openid_provider'],
|
||||
name='django-oidc-provider',
|
||||
version='0.0.5',
|
||||
packages=[
|
||||
'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints',
|
||||
'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/migrations',
|
||||
],
|
||||
include_package_data=True,
|
||||
license='MIT License',
|
||||
description='A simple OpenID Connect Provider implementation for Djangonauts.',
|
||||
description='OpenID Connect Provider implementation for Django.',
|
||||
long_description=README,
|
||||
url='http://github.com/juanifioren/django-openid-provider',
|
||||
url='http://github.com/juanifioren/django-oidc-provider',
|
||||
author='Juan Ignacio Fiorentino',
|
||||
author_email='juanifioren@gmail.com',
|
||||
classifiers=[
|
||||
|
@ -31,6 +34,6 @@ setup(
|
|||
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
||||
],
|
||||
install_requires=[
|
||||
'pyjwt==0.3.1',
|
||||
'pyjwt==1.1.0',
|
||||
],
|
||||
)
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue