Add Implicit flow support.

This commit is contained in:
juanifioren 2015-01-08 17:55:24 -03:00
parent a95d41a386
commit c24f0ccc29
12 changed files with 398 additions and 283 deletions

View file

@ -2,10 +2,12 @@
Django OpenID Provider
######################
**This project is in ALFA version and is rapidly changing. DO NOT USE IT FOR PRODUCTION SITES.**
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.
- This just cover the ``authorization_code`` flow, no support for ``implicit`` flow at this moment.
- This cover ``authorization_code`` flow and ``implicit`` flow, NO support for ``hibrid`` flow at this moment.
- Only support for requesting Claims using Scope Values.
************

View file

@ -0,0 +1,141 @@
from datetime import timedelta
from django.utils import timezone
from openid_provider.lib.errors import *
from openid_provider.lib.utils.params import *
from openid_provider.lib.utils.token import *
from openid_provider.models import *
import uuid
class AuthorizeEndpoint(object):
def __init__(self, request):
self.request = request
self.params = Params
# 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._extract_params()
# Determine which flow to use.
if self.params.response_type in ['code']:
self.grant_type = 'authorization_code'
elif self.params.response_type in ['id_token', 'id_token token']:
self.grant_type = 'implicit'
self._extract_implicit_params()
else:
self.grant_type = None
def _extract_params(self):
'''
Get all the params used by the Authorization Code Flow
(and also for the Implicit).
See: http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
'''
self.params.client_id = self.query_dict.get('client_id', '')
self.params.redirect_uri = self.query_dict.get('redirect_uri', '')
self.params.response_type = self.query_dict.get('response_type', '')
self.params.scope = self.query_dict.get('scope', '')
self.params.state = self.query_dict.get('state', '')
def _extract_implicit_params(self):
'''
Get specific params used by the Implicit Flow.
See: http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest
'''
self.params.nonce = self.query_dict.get('nonce', '')
def is_code_flow(self):
'''
True if the client is using Authorization Code Flow.
Return a boolean.
'''
return self.grant_type == 'authorization_code'
def is_implicit_flow(self):
'''
True if the client is using Implicit Flow.
Return a boolean.
'''
return self.grant_type == 'implicit'
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', self.grant_type)
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.grant_type) or not (self.params.response_type == self.client.response_type):
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type', self.grant_type)
except Client.DoesNotExist:
raise ClientIdError()
def create_response_uri(self, allow):
if not allow:
raise AuthorizeError(self.params.redirect_uri, 'access_denied', self.grant_type)
try:
self.validate_params()
if self.is_code_flow():
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) # TODO: Add this into settings.
code.scope = self.params.scope
code.save()
uri = self.params.redirect_uri + '?code={0}'.format(code.code)
else:
id_token_dic = create_id_token_dic(
self.request.user,
'http://localhost:8000', # TODO: Add this into settings.
self.client.client_id)
token = create_token(
user=self.request.user,
client=self.client,
id_token_dic=id_token_dic,
scope=self.params.scope)
# Store the token.
token.save()
id_token = encode_id_token(id_token_dic, self.client.client_secret)
# TODO: Check if response_type is 'id_token token' and
# 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)
except:
raise AuthorizeError(self.params.redirect_uri, 'server_error', self.grant_type)
# Add state if present.
uri = uri + ('&state={0}'.format(self.params.state) if self.params.state else '')
return uri

View file

@ -0,0 +1,92 @@
from django.http import JsonResponse
from openid_provider.lib.errors import *
from openid_provider.lib.utils.params import *
from openid_provider.lib.utils.token import *
from openid_provider.models import *
import urllib
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', '')
self.params.client_secret = query_dict.get('client_secret', '')
self.params.redirect_uri = urllib.unquote(query_dict.get('redirect_uri', ''))
self.params.grant_type = query_dict.get('grant_type', '')
self.params.code = query_dict.get('code', '')
self.params.state = query_dict.get('state', '')
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):
id_token_dic = create_id_token_dic(
self.code.user,
'http://localhost:8000', # TODO: Add this into settings.
self.client.client_id)
token = create_token(
user=self.code.user,
client=self.code.client,
id_token_dic=id_token_dic,
scope=self.code.scope)
# Store the token.
token.save()
# We don't need to store the code anymore.
self.code.delete()
id_token = encode_id_token(id_token_dic, self.client.client_secret)
dic = {
'access_token': token.access_token,
'token_type': 'bearer',
'expires_in': 60*60, # TODO: Add this into settings.
'id_token': id_token,
}
return dic
@classmethod
def response(self, dic, status=200):
'''
Create and return a response object.
'''
response = JsonResponse(dic, status=status)
response['Cache-Control'] = 'no-store'
response['Pragma'] = 'no-cache'
return response

