Initial commit.
This commit is contained in:
commit
9def141582
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Byte-compiled python files
|
||||
|
||||
__pycache__/
|
||||
*.py[cod]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Juan Ignacio Fiorentino
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
|
@ -0,0 +1,3 @@
|
|||
include LICENSE
|
||||
include README.rst
|
||||
recursive-include openid_provider/templates *
|
100
README.rst
Normal file
100
README.rst
Normal file
|
@ -0,0 +1,100 @@
|
|||
######################
|
||||
Django OpenID Provider
|
||||
######################
|
||||
|
||||
************
|
||||
Installation
|
||||
************
|
||||
|
||||
Install the package using pip.
|
||||
|
||||
Add it to your proyect 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 to your proyect.
|
||||
|
||||
.. code:: python
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# ...
|
||||
url(r'^openid/', include('openid_provider.urls', namespace='openid_provider')),
|
||||
# ...
|
||||
)
|
||||
|
||||
Finally, add a login view and ensure that has the same url defined in `LOGIN_URL` setting.
|
||||
|
||||
See: https://docs.djangoproject.com/en/1.7/ref/settings/#login-url
|
||||
|
||||
********************
|
||||
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', client_type='public', grant_type='authorization_code', response_type='code', _redirect_uris='http://example.com/')
|
||||
>>> from django.contrib.auth.models import User
|
||||
>>> c.user = User.objects.all()[0]
|
||||
>>> c.save()
|
||||
|
||||
*******************
|
||||
/authorize endpoint
|
||||
*******************
|
||||
|
||||
.. 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]
|
0
openid_provider/__init__.py
Normal file
0
openid_provider/__init__.py
Normal file
0
openid_provider/lib/__init__.py
Normal file
0
openid_provider/lib/__init__.py
Normal file
110
openid_provider/lib/errors.py
Normal file
110
openid_provider/lib/errors.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
import urllib
|
||||
|
||||
|
||||
class RedirectUriError(Exception):
|
||||
|
||||
error = None
|
||||
description = 'The request fails due to a missing, invalid, or mismatching redirection URI (redirect_uri).'
|
||||
|
||||
|
||||
class ClientIdError(Exception):
|
||||
|
||||
error = None
|
||||
description = 'The client identifier (client_id) is missing or invalid.'
|
||||
|
||||
class MissingScopeError(Exception):
|
||||
|
||||
error = 'openid scope'
|
||||
description = 'The openid scope value is missing.'
|
||||
|
||||
|
||||
class AuthorizeError(Exception):
|
||||
|
||||
_errors = {
|
||||
# Oauth2 errors.
|
||||
# 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',
|
||||
'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',
|
||||
'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',
|
||||
# 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',
|
||||
'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',
|
||||
'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_object': 'The request parameter contains an invalid Request Object',
|
||||
'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',
|
||||
'registration_not_supported': 'The provider does not support use of the registration parameter',
|
||||
}
|
||||
|
||||
def __init__(self, redirect_uri, error):
|
||||
|
||||
self.error = error
|
||||
self.description = self._errors.get(error)
|
||||
self.redirect_uri = redirect_uri
|
||||
|
||||
def create_uri(self, redirect_uri, state):
|
||||
|
||||
description = urllib.quote(self.description)
|
||||
|
||||
uri = '{0}?error={1}&error_description={2}'.format(redirect_uri, self.error, description)
|
||||
|
||||
# Add state if present.
|
||||
uri = uri + '&state={0}'.format(state) if state else ''
|
||||
|
||||
return uri
|
||||
|
||||
@property
|
||||
def response(self):
|
||||
pass
|
||||
|
||||
|
||||
class TokenError(Exception):
|
||||
|
||||
_errors = {
|
||||
# Oauth2 errors.
|
||||
# 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_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',
|
||||
'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',
|
||||
}
|
||||
|
||||
def __init__(self, error):
|
||||
|
||||
self.error = error
|
||||
self.description = self._errors.get(error)
|
||||
|
||||
def create_dict(self):
|
||||
|
||||
dic = {
|
||||
'error': self.error,
|
||||
'error_description': self.description,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
class UserInfoError(Exception):
|
||||
|
||||
_errors = {
|
||||
# Oauth2 errors.
|
||||
# https://tools.ietf.org/html/rfc6750#section-3.1
|
||||
'invalid_request': ('The request is otherwise malformed', 400),
|
||||
'invalid_token': ('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),
|
||||
}
|
||||
|
||||
def __init__(self, code):
|
||||
|
||||
self.code = code
|
||||
error_tuple = self._errors.get(code, ('', ''))
|
||||
self.description = error_tuple[0]
|
||||
self.status = error_tuple[1]
|
0
openid_provider/lib/grants/__init__.py
Normal file
0
openid_provider/lib/grants/__init__.py
Normal file
260
openid_provider/lib/grants/authorization_code.py
Normal file
260
openid_provider/lib/grants/authorization_code.py
Normal file
|
@ -0,0 +1,260 @@
|
|||
from datetime import timedelta
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.utils import timezone
|
||||
import urllib
|
||||
import uuid
|
||||
import json
|
||||
import jwt
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from openid_provider.models import *
|
||||
from openid_provider.lib.errors import *
|
||||
from openid_provider.lib.scopes import *
|
||||
|
||||
|
||||
class AuthorizeEndpoint(object):
|
||||
|
||||
def __init__(self, request):
|
||||
|
||||
self.request = request
|
||||
self.extract_params()
|
||||
|
||||
def extract_params(self):
|
||||
|
||||
query_dict = self.request.POST if self.request.method == 'POST' else self.request.GET
|
||||
|
||||
class Params(object): pass
|
||||
|
||||
Params.client_id = query_dict.get('client_id', '')
|
||||
Params.redirect_uri = query_dict.get('redirect_uri', '')
|
||||
Params.response_type = query_dict.get('response_type', '')
|
||||
Params.scope = query_dict.get('scope', '')
|
||||
Params.state = query_dict.get('state', '')
|
||||
|
||||
self.params = Params
|
||||
|
||||
def validate_params(self):
|
||||
|
||||
if not self.params.redirect_uri:
|
||||
raise RedirectUriError()
|
||||
|
||||
if not ('openid' in self.params.scope.split()):
|
||||
raise AuthorizeError(self.params.redirect_uri, 'invalid_scope')
|
||||
|
||||
try:
|
||||
self.client = Client.objects.get(client_id=self.params.client_id)
|
||||
|
||||
if not (self.params.redirect_uri in self.client.redirect_uris):
|
||||
raise RedirectUriError()
|
||||
|
||||
if not (self.params.response_type == 'code'):
|
||||
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type')
|
||||
|
||||
except Client.DoesNotExist:
|
||||
raise ClientIdError()
|
||||
|
||||
def create_response_uri(self, allow):
|
||||
|
||||
if not allow:
|
||||
raise AuthorizeError(self.params.redirect_uri, 'access_denied')
|
||||
|
||||
try:
|
||||
self.validate_params()
|
||||
|
||||
code = Code()
|
||||
code.user = self.request.user
|
||||
code.client = self.client
|
||||
code.code = uuid.uuid4().hex
|
||||
code.expires_at = timezone.now() + timedelta(seconds=60*10)
|
||||
code.scope = self.params.scope
|
||||
|
||||
code.save()
|
||||
except:
|
||||
raise AuthorizeError(self.params.redirect_uri, 'server_error')
|
||||
|
||||
uri = self.params.redirect_uri + '?code={0}'.format(code.code)
|
||||
|
||||
# Add state if present.
|
||||
uri = uri + ('&state={0}'.format(self.params.state) if self.params.state else '')
|
||||
|
||||
return uri
|
||||
|
||||
class TokenEndpoint(object):
|
||||
|
||||
def __init__(self, request):
|
||||
|
||||
self.request = request
|
||||
self.extract_params()
|
||||
|
||||
def extract_params(self):
|
||||
|
||||
query_dict = self.request.POST
|
||||
|
||||
class Params(object): pass
|
||||
|
||||
Params.client_id = query_dict.get('client_id', '')
|
||||
Params.client_secret = query_dict.get('client_secret', '')
|
||||
Params.redirect_uri = urllib.unquote(query_dict.get('redirect_uri', ''))
|
||||
Params.grant_type = query_dict.get('grant_type', '')
|
||||
Params.code = query_dict.get('code', '')
|
||||
Params.state = query_dict.get('state', '')
|
||||
|
||||
self.params = Params
|
||||
|
||||
def validate_params(self):
|
||||
|
||||
if not (self.params.grant_type == 'authorization_code'):
|
||||
raise TokenError('unsupported_grant_type')
|
||||
|
||||
try:
|
||||
self.client = Client.objects.get(client_id=self.params.client_id)
|
||||
|
||||
if not (self.client.client_secret == self.params.client_secret):
|
||||
raise TokenError('invalid_client')
|
||||
|
||||
if not (self.params.redirect_uri in self.client.redirect_uris):
|
||||
raise TokenError('invalid_client')
|
||||
|
||||
self.code = Code.objects.get(code=self.params.code)
|
||||
|
||||
if not (self.code.client == self.client) and not self.code.has_expired():
|
||||
raise TokenError('invalid_grant')
|
||||
|
||||
except Client.DoesNotExist:
|
||||
raise TokenError('invalid_client')
|
||||
|
||||
except Code.DoesNotExist:
|
||||
raise TokenError('invalid_grant')
|
||||
|
||||
def create_response_dic(self):
|
||||
|
||||
expires_in = 60*60 # TODO: Probably add into settings
|
||||
|
||||
token = Token()
|
||||
token.user = self.code.user
|
||||
token.client = self.code.client
|
||||
token.access_token = uuid.uuid4().hex
|
||||
|
||||
id_token_dic = self.generate_id_token_dic()
|
||||
token.id_token = id_token_dic
|
||||
|
||||
token.refresh_token = uuid.uuid4().hex
|
||||
token.expires_at = timezone.now() + timedelta(seconds=expires_in)
|
||||
token.scope = self.code.scope
|
||||
|
||||
token.save()
|
||||
|
||||
self.code.delete()
|
||||
|
||||
id_token = jwt.encode(id_token_dic, self.client.client_secret)
|
||||
|
||||
dic = {
|
||||
'access_token': token.access_token,
|
||||
'token_type': 'bearer',
|
||||
'expires_in': expires_in,
|
||||
'id_token': id_token,
|
||||
# TODO: 'refresh_token': token.refresh_token,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
def generate_id_token_dic(self):
|
||||
|
||||
expires_in = 60*10
|
||||
|
||||
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(self.code.user.last_login.timetuple())
|
||||
|
||||
dic = {
|
||||
'iss': 'https://localhost:8000', # TODO: this should not be hardcoded.
|
||||
'sub': self.code.user.id,
|
||||
'aud': self.client.client_id,
|
||||
'exp': exp_time,
|
||||
'iat': iat_time,
|
||||
'auth_time': user_auth_time,
|
||||
}
|
||||
|
||||
return dic
|
||||
|
||||
@classmethod
|
||||
def response(self, dic, status=200):
|
||||
|
||||
response = JsonResponse(dic, status=status)
|
||||
response['Cache-Control'] = 'no-store'
|
||||
response['Pragma'] = 'no-cache'
|
||||
|
||||
return response
|
||||
|
||||
class UserInfoEndpoint(object):
|
||||
|
||||
def __init__(self, request):
|
||||
|
||||
self.request = request
|
||||
self.extract_params()
|
||||
|
||||
def extract_params(self):
|
||||
|
||||
# TODO: Add other ways of passing access token
|
||||
# http://tools.ietf.org/html/rfc6750#section-2
|
||||
|
||||
class Params(object): pass
|
||||
|
||||
Params.access_token = self._get_access_token()
|
||||
|
||||
self.params = Params
|
||||
|
||||
def validate_params(self):
|
||||
|
||||
try:
|
||||
self.token = Token.objects.get(access_token=self.params.access_token)
|
||||
|
||||
except Token.DoesNotExist:
|
||||
raise UserInfoError('invalid_token')
|
||||
|
||||
def _get_access_token(self):
|
||||
|
||||
# Using Authorization Request Header Field
|
||||
# http://tools.ietf.org/html/rfc6750#section-2.1
|
||||
|
||||
auth_header = self.request.META.get('HTTP_AUTHORIZATION', '')
|
||||
|
||||
if re.compile('^Bearer\s{1}.+$').match(auth_header):
|
||||
access_token = auth_header.split()[1]
|
||||
else:
|
||||
access_token = ''
|
||||
|
||||
return access_token
|
||||
|
||||
def create_response_dic(self):
|
||||
|
||||
dic = {
|
||||
'sub': self.token.id_token.get('sub'),
|
||||
}
|
||||
|
||||
standard_claims = StandardClaims(self.token.user, self.token.scope.split())
|
||||
|
||||
dic.update(standard_claims.response_dic)
|
||||
|
||||
return dic
|
||||
|
||||
@classmethod
|
||||
def response(self, dic):
|
||||
|
||||
response = JsonResponse(dic, status=200)
|
||||
response['Cache-Control'] = 'no-store'
|
||||
response['Pragma'] = 'no-cache'
|
||||
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
def error_response(self, code, description, status):
|
||||
|
||||
response = HttpResponse(status=status)
|
||||
response['WWW-Authenticate'] = 'error="{0}", error_description="{1}"'.format(code, description)
|
||||
|
||||
return response
|
116
openid_provider/lib/scopes.py
Normal file
116
openid_provider/lib/scopes.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
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__()
|
||||
|
||||
@property
|
||||
def 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
|
0
openid_provider/migrations/__init__.py
Normal file
0
openid_provider/migrations/__init__.py
Normal file
115
openid_provider/models.py
Normal file
115
openid_provider/models.py
Normal file
|
@ -0,0 +1,115 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
import json
|
||||
|
||||
|
||||
class Client(models.Model):
|
||||
|
||||
CLIENT_TYPE_CHOICES = [
|
||||
('confidential', 'Confidential'),
|
||||
#('public', 'Public'),
|
||||
]
|
||||
|
||||
GRANT_TYPE_CHOICES = [
|
||||
('authorization_code', 'Authorization Code Flow'),
|
||||
#('implicit', 'Implicit Flow'),
|
||||
]
|
||||
|
||||
RESPONSE_TYPE_CHOICES = [
|
||||
('code', 'Authorization Code Flow'),
|
||||
#('id_token', 'Implicit Flow'),
|
||||
#('id_token token', 'Implicit Flow'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=100, default='')
|
||||
user = models.ForeignKey(User)
|
||||
client_id = models.CharField(max_length=255, unique=True)
|
||||
client_secret = models.CharField(max_length=255, unique=True)
|
||||
client_type = models.CharField(max_length=20, choices=CLIENT_TYPE_CHOICES)
|
||||
grant_type = models.CharField(max_length=30, choices=GRANT_TYPE_CHOICES)
|
||||
response_type = models.CharField(max_length=30, choices=RESPONSE_TYPE_CHOICES)
|
||||
_redirect_uris = models.TextField()
|
||||
_scope = models.TextField() # TODO: add getter and setter for this.
|
||||
|
||||
@property
|
||||
def redirect_uris(self):
|
||||
if self._redirect_uris:
|
||||
return self._redirect_uris.split()
|
||||
return []
|
||||
|
||||
@property
|
||||
def default_redirect_uri(self):
|
||||
return self.redirect_uris[0]
|
||||
|
||||
@property
|
||||
def scope(self):
|
||||
if self._scopes:
|
||||
return self._scopes.split()
|
||||
return []
|
||||
|
||||
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() # TODO: add getter and setter for this.
|
||||
|
||||
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)
|
||||
_id_token = models.TextField()
|
||||
refresh_token = models.CharField(max_length=255, unique=True)
|
||||
expires_at = models.DateTimeField()
|
||||
scope = models.TextField() # TODO: add getter and setter for this.
|
||||
|
||||
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
|
47
openid_provider/templates/openid_provider/authorize.html
Normal file
47
openid_provider/templates/openid_provider/authorize.html
Normal file
|
@ -0,0 +1,47 @@
|
|||
{% extends "openid_provider/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Request for Permission</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>Client {{ client.name }} 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 }}" />
|
||||
<input name="state" type="hidden" value="{{ params.state }}" />
|
||||
|
||||
<ul class="list-group">
|
||||
{% for scope in params.scope.split %}
|
||||
{% if scope != 'openid' %}
|
||||
<li class="list-group-item">{{ scope | capfirst }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="btn-group btn-group-justified" role="group" aria-label="Justified button group">
|
||||
<div class="btn-group" role="group">
|
||||
<input type="submit" class="btn btn-danger" value="Decline" />
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<input name="allow" type="submit" class="btn btn-success" value="Authorize" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
55
openid_provider/templates/openid_provider/base.html
Normal file
55
openid_provider/templates/openid_provider/base.html
Normal file
|
@ -0,0 +1,55 @@
|
|||
{% load staticfiles %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OpenID Provider</title>
|
||||
|
||||
<link href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.1/flatly/bootstrap.min.css" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
body {
|
||||
padding-top: 90px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[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>
|
||||
|
||||
<!-- Fixed navbar -->
|
||||
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="#">OpenID Provider</a>
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="#">{{ user.email }}</a></li>
|
||||
</ul>
|
||||
</div><!--/.nav-collapse -->
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
12
openid_provider/urls.py
Normal file
12
openid_provider/urls.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from django.conf.urls import patterns, include, url
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
||||
url(r'^authorize/$', views.AuthorizeView.as_view(), name='authorize'),
|
||||
url(r'^token/$', csrf_exempt(views.TokenView.as_view()), name='token'),
|
||||
url(r'^userinfo/$', csrf_exempt(views.userinfo), name='userinfo'),
|
||||
|
||||
)
|
90
openid_provider/views.py
Normal file
90
openid_provider/views.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic import View
|
||||
import urllib
|
||||
from .lib.errors import *
|
||||
from .lib.grants.authorization_code import *
|
||||
|
||||
|
||||
class AuthorizeView(View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
authorize = AuthorizeEndpoint(request)
|
||||
|
||||
try:
|
||||
authorize.validate_params()
|
||||
|
||||
if request.user.is_authenticated():
|
||||
data = {
|
||||
'params': authorize.params,
|
||||
'client': authorize.client,
|
||||
}
|
||||
|
||||
return render(request, 'openid_provider/authorize.html', data)
|
||||
else:
|
||||
next = urllib.quote(request.get_full_path())
|
||||
login_url = settings.LOGIN_URL + '?next={0}'.format(next)
|
||||
|
||||
return HttpResponseRedirect(login_url)
|
||||
|
||||
except (ClientIdError, RedirectUriError) as error:
|
||||
return HttpResponse(error.description)
|
||||
|
||||
except (AuthorizeError) as error:
|
||||
uri = error.create_uri(authorize.params.redirect_uri, authorize.params.state)
|
||||
|
||||
return HttpResponseRedirect(uri)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
authorize = AuthorizeEndpoint(request)
|
||||
|
||||
allow = True if request.POST.get('allow') else False
|
||||
|
||||
try:
|
||||
uri = authorize.create_response_uri(allow)
|
||||
|
||||
return HttpResponseRedirect(uri)
|
||||
|
||||
except (AuthorizeError) as error:
|
||||
uri = error.create_uri(authorize.params.redirect_uri, authorize.params.state)
|
||||
|
||||
return HttpResponseRedirect(uri)
|
||||
|
||||
class TokenView(View):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
token = TokenEndpoint(request)
|
||||
|
||||
try:
|
||||
token.validate_params()
|
||||
|
||||
dic = token.create_response_dic()
|
||||
|
||||
return TokenEndpoint.response(dic)
|
||||
|
||||
except (TokenError) as error:
|
||||
return TokenEndpoint.response(error.create_dict(), status=400)
|
||||
|
||||
@require_http_methods(['GET', 'POST'])
|
||||
def userinfo(request):
|
||||
|
||||
userinfo = UserInfoEndpoint(request)
|
||||
|
||||
try:
|
||||
userinfo.validate_params()
|
||||
|
||||
dic = userinfo.create_response_dic()
|
||||
|
||||
return UserInfoEndpoint.response(dic)
|
||||
|
||||
except (UserInfoError) as error:
|
||||
return UserInfoEndpoint.error_response(
|
||||
error.code,
|
||||
error.description,
|
||||
error.status)
|
33
setup.py
Normal file
33
setup.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import os
|
||||
from setuptools import setup
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
|
||||
README = readme.read()
|
||||
|
||||
# allow setup.py to be run from any path
|
||||
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'],
|
||||
include_package_data=True,
|
||||
license='MIT License',
|
||||
description='A simple OpenID Connect Provider implementation for Djangonauts.',
|
||||
long_description=README,
|
||||
url='http://github.com/juanifioren/django-openid-provider',
|
||||
author='Juan Ignacio Fiorentino',
|
||||
author_email='juanifioren@gmail.com',
|
||||
classifiers=[
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
||||
],
|
||||
)
|
Loading…
Reference in a new issue