updating library...

This commit is contained in:
niccolasmendoza@gmail.com 2015-06-08 11:55:54 -03:00
commit c48832b8cd
54 changed files with 1969 additions and 562 deletions

6
.gitignore vendored
View file

@ -1,3 +1,7 @@
__pycache__/
build/
dist/
*.py[cod]
*.egg-info
*.egg-info
.ropeproject
src/

56
CHANGELOG.md Normal file
View 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
View 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)

View file

@ -1,3 +1,3 @@
include LICENSE
include README.rst
recursive-include openid_provider/templates *
recursive-include oidc_provider/templates *

View file

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

@ -0,0 +1 @@
*.sqlite3

27
example_project/README.md Normal file
View 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
View 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)

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

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

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

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

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

View file

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

View file

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

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

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

View file

@ -0,0 +1,2 @@
django==1.8
django-oidc-provider==0.0.5

9
oidc_provider/admin.py Normal file
View 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
View 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

View file

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

View 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

View file

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

View file

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

View file

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

View 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

View file

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

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

View file

143
oidc_provider/models.py Normal file
View 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
View 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

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

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

View file

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

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

View 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".')

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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