View file

@ -0,0 +1,79 @@
from django.http import HttpResponse, JsonResponse
from openid_provider.lib.errors import *
from openid_provider.lib.scopes import *
from openid_provider.lib.utils.params import *
from openid_provider.models import *
import re
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()
def _get_access_token(self):
'''
Get the access token using Authorization Request Header Field method.
See: http://tools.ietf.org/html/rfc6750#section-2.1
Return a string.
'''
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 validate_params(self):
try:
self.token = Token.objects.get(access_token=self.params.access_token)
except Token.DoesNotExist:
raise UserInfoError('invalid_token')
def create_response_dic(self):
'''
Create a diccionary with all the requested claims about the End-User.
See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
Return a diccionary.
'''
dic = {
'sub': self.token.id_token.get('sub'),
}
standard_claims = StandardClaims(self.token.user, self.token.scope.split())
dic.update(standard_claims.create_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

View file

@ -43,17 +43,25 @@ class AuthorizeError(Exception):
'registration_not_supported': 'The provider does not support use of the registration parameter',
}
def __init__(self, redirect_uri, error):
def __init__(self, redirect_uri, error, grant_type):
self.error = error
self.description = self._errors.get(error)
self.redirect_uri = redirect_uri
self.grant_type = grant_type
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)
# 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(
redirect_uri,
hash_or_question,
self.error,
description)
# Add state if present.
uri = uri + ('&state={0}'.format(state) if state else '')

View file

@ -1,269 +0,0 @@
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):
'''
Create and return a response object.
'''
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):
'''
Get the access token using Authorization Request Header Field method.
See: http://tools.ietf.org/html/rfc6750#section-2.1
Return a string.
'''
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):
'''
Create a diccionary with all the requested claims about the End-User.
See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
Return a diccionary.
'''
dic = {
'sub': self.token.id_token.get('sub'),
}
standard_claims = StandardClaims(self.token.user, self.token.scope.split())
dic.update(standard_claims.create_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

View file

View file

@ -0,0 +1,2 @@
class Params(object):
pass

View file

@ -0,0 +1,64 @@
from datetime import timedelta
from django.utils import timezone
import jwt
from openid_provider.models import *
import time
import uuid
def create_id_token_dic(user, iss, aud):
'''
Receives a user object, iss (issuer) and aud (audience).
Then creates the id_token dic.
See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken
Return a dic.
'''
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(user.last_login.timetuple())
dic = {
'iss': iss, # TODO: this should not be hardcoded.
'sub': user.id,
'aud': aud,
'exp': exp_time,
'iat': iat_time,
'auth_time': user_auth_time,
}
return dic
def encode_id_token(id_token_dic, client_secret):
'''
Represent the ID Token as a JSON Web Token (JWT).
Return a hash.
'''
id_token_hash = jwt.encode(id_token_dic, client_secret)
return id_token_hash
def create_token(user, client, id_token_dic, scope):
'''
Create and populate a Token object.
Return a Token object.
'''
token = Token()
token.user = user
token.client = client
token.access_token = uuid.uuid4().hex
token.id_token = id_token_dic
token.refresh_token = uuid.uuid4().hex
token.expires_at = timezone.now() + timedelta(seconds=60*60) # TODO: Add this into settings.
token.scope = scope
return token

View file

@ -8,18 +8,13 @@ class Client(models.Model):
CLIENT_TYPE_CHOICES = [
('confidential', 'Confidential'),
#('public', 'Public'),
]
GRANT_TYPE_CHOICES = [
('authorization_code', 'Authorization Code Flow'),
#('implicit', 'Implicit Flow'),
('public', 'Public'),
]
RESPONSE_TYPE_CHOICES = [
('code', 'Authorization Code Flow'),
#('id_token', 'Implicit Flow'),
#('id_token token', 'Implicit Flow'),
('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='')
@ -27,7 +22,6 @@ class Client(models.Model):
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)
# TODO: Need to be implemented.

View file

@ -6,7 +6,9 @@ from django.views.decorators.http import require_http_methods
from django.views.generic import View
import urllib
from openid_provider.lib.errors import *
from openid_provider.lib.grants.authorization_code import *
from openid_provider.lib.endpoints.authorize import *
from openid_provider.lib.endpoints.token import *
from openid_provider.lib.endpoints.userinfo import *
class AuthorizeView(View):