#115 Split OpenID Connect code
Closed 7 years ago by puiterwijk. Opened 7 years ago by puiterwijk.
puiterwijk/ipsilon oidc-split  into  master

@@ -0,0 +1,345 @@ 

+ # Copyright (C) 2016 Ipsilon project Contributors, for license see COPYING

+ 

+ from ipsilon.util.log import Log

+ from ipsilon.providers.common import ProviderPageBase

+ from ipsilon.util.security import constant_time_string_comparison

+ 

+ from jwcrypto.jwt import JWT

+ import base64

+ import cherrypy

+ import time

+ import json

+ 

+ 

+ class APIError(cherrypy.HTTPError, Log):

+ 

+     def __init__(self, code, error, description=None):

+         super(APIError, self).__init__(code, error)

+         self.debug('OpenIDC API error: %s, desc: %s'

+                    % (error, description))

+         response = {'error': error}

+         if description:

+             response['error_description'] = description

+         self._error_response = json.dumps(response)

+         cherrypy.response.headers.update({

+             'Content-Type': 'application/json'

+         })

+ 

+     def get_error_page(self, *args, **kwargs):

+         return self._error_response

+ 

+ 

+ class APIRequest(ProviderPageBase):

+     # Bearer token (RFC 6750) and Client Auth

+     authenticate_client = False

+     authenticate_token = False

+     requires_client_auth = False

+     requires_valid_token = False

+     required_scope = None

+ 

+     def __init__(self, site, provider, *args, **kwargs):

+         super(APIRequest, self).__init__(site, provider)

+         self.api_client_id = None

+         self.api_valid_token = False

+         self.api_username = None

+         self.api_scopes = []

+         self.api_client_authenticated = False

+         self.api_client_id = None

+         self.api_token = None

+         self.api_client = None

+ 

+     def pre_GET(self, *args, **kwargs):

+         # Note that we explicitly do NOT support URI Query parameter posting

+         # of bearer tokens (RFC6750, section 2.3 marks this as MAY)

+         self._preop()

+ 

+     def pre_POST(self, *args, **kwargs):

+         self._preop(*args, **kwargs)

+ 

+     def _preop(self, *args, **kwargs):

+         cherrypy.response.headers.update({

+             'Content-Type': 'application/json'

+         })

+ 

+         self.api_client_id = None

+         self.api_valid_token = False

+         self.api_username = None

+         self.api_scopes = []

+         self.api_client_authenticated = False

+         self.api_client_id = None

+         self.api_token = None

+         self.api_client = None

+ 

+         if self.authenticate_client:

+             self._authenticate_client(kwargs)

+             if self.requires_client_auth and not self.api_client_id:

+                 raise APIError(400, 'invalid_client',

+                                'client authentication required')

+ 

+         if self.authenticate_token:

+             self._authenticate_token(kwargs)

+ 

+     def _respond(self, response):

+         return json.dumps(response)

+ 

+     def _respond_error(self, error, message):

+         return self._respond({'error': error,

+                               'error_description': message})

+ 

+     def _handle_client_authentication(self, auth_method, client_id,

+                                       client_secret):

+         self.debug('Trying client auth for %s with method %s'

+                    % (client_id, auth_method))

+         if not client_id:

+             self.error('Client authentication without client_id')

+             raise APIError(400, 'invalid_client',

+                            'client authentication error')

+ 

+         client = self.cfg.datastore.getClient(client_id)

+         if not client:

+             self.error('Client authentication with invalid client ID')

+             raise APIError(400, 'invalid_client',

+                            'client authentication error')

+ 

+         if client['client_secret_expires_at'] != 0 and \

+                 client['client_secret_expires_at'] <= int(time.time()):

+             self.error('Client authentication with expired secret')

+             raise APIError(400, 'invalid_client',

+                            'client authentication error')

+ 

+         if client['token_endpoint_auth_method'] != auth_method:

+             self.error('Client authentication with invalid auth method: %s'

+                        % auth_method)

+             raise APIError(400, 'invalid_client',

+                            'client authentication error')

+ 

+         if not constant_time_string_comparison(client['client_secret'],

+                                                client_secret):

+             self.error('Client authentication with invalid secret: %s'

+                        % client_secret)

+             raise APIError(400, 'invalid_client',

+                            'client authentication error')

+ 

+         self.api_client_authenticated = True

+         self.api_client_id = client_id

+         self.api_client = client

+ 

+     def _authenticate_client(self, post_args):

+         request = cherrypy.serving.request

+         self.debug('Trying to authenticate client')

+         if 'authorization' in request.headers:

+             self.debug('Authorization header found')

+             hdr = request.headers['authorization']

+             if hdr.startswith('Basic '):

+                 self.debug('Authorization header is basic')

+                 hdr = hdr[len('Basic '):]

+                 try:

+                     client_id, client_secret = \

+                         base64.b64decode(hdr).split(':', 1)

