Skip to content

Commit

Permalink
Add dynamic registration endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Baptiste committed Apr 5, 2019
1 parent a7b07e2 commit f2fd51b
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 4 deletions.
115 changes: 115 additions & 0 deletions oidc_provider/lib/endpoints/register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import json
import logging
import re

from django.http import HttpResponse
from django.http import JsonResponse

from oidc_provider import settings
from oidc_provider.lib.errors import RegisterError
from oidc_provider.lib.utils.register import create_client
from oidc_provider.models import Token

logger = logging.getLogger(__name__)


class RegisterEndpoint(object):

def __init__(self, request):
self.request = request
self.params = {}
self._extract_params()

def _extract_params(self):
jsonStr = self.request.body

paramDic = json.loads(jsonStr)

self.params['name'] = paramDic.get('client_name', None)
if 'redirect_uris' in paramDic:
self.params['redirect_uris'] = '\n'.join(paramDic['redirect_uris'])
else:
self.params['redirect_uris'] = None
if 'response_types' in paramDic:
self.params['response_types'] = paramDic['response_types']
else:
self.params['response_types'] = ['code']

self.params['access_token'] = None
header_dic = self.request.META
if 'HTTP_AUTHORIZATION' in header_dic:
auth_header = header_dic['HTTP_AUTHORIZATION']
if re.compile('^Bearer\\s.+$').match(auth_header):
self.params['access_token'] = auth_header.split()[1]

def validate_params(self):
# Make sure appropriate parameters are there
# See: https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
# We want client name (optional), and redirect URIs (required).
# "Response type" will default to code (Authorization code flow)

# Is this endpoint enabled in the configuration?
if not settings.get('OIDC_REGISTRATION_ENDPOINT_ENABLED'):
raise RegisterError('invalid_request')

# If authorization is required, has user provided valid access token?
if settings.get('OIDC_REGISTRATION_ENDPOINT_REQ_TOKEN'):
if self.params['access_token'] is None:
raise RegisterError('invalid_request')
# Check whether token is valid
try:
self.token = Token.objects.get(access_token=self.params['access_token'])

if self.token.has_expired():
logger.error('[Register] Token has expired: %s', self.params['access_token'])
raise RegisterError('invalid_token')

if not ('openid' in self.token.scope):
logger.error('[Register] Missing openid scope.')
raise RegisterError('insufficient_scope')

except Token.DoesNotExist:
# logger.error('[UserInfo] Token does not exist: %s', self.params.access_token)
raise RegisterError('invalid_token')

# Has the user provided redirect URIs in JSON?
if self.params['redirect_uris'] is None:
raise RegisterError('invalid_request')

def create_response_dic(self):
"""
Create a dictionary with client_id, client_secret, and
client_secret_expires_at (set to 0 at this point)
See: https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse
"""
client = create_client(redirect_uris=self.params['redirect_uris'],
name=self.params['name'],
response_types=self.params['response_types'])

client.save()
# At this point, no support for client secret expiration so
# we return 0 meaning that it doesn't expire
dic = {
'client_id': client.client_id,
'secret': client.client_secret,
'redirect_uris': self.params['redirect_uris'],
'client_secret_expires_at': '0'
}

return dic

@classmethod
def response(cls, dic):
response = JsonResponse(dic, status=200)
response['Cache-Control'] = 'no-store'
response['Pragma'] = 'no-cache'

return response

@classmethod
def error_response(cls, code, description, status):
response = HttpResponse(status=status)
error_pattern = 'error="{0}", error_description="{1}"'
response['WWW-Authenticate'] = error_pattern.format(code, description)

return response
27 changes: 24 additions & 3 deletions oidc_provider/lib/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@


class RedirectUriError(Exception):

error = 'Redirect URI Error'
description = 'The request fails due to a missing, invalid, or mismatching' \
' redirection URI (redirect_uri).'


class ClientIdError(Exception):

error = 'Client ID Error'
description = 'The client identifier (client_id) is missing or invalid.'

Expand Down Expand Up @@ -42,7 +40,6 @@ class TokenIntrospectionError(Exception):