+                 except Exception as e:  # pylint: disable=broad-except

+                     self.error('Invalid request received: %s' % repr(e))

+                     self._respond_error('invalid_request',

+                                         'invalid auth header')

+                 self.debug('Client ID: %s' % client_id)

+                 self._handle_client_authentication('client_secret_basic',

+                                                    client_id,

+                                                    client_secret)

+             else:

+                 self.error('Invalid authorization header presented')

+                 response = cherrypy.serving.response

+                 response.headers['WWW-Authenticate'] = 'Bearer realm="Ipsilon"'

+                 raise cherrypy.HTTPError(401, "Unauthorized")

+         elif 'client_id' in post_args:

+             self.debug('Client id found in post args: %s'

+                        % post_args['client_id'])

+             self._handle_client_authentication('client_secret_post',

+                                                post_args['client_id'],

+                                                post_args.get('client_secret',

+                                                              ''))

+         else:

+             self.error('No authorization presented')

+             response = cherrypy.serving.response

+             response.headers['WWW-Authenticate'] = 'Bearer realm="Ipsilon"'

+             raise cherrypy.HTTPError(401, "Unauthorized")

+         # FIXME: Perhaps add client_secret_jwt and private_key_jwt as per

+         # OpenID Connect Core section 9

+ 

+     def _handle_token_authentication(self, token):

+         token = self.cfg.datastore.lookupToken(token, 'Bearer')

+         if not token:

+             self.error('Unknown token provided')

+             raise APIError(400, 'invalid_token')

+ 

+         if self.api_client_id:

+             if token['client_id'] != self.api_client_id:

+                 self.error('Authenticated client is not token owner: %s != %s'

+                            % (token['client_id'], self.api_client_id))

+                 raise APIError(400, 'invalid_request')

+         else:

+             self.api_client = self.cfg.datastore.getClient(token['client_id'])

+             if not self.api_client:

+                 self.error('Token authentication with invalid client ID')

+                 raise APIError(400, 'invalid_client',

+                                'client authentication error')

+ 

+         if (self.required_scope is not None and

+                 self.required_scope not in token['scope']):

+             self.error('Required %s not in token scopes %s'

+                        % (self.required_scope, token['scope']))

+             raise APIError(403, 'insufficient_scope')

+ 

+         self.api_client_id = token['client_id']

+         self.api_valid_token = True

+         self.api_username = token['username']

+         self.api_scopes = token['scope']

+         self.api_token = token

+ 

+     def _authenticate_token(self, post_args):

+         request = cherrypy.serving.request

+         if 'authorization' in request.headers:

+             hdr = request.headers['authorization']

+             if hdr.startswith('Bearer '):

+                 token = hdr[len('Bearer '):]

+                 self._handle_token_authentication(token)

+             else:

+                 raise APIError(400, 'invalid_request')

+         elif 'access_token' in post_args:

+             # Bearer token

+             token = post_args['access_token']

+             self._handle_token_authentication(token)

+ 

+     def require_scope(self, scope):

+         if scope not in self.api_scopes:

+             raise APIError(403, 'insufficient_scope')

+ 

+ 

+ class Token(APIRequest):

+     authenticate_client = True

+ 

+     def POST(self, *args, **kwargs):

+         grant_type = kwargs.get('grant_type', None)

+ 

+         if grant_type == 'authorization_code':

+             # Handle authz code

+             code = kwargs.get('code', None)

+             redirect_uri = kwargs.get('redirect_uri', None)

+ 

+             token = self.cfg.datastore.lookupToken(code, 'Authorization')

+             if not token:

+                 self.error('Unknown authz token provided')

+                 return self._respond_error('invalid_request',

+                                            'invalid token')

+ 

+             if token['client_id'] != self.api_client_id:

+                 self.error('Authz code owner does not match authenticated ' +

+                            'client: %s != %s' % (token['client_id'],

+                                                  self.api_client_id))

+                 return self._respond_error('invalid_request',

+                                            'invalid token for client ID')

+ 

+             if token['redirect_uri'] != redirect_uri:

+                 self.error('Token redirect URI does not match request: ' +

+                            '%s != %s' % (token['redirect_uri'], redirect_uri))

+                 return self._respond_error('invalid_request',

+                                            'redirect_uri does not match')

+ 

+             new_token = self.cfg.datastore.exchangeAuthorizationCode(code)

+             if not new_token:

+                 self.error('Was unable to exchange for token')

+                 return self._respond_error('invalid_grant',

+                                            'Could not refresh')

+             new_token['token_type'] = 'Bearer'

+ 

+             return self._respond(new_token)

+ 

+         elif grant_type == 'refresh_token':

+             # Handle token refresh

+             refresh_token = kwargs.get('refresh_token', None)

+ 

+             refresh_result = self.cfg.datastore.refreshToken(

+                 refresh_token,

+                 self.api_client_id)

+ 

+             if not refresh_result:

+                 return self._respond_error('invalid_grant',

+                                            'Something went wrong refreshing')

+ 

+             return self._respond({

+                 'access_token': refresh_result['access_token'],

+                 'token_type': 'Bearer',

+                 'refresh_token': refresh_result['refresh_token'],

+                 'expires_in': refresh_result['expires_in']})

+ 

+         else:

+             return self._respond_error('unsupported_grant_type',

+                                        'unknown grant_type')

+ 

+ 

+ class TokenInfo(APIRequest):

+     # RFC7662 (Token introspection)

+     authenticate_client = True

+     requires_client_auth = True

+ 

+     def POST(self, *args, **kwargs):

+         token = kwargs.get('token', None)

+ 

+         token = self.cfg.datastore.lookupToken(token, None, True)

+         if not token:

+             # Per spec, if this token is not valid, but the request itself is,

+             # we return with an "empty" response

+             return self._respond({

+                 'active': False

+             })

+ 

+         # FIXME: At this moment, we only have Bearer tokens

+         token_type = 'Bearer'

+ 

+         return self._respond({

+             'active': int(time.time()) <= int(token['expires_at']),

+             'scope': ' '.join(token['scope']),

+             'client_id': token['client_id'],

+             'username': token['username'],

+             'token_type': token_type,

+             'exp': token['expires_at'],

+             'iat': token['issued_at'],

+             'sub': token['username'],

+             'aud': token['client_id'],

+             'iss': self.cfg.endpoint_url,

+         })

+ 

+ 

+ class UserInfo(APIRequest):

+     authenticate_token = True

+     requires_valid_token = True

+     required_scope = 'openid'

+ 

+     def _get_userinfo(self, *args, **kwargs):

+         info = self.cfg.datastore.getUserInfo(self.api_token['userinfocode'])

+         if not info:

+             return self._respond_error('invalid_request',

+                                        'No userinfo for token')

+ 

+         if 'userinfo_signed_response_alg' in self.api_client:

+             cherrypy.response.headers.update({

+                 'Content-Type': 'application/jwt'

+             })

+ 

+             sig = JWT(header={'alg': 'RS256',

+                               'kid': self.cfg.idp_sig_key_id},

+                       claims=info)

+             # FIXME: Maybe add other algorithms in the future

+             sig.make_signed_token(self.cfg.keyset.get_key(

+                 self.cfg.idp_sig_key_id))

+             # FIXME: Maybe encrypt in the future

+             info = sig.serialize(compact=True)

+ 

+         if isinstance(info, dict):

+             info = json.dumps(info)

+ 

+         return info

+ 

+     def GET(self, *args, **kwargs):

+         return self._get_userinfo(*kwargs, **kwargs)

+ 

+     def POST(self, *args, **kwargs):

+         return self._get_userinfo(*kwargs, **kwargs)

@@ -1,12 +1,14 @@ 

  # Copyright (C) 2016 Ipsilon project Contributors, for license see COPYING

  

- from ipsilon.util.log import Log

  from ipsilon.providers.common import ProviderPageBase

  from ipsilon.providers.common import InvalidRequest

  from ipsilon.util.policy import Policy

  from ipsilon.util.trans import Transaction

- from ipsilon.util.security import (generate_random_secure_string,

-                                    constant_time_string_comparison)

+ from ipsilon.providers.openidc.api import (Token,

+                                            TokenInfo,

+                                            UserInfo)

+ from ipsilon.providers.openidc.provider import (get_url_hostpart,

+                                                 Registration)

  from ipsilon.util.user import UserSession

  

  from jwcrypto.jwt import JWT
@@ -20,19 +22,10 @@ 

  import time

  import json

  import urllib

- from urlparse import urlparse

  

  URLROOT = 'openidc'

  

  

- def get_url_hostpart(url):

-     try:

-         o = urlparse(url)

-         return o.hostname

-     except:  # pylint: disable=bare-except

-         return url

- 

- 

  class AuthenticateRequest(ProviderPageBase):

  

      def __init__(self, site, provider, *args, **kwargs):
@@ -146,208 +139,6 @@ 

              return None

  

  

- class APIError(cherrypy.HTTPError, Log):

- 

-     def __init__(self, code, error, description=None):

-         super(APIError, self).__init__(code, error)

-         self.debug('OpenIDC API error: %s, desc: %s'

-                    % (error, description))

-         response = {'error': error}

-         if description:

-             response['error_description'] = description

-         self._error_response = json.dumps(response)

-         cherrypy.response.headers.update({

-             'Content-Type': 'application/json'

-         })

- 

-     def get_error_page(self, *args, **kwargs):

-         return self._error_response

- 

- 

- class APIRequest(ProviderPageBase):

-     # Bearer token (RFC 6750) and Client Auth

-     authenticate_client = False

-     authenticate_token = False

-     requires_client_auth = False

-     requires_valid_token = False

-     required_scope = None

- 