class AuthorizeError(Exception):

_errors = {
# Oauth2 errors.
# https://tools.ietf.org/html/rfc6749#section-4.1.2.1
Expand Down Expand Up @@ -189,3 +186,27 @@ def __init__(self, code):
error_tuple = self._errors.get(code, ('', ''))
self.description = error_tuple[0]
self.status = error_tuple[1]


class RegisterError(Exception):
_errors = {
# https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError
# https://tools.ietf.org/html/rfc6749#section-5.2
'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]
17 changes: 17 additions & 0 deletions oidc_provider/lib/utils/register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import uuid

from oidc_provider.models import Client, ResponseType


def create_client(redirect_uris=None, name=None, response_types=['code']):

client = Client()
client._redirect_uris = redirect_uris
client.name = name or uuid.uuid4().hex
client.client_id = uuid.uuid4().hex
client.client_secret = uuid.uuid4().hex
client.save()

client.response_types = ResponseType.objects.filter(value__in=response_types).all()

return client
17 changes: 17 additions & 0 deletions oidc_provider/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,23 @@ def OIDC_TEMPLATES(self):
'error': 'oidc_provider/error.html'
}

@property
def OIDC_REGISTRATION_ENDPOINT_ENABLED(self):
"""
OPTIONAL. True if dynamic client registration endpoint is enabled
https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration
"""
return True

@property
def OIDC_REGISTRATION_ENDPOINT_REQ_TOKEN(self):
"""
OPTIONAL. True if client registration requires bearer (access) token.
False if clients can be dynamically registered without authentication
http://tools.ietf.org/html/rfc6750#section-2.1
"""
return True


default_settings = DefaultSettings()

Expand Down
3 changes: 3 additions & 0 deletions oidc_provider/urls.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from oidc_provider.views import RegisterView

try:
from django.urls import url
except ImportError:
Expand All @@ -19,6 +21,7 @@
name='provider-info'),
url(r'^introspect/?$', views.TokenIntrospectionView.as_view(), name='token-introspection'),
url(r'^jwks/?$', views.JwksView.as_view(), name='jwks'),
url(r'^register/$', csrf_exempt(RegisterView.as_view()), name='register'),
]

if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'):
Expand Down
24 changes: 23 additions & 1 deletion oidc_provider/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from django.views.decorators.csrf import csrf_exempt

from oidc_provider.lib.endpoints.introspection import TokenIntrospectionEndpoint
from oidc_provider.lib.endpoints.register import RegisterEndpoint

try:
from urllib import urlencode
from urlparse import urlsplit, parse_qs, urlunsplit
Expand Down Expand Up @@ -38,7 +40,7 @@
RedirectUriError,
TokenError,
UserAuthError,
TokenIntrospectionError)
TokenIntrospectionError, RegisterError)
from oidc_provider.lib.utils.authorize import strip_prompt_login
from oidc_provider.lib.utils.common import (
redirect,
Expand Down Expand Up @@ -288,6 +290,9 @@ def get(self, request, *args, **kwargs):
if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'):
dic['check_session_iframe'] = site_url + reverse('oidc_provider:check-session-iframe')

if settings.get('OIDC_REGISTRATION_ENDPOINT_ENABLED'):
dic['registration_endpoint'] = site_url + reverse('oidc_provider:register')

response = JsonResponse(dic)
response['Access-Control-Allow-Origin'] = '*'

Expand Down Expand Up @@ -379,3 +384,20 @@ def post(self, request, *args, **kwargs):
return self.token_instrospection_endpoint_class.response(dic)
except TokenIntrospectionError:
return self.token_instrospection_endpoint_class.response({'active': False})


class RegisterView(View):
def post(self, request, *args, **kwargs):

register = RegisterEndpoint(request)

try:
register.validate_params()
dic = register.create_response_dic()
return register.response(dic)

except RegisterError as error:
return RegisterEndpoint.error_response(
error.code,
error.description,
error.status)

0 comments on commit f2fd51b

Please sign in to comment.