-     def __init__(self, site, provider, *args, **kwargs):

-         super(APIRequest, self).__init__(site, provider)

-         self.api_client_id = None

-         self.api_valid_token = False

-         self.api_username = None

-         self.api_scopes = []

-         self.api_client_authenticated = False

-         self.api_client_id = None

-         self.api_token = None

-         self.api_client = None

- 

-     def pre_GET(self, *args, **kwargs):

-         # Note that we explicitly do NOT support URI Query parameter posting

-         # of bearer tokens (RFC6750, section 2.3 marks this as MAY)

-         self._preop()

- 

-     def pre_POST(self, *args, **kwargs):

-         self._preop(*args, **kwargs)

- 

-     def _preop(self, *args, **kwargs):

-         cherrypy.response.headers.update({

-             'Content-Type': 'application/json'

-         })

- 

-         self.api_client_id = None

-         self.api_valid_token = False

-         self.api_username = None

-         self.api_scopes = []

-         self.api_client_authenticated = False

-         self.api_client_id = None

-         self.api_token = None

-         self.api_client = None

- 

-         if self.authenticate_client:

-             self._authenticate_client(kwargs)

-             if self.requires_client_auth and not self.api_client_id:

-                 raise APIError(400, 'invalid_client',

-                                'client authentication required')

- 

-         if self.authenticate_token:

-             self._authenticate_token(kwargs)

- 

-     def _respond(self, response):

-         return json.dumps(response)

- 

-     def _respond_error(self, error, message):

-         return self._respond({'error': error,

-                               'error_description': message})

- 

-     def _handle_client_authentication(self, auth_method, client_id,

-                                       client_secret):

-         self.debug('Trying client auth for %s with method %s'

-                    % (client_id, auth_method))

-         if not client_id:

-             self.error('Client authentication without client_id')

-             raise APIError(400, 'invalid_client',

-                            'client authentication error')

- 

-         client = self.cfg.datastore.getClient(client_id)

-         if not client:

-             self.error('Client authentication with invalid client ID')

-             raise APIError(400, 'invalid_client',

-                            'client authentication error')

- 

-         if client['client_secret_expires_at'] != 0 and \

-                 client['client_secret_expires_at'] <= int(time.time()):

-             self.error('Client authentication with expired secret')

-             raise APIError(400, 'invalid_client',

-                            'client authentication error')

- 

-         if client['token_endpoint_auth_method'] != auth_method:

-             self.error('Client authentication with invalid auth method: %s'

-                        % auth_method)

-             raise APIError(400, 'invalid_client',

-                            'client authentication error')

- 

-         if not constant_time_string_comparison(client['client_secret'],

-                                                client_secret):

-             self.error('Client authentication with invalid secret: %s'

-                        % client_secret)

-             raise APIError(400, 'invalid_client',

-                            'client authentication error')

- 

-         self.api_client_authenticated = True

-         self.api_client_id = client_id

-         self.api_client = client

- 

-     def _authenticate_client(self, post_args):

-         request = cherrypy.serving.request

-         self.debug('Trying to authenticate client')

-         if 'authorization' in request.headers:

-             self.debug('Authorization header found')

-             hdr = request.headers['authorization']

-             if hdr.startswith('Basic '):

-                 self.debug('Authorization header is basic')

-                 hdr = hdr[len('Basic '):]

-                 try:

-                     client_id, client_secret = \

-                         base64.b64decode(hdr).split(':', 1)

-                 except Exception as e:  # pylint: disable=broad-except

-                     self.error('Invalid request received: %s' % repr(e))

-                     self._respond_error('invalid_request',

-                                         'invalid auth header')

-                 self.debug('Client ID: %s' % client_id)

-                 self._handle_client_authentication('client_secret_basic',

-                                                    client_id,

-                                                    client_secret)

-             else:

-                 self.error('Invalid authorization header presented')

-                 response = cherrypy.serving.response

-                 response.headers['WWW-Authenticate'] = 'Bearer realm="Ipsilon"'

-                 raise cherrypy.HTTPError(401, "Unauthorized")

-         elif 'client_id' in post_args:

-             self.debug('Client id found in post args: %s'

-                        % post_args['client_id'])

-             self._handle_client_authentication('client_secret_post',

-                                                post_args['client_id'],

-                                                post_args.get('client_secret',

-                                                              ''))

-         else:

-             self.error('No authorization presented')

-             response = cherrypy.serving.response

-             response.headers['WWW-Authenticate'] = 'Bearer realm="Ipsilon"'

-             raise cherrypy.HTTPError(401, "Unauthorized")

-         # FIXME: Perhaps add client_secret_jwt and private_key_jwt as per

-         # OpenID Connect Core section 9

- 

-     def _handle_token_authentication(self, token):

-         token = self.cfg.datastore.lookupToken(token, 'Bearer')

-         if not token:

-             self.error('Unknown token provided')

-             raise APIError(400, 'invalid_token')

- 

-         if self.api_client_id:

-             if token['client_id'] != self.api_client_id:

-                 self.error('Authenticated client is not token owner: %s != %s'

-                            % (token['client_id'], self.api_client_id))

-                 raise APIError(400, 'invalid_request')

-         else:

-             self.api_client = self.cfg.datastore.getClient(token['client_id'])

-             if not self.api_client:

-                 self.error('Token authentication with invalid client ID')

-                 raise APIError(400, 'invalid_client',

-                                'client authentication error')

- 

-         if (self.required_scope is not None and

-                 self.required_scope not in token['scope']):

-             self.error('Required %s not in token scopes %s'

-                        % (self.required_scope, token['scope']))

-             raise APIError(403, 'insufficient_scope')

- 

-         self.api_client_id = token['client_id']

-         self.api_valid_token = True

-         self.api_username = token['username']

-         self.api_scopes = token['scope']

-         self.api_token = token

- 

-     def _authenticate_token(self, post_args):

-         request = cherrypy.serving.request

-         if 'authorization' in request.headers:

-             hdr = request.headers['authorization']

-             if hdr.startswith('Bearer '):

-                 token = hdr[len('Bearer '):]

-                 self._handle_token_authentication(token)

-             else:

-                 raise APIError(400, 'invalid_request')

-         elif 'access_token' in post_args:

-             # Bearer token

-             token = post_args['access_token']

-             self._handle_token_authentication(token)

- 

-     def require_scope(self, scope):

-         if scope not in self.api_scopes:

-             raise APIError(403, 'insufficient_scope')

- 

- 

  class Authorization(AuthenticateRequest):

  

      def start_authz(self, arguments):
@@ -867,282 +658,6 @@ 

          return self._perform_continue(*args, form_filled=True, **kwargs)

  

  

- class Registration(APIRequest):

- 

-     def POST(self, *args, **kwargs):

-         if not self.cfg.allow_dynamic_client_registration:

-             raise APIError(400, 'invalid_request',

-                            'dynamic client registration has been disabled')

- 

-         try:

-             client_metadata = json.loads(cherrypy.request.rfile.read())

-         except:

-             raise APIError(400, 'invalid_client_metadata',

-                            'unable to parse metadata')

-         self.debug('Received client registration request: %s'

-                    % client_metadata)

- 

-         # Fill in defaults for optional arguments

-         client_metadata['response_types'] = client_metadata.get(

-             'response_types', ['code'])

-         client_metadata['grant_types'] = client_metadata.get(

-             'grant_types', ['authorization_code'])

-         client_metadata['application_type'] = client_metadata.get(

-             'application_type', 'web')

-         client_metadata['contacts'] = client_metadata.get('contacts', [])

-         client_metadata['subject_type'] = client_metadata.get('subject_type',

-                                                               'pairwise')

-         client_metadata['id_token_signed_response_alg'] = client_metadata.get(

-             'id_token_signed_response_alg', 'RS256')

-         client_metadata['token_endpoint_auth_method'] = client_metadata.get(

-             'token_endpoint_auth_method', 'client_secret_basic')

-         client_metadata['require_auth_time'] = client_metadata.get(

-             'require_auth_time', False)

- 

-         # Check the client metadata received

-         if 'redirect_uris' not in client_metadata:

-             raise APIError(400, 'invalid_client_metadata',

-                            'missing redirect_uris')

- 

-         if client_metadata['application_type'] not in ['web', 'native']:

-             raise APIError(400, 'invalid_client_metadata',

-                            'application_type invalid')

- 

-         for redirect_uri in client_metadata['redirect_uris']:

-             if '#' in redirect_uri:

-                 raise APIError(400, 'invalid_redirect_uri',

-                                'redirect_uri contains fragment')

- 

-             if client_metadata['application_type'] == 'web':

-                 # In this case, it must be https:// and not https://localhost

-                 if (not redirect_uri.startswith('https://') or

-                         redirect_uri.startswith('https://localhost')):

-                     raise APIError(400, 'invalid_redirect_uri',

-                                    'redirect_uri %s not valid' % redirect_uri)

- 

-             elif client_metadata['application_type'] == 'native':

-                 # In this case, it must be http://localhost, or something

-                 # that is not http:// or https://

-                 if (redirect_uri.startswith('https://') or

-                         (redirect_uri.startswith('http://') and

-                          not redirect_uri.startswith('http://localhost'))):

-                     raise APIError(400, 'invalid_redirect_uri',

-                                    'redirect_uri %s not valid' % redirect_uri)

- 

-         if 'initiate_login_uri' in client_metadata:

-             if not client_metadata['initiate_login_uri'].startswith(

-                     'https://'):

-                 raise APIError(400, 'invalid_client_metadata',

-                                'initiate_login_uri must be https')

- 

-         if 'sector_identifier_uri' not in client_metadata:

-             hostname = None

-             for redir_uri in client_metadata['redirect_uris']:

-                 cur_host = get_url_hostpart(redir_uri)

-                 if not cur_host:

-                     raise APIError(400, 'invalid_client_metadata',

-                                    'Unable to parse hostname from ' +

-                                    'redirect_uri %s' % redir_uri)

-                 if hostname is not None and cur_host != hostname:

-                     raise APIError(400, 'invalid_client_metadata',

-                                    'Multiple redirect_uri hostnames without ' +

-                                    'sector_identifier_uri')

-                 hostname = cur_host

-         else:

-             if not client_metadata['sector_identifier_uri'].startswith(

-                     'https://'):

-                 raise APIError(400, 'invalid_client_metadata',

-                                'sector_identifier_uri must be https')

- 

-             try:

-                 resp = requests.get(client_metadata['sector_identifier_uri'])

-                 resp = resp.json()

-                 for redirect_uri in client_metadata['redirect_uris']:

-                     if redirect_uri not in resp:

-                         raise APIError(400, 'invalid_client_metadata',

-                                        'redirect_uri %s not in ' +

-                                        'sector_identifier_uri document' %

-                                        redirect_uri)

-             except Exception as ex:

-                 self.debug('Unable to process sector_identifier_uri: %s' %

-                            ex)

-                 raise APIError(400, 'invalid_client_metadata',

-                                'unable to process sector_identifier_uri')

- 

-         if 'code' in client_metadata['response_types']:

-             if 'authorization_code' not in client_metadata['grant_types']:

-                 raise APIError(400, 'invalid_client_metadata',

-                                'authorization_code missing with code')

- 

-         if ('token' in client_metadata['response_types'] or

-                 'id_token' in client_metadata['response_types']):

-             if 'implicit' not in client_metadata['grant_types']:

-                 raise APIError(400, 'invalid_client_metadata',

-                                'implicit missing with token or id_token')

- 

-         if 'jwks' in client_metadata and 'jwks_uri' in client_metadata:

-             raise APIError(400, 'invalid_client_metadata',

-                            'both jwks and jwks_uri provided')

- 

-         # If all checks pass, generate client ID and secret

-         client_metadata['client_secret'] = \

-             generate_random_secure_string()

-         client_metadata['client_secret_expires_at'] = 0  # FIXME: Expire?

-         client_metadata['client_id_issued_at'] = int(time.time())

- 

-         # Store some internal data

-         client_metadata['ipsilon_internal'] = {

-             'trusted': False

-         }

- 

-         # Store and add reg uri

-         client_id = self.cfg.datastore.registerDynamicClient(client_metadata)

-         client_metadata['client_id'] = client_id

- 

-         # Clear internal data from returned values

-         del client_metadata['ipsilon_internal']

- 

-         # FIXME: Offer this once we add a ClientConfiguration endpoint

-         # client_metadata['registration_access_token'] = \

-         #    generate_random_secure_string()

-         # client_metadata['registration_client_uri'] = '%s%s' % (

-         #    self.cfg.endpoint_url, 'ClientConfiguration')

- 

-         return self._respond(client_metadata)

- 

- 

- class Token(APIRequest):

-     authenticate_client = True

- 

-     def POST(self, *args, **kwargs):

-         grant_type = kwargs.get('grant_type', None)

- 

-         if grant_type == 'authorization_code':

-             # Handle authz code

-             code = kwargs.get('code', None)

-             redirect_uri = kwargs.get('redirect_uri', None)

- 

-             token = self.cfg.datastore.lookupToken(code, 'Authorization')

-             if not token:

-                 self.error('Unknown authz token provided')

-                 return self._respond_error('invalid_request',

-                                            'invalid token')

- 

-             if token['client_id'] != self.api_client_id:

-                 self.error('Authz code owner does not match authenticated ' +

-                            'client: %s != %s' % (token['client_id'],

-                                                  self.api_client_id))

-                 return self._respond_error('invalid_request',

-                                            'invalid token for client ID')

- 

-             if token['redirect_uri'] != redirect_uri:

-                 self.error('Token redirect URI does not match request: ' +

-                            '%s != %s' % (token['redirect_uri'], redirect_uri))

-                 return self._respond_error('invalid_request',

-                                            'redirect_uri does not match')

- 

-             new_token = self.cfg.datastore.exchangeAuthorizationCode(code)

-             if not new_token:

-                 self.error('Was unable to exchange for token')

-                 return self._respond_error('invalid_grant',

-                                            'Could not refresh')

-             new_token['token_type'] = 'Bearer'

- 

-             return self._respond(new_token)

- 

-         elif grant_type == 'refresh_token':

-             # Handle token refresh

-             refresh_token = kwargs.get('refresh_token', None)

- 

-             refresh_result = self.cfg.datastore.refreshToken(

-                 refresh_token,

-                 self.api_client_id)

- 

-             if not refresh_result:

-                 return self._respond_error('invalid_grant',

-                                            'Something went wrong refreshing')

- 

-             return self._respond({

-                 'access_token': refresh_result['access_token'],

-                 'token_type': 'Bearer',

-                 'refresh_token': refresh_result['refresh_token'],

-                 'expires_in': refresh_result['expires_in']})

- 

-         else:

-             return self._respond_error('unsupported_grant_type',

-                                        'unknown grant_type')

- 

- 

- class TokenInfo(APIRequest):

-     # RFC7662 (Token introspection)

-     authenticate_client = True

-     requires_client_auth = True

- 

-     def POST(self, *args, **kwargs):

-         token = kwargs.get('token', None)

- 

-         token = self.cfg.datastore.lookupToken(token, None, True)

-         if not token:

-             # Per spec, if this token is not valid, but the request itself is,

-             # we return with an "empty" response

-             return self._respond({

-                 'active': False

-             })

- 

-         # FIXME: At this moment, we only have Bearer tokens

-         token_type = 'Bearer'

- 

-         return self._respond({

-             'active': int(time.time()) <= int(token['expires_at']),

-             'scope': ' '.join(token['scope']),

-             'client_id': token['client_id'],

-             'username': token['username'],

-             'token_type': token_type,

-             'exp': token['expires_at'],

-             'iat': token['issued_at'],

-             'sub': token['username'],

-             'aud': token['client_id'],

-             'iss': self.cfg.endpoint_url,

-         })

- 

- 

- class UserInfo(APIRequest):

-     authenticate_token = True

-     requires_valid_token = True

-     required_scope = 'openid'

- 

-     def _get_userinfo(self, *args, **kwargs):

-         info = self.cfg.datastore.getUserInfo(self.api_token['userinfocode'])

-         if not info:

-             return self._respond_error('invalid_request',

-                                        'No userinfo for token')

- 

-         if 'userinfo_signed_response_alg' in self.api_client:

-             cherrypy.response.headers.update({

-                 'Content-Type': 'application/jwt'

-             })

- 

-             sig = JWT(header={'alg': 'RS256',

-                               'kid': self.cfg.idp_sig_key_id},

-                       claims=info)

-             # FIXME: Maybe add other algorithms in the future

-             sig.make_signed_token(self.cfg.keyset.get_key(

-                 self.cfg.idp_sig_key_id))

-             # FIXME: Maybe encrypt in the future

-             info = sig.serialize(compact=True)

- 

-         if isinstance(info, dict):

-             info = json.dumps(info)

- 

-         return info

- 

-     def GET(self, *args, **kwargs):

-         return self._get_userinfo(*kwargs, **kwargs)

- 

-     def POST(self, *args, **kwargs):

-         return self._get_userinfo(*kwargs, **kwargs)

- 

- 

  class OpenIDC(ProviderPageBase):

  

      def __init__(self, *args, **kwargs):

@@ -0,0 +1,164 @@ 

+ # Copyright (C) 2016 Ipsilon project Contributors, for license see COPYING

+ 

+ from ipsilon.providers.openidc.api import APIError, APIRequest

+ from ipsilon.util.security import generate_random_secure_string

+ 

+ 

+ import cherrypy

+ import json

+ import time

+ import requests

+ from urlparse import urlparse

+ 

+ 

+ def get_url_hostpart(url):

+     try:

+         o = urlparse(url)

+         return o.hostname

+     except:  # pylint: disable=bare-except

+         return url

+ 

+ 

+ def validate_client_metadata(client_metadata):

+     # Fill in defaults for optional arguments

+     client_metadata['response_types'] = client_metadata.get(

+         'response_types', ['code'])

+     client_metadata['grant_types'] = client_metadata.get(

+         'grant_types', ['authorization_code'])

+     client_metadata['application_type'] = client_metadata.get(

+         'application_type', 'web')

+     client_metadata['contacts'] = client_metadata.get('contacts', [])

+     client_metadata['subject_type'] = client_metadata.get('subject_type',

+                                                           'pairwise')

+     client_metadata['id_token_signed_response_alg'] = client_metadata.get(

+         'id_token_signed_response_alg', 'RS256')

+     client_metadata['token_endpoint_auth_method'] = client_metadata.get(

+         'token_endpoint_auth_method', 'client_secret_basic')

+     client_metadata['require_auth_time'] = client_metadata.get(

+         'require_auth_time', False)

+ 

+     # Check the client metadata received

+     if 'redirect_uris' not in client_metadata:

+         raise APIError(400, 'invalid_client_metadata',

+                        'missing redirect_uris')

+ 

+     if client_metadata['application_type'] not in ['web', 'native']:

+         raise APIError(400, 'invalid_client_metadata',

+                        'application_type invalid')

+ 

+     for redirect_uri in client_metadata['redirect_uris']:

+         if '#' in redirect_uri:

+             raise APIError(400, 'invalid_redirect_uri',

+                            'redirect_uri contains fragment')

+ 

+         if client_metadata['application_type'] == 'web':

+             # In this case, it must be https:// and not https://localhost

+             if (not redirect_uri.startswith('https://') or

+                     redirect_uri.startswith('https://localhost')):

+                 raise APIError(400, 'invalid_redirect_uri',

+                                'redirect_uri %s not valid' % redirect_uri)

+ 

+         elif client_metadata['application_type'] == 'native':

+             # In this case, it must be http://localhost, or something

+             # that is not http:// or https://

+             if (redirect_uri.startswith('https://') or

+                     (redirect_uri.startswith('http://') and

+                      not redirect_uri.startswith('http://localhost'))):

+                 raise APIError(400, 'invalid_redirect_uri',

+                                'redirect_uri %s not valid' % redirect_uri)

+ 

+     if 'initiate_login_uri' in client_metadata:

+         if not client_metadata['initiate_login_uri'].startswith(

+                 'https://'):

+             raise APIError(400, 'invalid_client_metadata',

+                            'initiate_login_uri must be https')

+ 

+     if 'sector_identifier_uri' not in client_metadata:

+         hostname = None

+         for redir_uri in client_metadata['redirect_uris']:

+             cur_host = get_url_hostpart(redir_uri)

+             if not cur_host:

+                 raise APIError(400, 'invalid_client_metadata',

+                                'Unable to parse hostname from ' +

+                                'redirect_uri %s' % redir_uri)

+             if hostname is not None and cur_host != hostname:

+                 raise APIError(400, 'invalid_client_metadata',

+                                'Multiple redirect_uri hostnames without ' +

+                                'sector_identifier_uri')

+             hostname = cur_host

+     else:

+         if not client_metadata['sector_identifier_uri'].startswith(

+                 'https://'):

+             raise APIError(400, 'invalid_client_metadata',

+                            'sector_identifier_uri must be https')

+ 

+         try:

+             resp = requests.get(client_metadata['sector_identifier_uri'])

+             resp = resp.json()

+             for redirect_uri in client_metadata['redirect_uris']:

+                 if redirect_uri not in resp:

+                     raise APIError(400, 'invalid_client_metadata',

+                                    'redirect_uri %s not in ' +

+                                    'sector_identifier_uri document' %

+                                    redirect_uri)

+         except Exception as ex:

+             raise APIError(400, 'invalid_client_metadata',

+                            'unable to process sector_identifier_uri: %s' % ex)

+ 

+     if 'code' in client_metadata['response_types']:

+         if 'authorization_code' not in client_metadata['grant_types']:

+             raise APIError(400, 'invalid_client_metadata',

+                            'authorization_code missing with code')

+ 

+     if ('token' in client_metadata['response_types'] or

+             'id_token' in client_metadata['response_types']):

+         if 'implicit' not in client_metadata['grant_types']:

+             raise APIError(400, 'invalid_client_metadata',

+                            'implicit missing with token or id_token')

+ 

+     if 'jwks' in client_metadata and 'jwks_uri' in client_metadata:

+         raise APIError(400, 'invalid_client_metadata',

+                        'both jwks and jwks_uri provided')

+ 

+ 

+ class Registration(APIRequest):

+ 

+     def POST(self, *args, **kwargs):

+         if not self.cfg.allow_dynamic_client_registration:

+             raise APIError(400, 'invalid_request',

+                            'dynamic client registration has been disabled')

+ 

+         try:

+             client_metadata = json.loads(cherrypy.request.rfile.read())

+         except:

+             raise APIError(400, 'invalid_client_metadata',

+                            'unable to parse metadata')

+         self.debug('Received client registration request: %s'

+                    % client_metadata)

+ 

+         validate_client_metadata(client_metadata)

+ 

+         client_metadata['client_secret'] = \

+             generate_random_secure_string()

+         client_metadata['client_secret_expires_at'] = 0  # FIXME: Expire?

+         client_metadata['client_id_issued_at'] = int(time.time())

+ 

+         # Store some internal data

+         client_metadata['ipsilon_internal'] = {

+             'trusted': False

+         }

+ 

+         # Store and add reg uri

+         client_id = self.cfg.datastore.registerDynamicClient(client_metadata)

+         client_metadata['client_id'] = client_id

+ 

+         # Clear internal data from returned values

+         del client_metadata['ipsilon_internal']

+ 

+         # FIXME: Offer this once we add a ClientConfiguration endpoint

+         # client_metadata['registration_access_token'] = \

+         #    generate_random_secure_string()

+         # client_metadata['registration_client_uri'] = '%s%s' % (

+         #    self.cfg.endpoint_url, 'ClientConfiguration')

+ 

+         return self._respond(client_metadata)

This splits the various parts of the OpenID Connect protocol into
files with specific configurations.

Signed-off-by: Patrick Uiterwijk puiterwijk@redhat.com

rebased

7 years ago

rebased

7 years ago

Pull-Request has been closed by puiterwijk

7 years ago