#90 Implement OpenID Connect + WebFinger
Merged 7 years ago by puiterwijk. Opened 7 years ago by puiterwijk.
puiterwijk/ipsilon OpenIDConnect  into  master

file modified
+1
@@ -107,6 +107,7 @@ 

  	PYTHONPATH=./ ./tests/tests.py --test=ldap

  	PYTHONPATH=./ ./tests/tests.py --test=ldapdown

  	PYTHONPATH=./ ./tests/tests.py --test=openid

+ 	PYTHONPATH=./ ./tests/tests.py --test=openidc

  	PYTHONPATH=./ ./tests/tests.py --test=dbupgrades

  

  test: lp-test unittests tests

file modified
+1 -1
@@ -97,7 +97,7 @@ 

  

  The default configuration for the client will install a configuration in Apache

  that will authenticate via the IdP any attempt to connect to the location named

- '/saml2protected', a test file is returned at that location.

+ '/protected', a test file is returned at that location.

  

  In order to successfully install a client 2 steps are necessary:

  

file modified
+1 -1
@@ -212,7 +212,7 @@ 

    beneath the base URI.  Accessing this URI will trigger the authentication

    flow described above.  The browser will then return to this URI upon

    successful authentication.  This should typically be set to the "Log In" URI

-   of your web application.  It defaults to ``/saml2protected``, but it can be

+   of your web application.  It defaults to ``/protected``, but it can be

    set with the ``--saml-auth`` option.

  

  endpoint

@@ -24,9 +24,10 @@ 

  

  HTTPDCONFD = '/etc/httpd/conf.d'

  SAML2_TEMPLATE = '/usr/share/ipsilon/templates/install/saml2/sp.conf'

- SAML2_CONFFILE = '/etc/httpd/conf.d/ipsilon-saml.conf'

- SAML2_HTTPDIR = '/etc/httpd/saml2'

- SAML2_PROTECTED = '/saml2protected'

+ OPENIDC_TEMPLATE = '/usr/share/ipsilon/templates/install/openidc/rp.conf'

+ CONFFILE = '/etc/httpd/conf.d/ipsilon-%s.conf'

+ HTTPDIR = '/etc/httpd/%s'

+ PROTECTED = '/protected'

  

  #Installation arguments

  args = dict()
@@ -73,7 +74,7 @@ 

  

      path = None

      if not args['saml_no_httpd']:

-         path = os.path.join(SAML2_HTTPDIR, args['hostname'])

+         path = os.path.join(HTTPDIR % 'saml2', args['hostname'])

          if os.path.exists(path):

              raise Exception('Service Provider is already configured')

          os.makedirs(path, 0750)
@@ -153,7 +154,7 @@ 

                  logger.error("Failed to read SP Image file!\n" +

                               "Error: [%s]" % e)

  

-         sp_link = 'https://%s%s' % (args['hostname'], args['saml_auth'])

+         sp_link = 'https://%s%s' % (args['hostname'], args['auth_location'])

  

          # Register the SP

          try:
@@ -173,15 +174,15 @@ 

  

          saml_protect = 'auth'

          saml_auth=''

-         if args['saml_base'] != args['saml_auth']:

+         if args['saml_base'] != args['auth_location']:

              saml_protect = 'info'

              saml_auth = '<Location %s>\n' \

                          '    MellonEnable "auth"\n' \

                          '    Header append Cache-Control "no-cache"\n' \

-                         '</Location>\n' % args['saml_auth']

+                         '</Location>\n' % args['auth_location']

  

          psp = '# '

-         if args['saml_auth'] == SAML2_PROTECTED:

+         if args['auth_location'] == PROTECTED:

              # default location, enable the default page

              psp = ''

  
@@ -213,13 +214,13 @@ 

                      'sp_hostname': args['hostname'],

                      'sp_port': port_str,

                      'sp': psp}

-         files.write_from_template(SAML2_CONFFILE, SAML2_TEMPLATE, samlopts)

+         files.write_from_template(CONFFILE % 'saml', SAML2_TEMPLATE, samlopts)

  

-         files.fix_user_dirs(SAML2_HTTPDIR, args['httpd_user'])

+         files.fix_user_dirs(HTTPDIR % 'saml2', args['httpd_user'])

  

          logger.info('SAML Service Provider configured.')

          logger.info('You should be able to restart the HTTPD server and' +

-                     ' then access it at %s%s' % (url, args['saml_auth']))

+                     ' then access it at %s%s' % (url, args['auth_location']))

      else:

          logger.info('SAML Service Provider configuration ready.')

          logger.info('Use the certificate, key and metadata.xml files to' +
@@ -264,33 +265,200 @@ 

          raise Exception('%s' % message)

  

  

- def install():

-     if args['saml']:

-         saml2()

- 

- 

  def saml2_uninstall():

-     path = os.path.join(SAML2_HTTPDIR, args['hostname'])

+     path = os.path.join(HTTPDIR % 'saml2', args['hostname'])

      if os.path.exists(path):

          try:

              shutil.rmtree(path)

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

              log_exception(e)

  

-     if os.path.exists(SAML2_CONFFILE):

+     if os.path.exists(CONFFILE % 'saml'):

          try:

-             os.remove(SAML2_CONFFILE)

+             os.remove(CONFFILE % 'saml')

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

              log_exception(e)

  

  

- def uninstall():

-     logger.info('Uninstalling Service Provider')

-     #FXIME: ask confirmation

-     saml2_uninstall()

-     logger.info('Uninstalled SAML2 data')

+ def saml2_add_arguments(parser):

+     parser.add_argument('--saml', action='store_true',

+                         help="Whether to install a saml2 SP")

+     parser.add_argument('--saml-idp-url', default=None,

+                         help="A URL of the IDP to register the SP with")

+     parser.add_argument('--saml-idp-metadata', default=None,

+                         help="A URL pointing at the IDP Metadata (FILE or HTTP)")

+     parser.add_argument('--saml-no-httpd', action='store_true', default=False,

+                         help="Do not configure httpd")

+     parser.add_argument('--saml-base', default='/',

+                         help="Where saml2 authdata is available")

+     parser.add_argument('--saml-sp', default='/saml2',

+                         help="Where saml communication happens")

+     parser.add_argument('--saml-sp-logout', default=None,

+                         help="Single Logout URL")

+     parser.add_argument('--saml-sp-post', default=None,

+                         help="Post response URL")

+     parser.add_argument('--saml-sp-paos', default=None,

+                         help="PAOS response URL, used for ECP")

+     parser.add_argument('--no-saml-soap-logout', action='store_true',

+                         default=False,

+                         help="Disable Single Logout over SOAP")

+     parser.add_argument('--saml-secure-setup', action='store_true',

+                         default=True, help="Turn on all security checks")

+     parser.add_argument('--saml-nameid', default='unspecified',

+                         choices=SAML2_NAMEID_MAP.keys(),

+                         help="SAML NameID format to use")

+     parser.add_argument('--saml-sp-name', default=None,

+                         help="The SP name to register with the IdP")

+     parser.add_argument('--saml-sp-description', default=None,

+                         help="The description of the SP to display on the " +

+                         "portal")

+     parser.add_argument('--saml-sp-visible', action='store_false',

+                         default=True,

+                         help="The SP is visible in the portal")

+     parser.add_argument('--saml-sp-image', default=None,

+                         help="Image to display for this SP on the portal")

+     parser.add_argument('--debug', action='store_true', default=False,

+                         help="Turn on script debugging")

+     parser.add_argument('--config-profile', default=None,

+                         help=argparse.SUPPRESS)

+     parser.add_argument('--saml-auth', default=None,

+                         help="Backwards compatibility. Use --auth-location.")

+ 

+ 

+ def saml2_verify_arguments(args):

+     if args['saml_auth']:

+         logger.warn('--saml-auth is deprecated. Please use --auth-location')

+         args['auth_location'] = args['saml_auth']

+ 

+     # Validate that all path options begin with '/'

+     path_args = ['saml_base', 'auth_location', 'saml_sp', 'saml_sp_logout',

+                  'saml_sp_post', 'saml_sp_paos']

+     for path_arg in path_args:

+         if args[path_arg] is not None and not args[path_arg].startswith('/'):

+             raise ValueError('--%s must begin with a / character.' %

+                              path_arg.replace('_', '-'))

+ 

+     # The saml_sp setting must be a subpath of saml_base since it is

+     # used as the MellonEndpointPath.

+     if not args['saml_sp'].startswith(args['saml_base']):

+         raise ValueError('--saml-sp must be a subpath of --saml-base.')

+ 

+     # The samle_auth setting must be a subpath of saml_base otherwise

+     # the IdP cannot be identified by mod_auth_mellon.

+     if not args['auth_location'].startswith(args['saml_base']):

+         raise ValueError('--auth-location must be a subpath of --saml-base.')

+ 

+     # The saml_sp_logout, saml_sp_post and saml_sp_paos settings must

+     # be subpaths of saml_sp (the mellon endpoint).

+     path_args = {'saml_sp_logout': 'logout',

+                  'saml_sp_post': 'postResponse',

+                  'saml_sp_paos': 'paosResponse'}

+     for path_arg, default_path in path_args.items():

+         if args[path_arg] is None:

+             args[path_arg] = '%s/%s' % (args['saml_sp'].rstrip('/'),

+                                         default_path)

+ 

+         elif not args[path_arg].startswith(args['saml_sp']):

+             raise ValueError('--%s must be a subpath of --saml-sp' %

+                              path_arg.replace('_', '-'))

+ 

+     # If saml_idp_url if being used, we require saml_sp_name to

+     # use when registering the SP.

+     if args['saml_idp_url'] and not args['saml_sp_name']:

+         raise ValueError('--saml-sp-name must be specified when using' +

+                          '--saml-idp-url')

+ 

+ 

+ # OpenID Connect

+ def openidc():

+     logger.info('Installing OpenID Connect Relying Party')

+ 

+     discovery_url = '%s/openidc/wellknown_openid_configuration' % \

+                     args['openidc_idp_url']

+     try:

+         r = requests.get(discovery_url)

+         r.raise_for_status()

+         discovered_info = r.json()

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

+         logger.error("Failed to retrieve IdP configuration!\n" +

+                      "Error: [%s]" % repr(e))

+         raise

+ 

+     if not 'registration_endpoint' in discovered_info:

+         raise ValueError('This IdP does not provide automatic registration')

+ 

+     proto = 'https'

+     port_str = ''

+     if args['port']:

+         port_str = ':%s' % args['port']

  

+     url = '%s://%s%s%s' % (proto, args['hostname'], port_str,

+                            args['auth_location'])

+     redirect_uri = '%s/redirect_uri' % url

+ 

+     # Generate client metadata

+     client_info = {}

+     client_info['redirect_uris'] = [redirect_uri]

+     client_info['response_types'] = ['code']

+     client_info['grant_types'] = ['authorization_code']

+     client_info['application_type'] = 'web'

+     client_info['client_name'] = 'Ipsilon Client %s' % url

+     client_info['client_uri'] = url

+     client_info['subject_type'] = args['openidc_subject_type']

+ 

+     # Submit client info

+     logger.info('Registering RP with the IdP')

+     try:

+         r = requests.post(discovered_info['registration_endpoint'],

+                           json=client_info)

+         r.raise_for_status()

+         registration_response = r.json()

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

+         logger.error("Failed to register with the IdP!\n" +

+                      "Error: [%s]" % repr(e))

+         raise

  

+     validate_server = 'On'

+     if args['openidc_skip_ssl_validation']:

+         validate_server = 'Off'

+ 

+     # Generate config

+     openidcopts = {'redirect_uri': redirect_uri,

+                    'crypto_passphrase': base64.b64encode(os.urandom(32))[:32],

+                    'idp_metadata_url': discovery_url,

+                    'client_id': registration_response['client_id'],

+                    'client_secret': registration_response['client_secret'],

+                    'validate_server': validate_server,

+                    'response_type': args['openidc_response_type'],

+                    'auth_location': args['auth_location']}

+     files.write_from_template(CONFFILE % 'openidc', OPENIDC_TEMPLATE,

+                               openidcopts)

+ 

+     logger.info('OpenID Connect Relying Party configured')

+     logger.info('You should be able to restart the HTTPD server and' +

+                 ' then access it at %s%s' % (url, args['auth_location']))

+ 

+ 

+ def openidc_verify_arguments(args):

+     if not args['openidc_idp_url']:

+         raise ValueError('OpenIDC IdP URL needs to be provided')

+ 

+ 

+ def openidc_add_arguments(parser):

+     parser.add_argument('--openidc', action='store_true', default=False,

+                         help='Whether to install an OpenID Connect RP')

+     parser.add_argument('--openidc-idp-url', default=None,

+                         help='A URL of the IdP to register the RP with')

+     parser.add_argument('--openidc-response-type', default='code',

+                         help='Which response type to use, determines the flow')

+     parser.add_argument('--openidc-subject-type', default='pairwise',

+                         help='Which subject type to request: pairwise or ' +

+                              'public')

+     parser.add_argument('--openidc-skip-ssl-validation', action='store_true',

+                         help='Whether to skip validating the IdP SSL cert')

+ 

+ 

+ # Global

  def log_exception(e):

      if 'debug' in args and args['debug']:

          logger.exception(e)
@@ -353,51 +521,14 @@ 

                               "used to create a SP (- to read from stdin)")

      parser.add_argument('--httpd-user', default='apache',

                          help="Web server account used to read certs")

-     parser.add_argument('--saml', action='store_true', default=True,

-                         help="Whether to install a saml2 SP")

-     parser.add_argument('--saml-idp-url', default=None,

-                         help="A URL of the IDP to register the SP with")

-     parser.add_argument('--saml-idp-metadata', default=None,

-                         help="A URL pointing at the IDP Metadata (FILE or HTTP)")

-     parser.add_argument('--saml-no-httpd', action='store_true', default=False,

-                         help="Do not configure httpd")

-     parser.add_argument('--saml-base', default='/',

-                         help="Where saml2 authdata is available")

-     parser.add_argument('--saml-auth', default=SAML2_PROTECTED,

-                         help="Where saml2 authentication is enforced")

-     parser.add_argument('--saml-sp', default='/saml2',

-                         help="Where saml communication happens")

-     parser.add_argument('--saml-sp-logout', default=None,

-                         help="Single Logout URL")

-     parser.add_argument('--saml-sp-post', default=None,

-                         help="Post response URL")

-     parser.add_argument('--saml-sp-paos', default=None,

-                         help="PAOS response URL, used for ECP")

-     parser.add_argument('--no-saml-soap-logout', action='store_true',

-                         default=False,

-                         help="Disable Single Logout over SOAP")

-     parser.add_argument('--saml-secure-setup', action='store_true',

-                         default=True, help="Turn on all security checks")

-     parser.add_argument('--saml-nameid', default='unspecified',

-                         choices=SAML2_NAMEID_MAP.keys(),

-                         help="SAML NameID format to use")

-     parser.add_argument('--saml-sp-name', default=None,

-                         help="The SP name to register with the IdP")

-     parser.add_argument('--saml-sp-description', default=None,

-                         help="The description of the SP to display on the " +

-                         "portal")

-     parser.add_argument('--saml-sp-visible', action='store_false',

-                         default=True,

-                         help="The SP is visible in the portal")

-     parser.add_argument('--saml-sp-image', default=None,

-                         help="Image to display for this SP on the portal")

-     parser.add_argument('--debug', action='store_true', default=False,

-                         help="Turn on script debugging")

-     parser.add_argument('--config-profile', default=None,

-                         help=argparse.SUPPRESS)

-     parser.add_argument('--uninstall', action='store_true',

+     parser.add_argument('--auth-location', default=PROTECTED,

+                         help="Where authentication is enforced")

+     parser.add_argument('--uninstall', action='store_true', default=False,

                          help="Uninstall the server and all data")

  

+     openidc_add_arguments(parser)

+     saml2_add_arguments(parser)

+ 

      args = vars(parser.parse_args())

  

      if args['config_profile']:
@@ -409,64 +540,51 @@ 

      if args['port'] and not args['port'].isdigit():

          raise ValueError('Port number: %s is not an integer.' % args['port'])

  

-     # Validate that all path options begin with '/'

-     path_args = ['saml_base', 'saml_auth', 'saml_sp', 'saml_sp_logout',

-                  'saml_sp_post', 'saml_sp_paos']

-     for path_arg in path_args:

-         if args[path_arg] is not None and not args[path_arg].startswith('/'):

-             raise ValueError('--%s must begin with a / character.' %

-                              path_arg.replace('_', '-'))

+     # Exactly one on this list needs to be specified or we do nothing

+     sp_list = ['saml', 'openidc']

+     service_type = None

+     for sp in sp_list:

+         if args[sp]:

+             if service_type:

+                 raise ValueError('Multiple service types selected')

+             service_type = sp

  

-     # The saml_sp setting must be a subpath of saml_base since it is

-     # used as the MellonEndpointPath.

-     if not args['saml_sp'].startswith(args['saml_base']):

-         raise ValueError('--saml-sp must be a subpath of --saml-base.')

+     if not service_type:

+         # Since this was our default previously, let's be backwards compatible

+         # and default to SAML2

+         args['saml'] = True

+         service_type = 'saml'

  

-     # The samle_auth setting must be a subpath of saml_base otherwise

-     # the IdP cannot be identified by mod_auth_mellon.

-     if not args['saml_auth'].startswith(args['saml_base']):

-         raise ValueError('--saml-auth must be a subpath of --saml-base.')

+     if service_type == 'saml':

+         saml2_verify_arguments(args)

  

-     # The saml_sp_logout, saml_sp_post and saml_sp_paos settings must

-     # be subpaths of saml_sp (the mellon endpoint).

-     path_args = {'saml_sp_logout': 'logout',

-                  'saml_sp_post': 'postResponse',

-                  'saml_sp_paos': 'paosResponse'}

-     for path_arg, default_path in path_args.items():

-         if args[path_arg] is None:

-             args[path_arg] = '%s/%s' % (args['saml_sp'].rstrip('/'),

-                                         default_path)

+     elif service_type == 'openidc':

+         openidc_verify_arguments(args)

  

-         elif not args[path_arg].startswith(args['saml_sp']):

-             raise ValueError('--%s must be a subpath of --saml-sp' %

-                              path_arg.replace('_', '-'))

- 

-     # If saml_idp_url if being used, we require saml_sp_name to

-     # use when registering the SP.

-     if args['saml_idp_url'] and not args['saml_sp_name']:

-         raise ValueError('--saml-sp-name must be specified when using' +

-                          '--saml-idp-url')

- 

-     # At least one on this list needs to be specified or we do nothing

-     sp_list = ['saml']

-     present = False

-     for sp in sp_list:

-         if args[sp]:

-             present = True

-     if not present and not args['uninstall']:

-         raise ValueError('Nothing to install, please select a Service type.')

+     return service_type

  

  

  if __name__ == '__main__':

      out = 0

      openlogs()

      try:

-         parse_args()

+         service_type = parse_args()

  

          if 'uninstall' in args and args['uninstall'] is True:

-             uninstall()

+             logger.info('Uninstalling Service Provider')

+             #FXIME: ask confirmation

+ 

+             if service_type == 'saml':

+                 saml2_uninstall()

+             elif service_type == 'openidc':

+                 openidc_uninstall()

+ 

+             logger.info('Uninstalled Service Provider')

          else:

-             install()

+             if service_type == 'saml':

+                 saml2()

+             elif service_type == 'openidc':

+                 openidc()

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

          log_exception(e)

          if 'uninstall' in args and args['uninstall'] is True:

file modified
+4 -4
@@ -9,6 +9,7 @@ 

  from ipsilon.util.cookies import SecureCookie

  from ipsilon.util.log import Log

  import cherrypy

+ import time

  

  

  USERNAME_COOKIE = 'ipsilon_default_username'
@@ -118,10 +119,9 @@ 

          self.debug("User %s attributes: %s" % (username, repr(userdata)))

  

          if auth_type:

-             if userdata:

-                 userdata.update({'_auth_type': auth_type})

-             else:

-                 userdata = {'_auth_type': auth_type}

+             userdata.update({'_auth_type': auth_type})

+ 

+         userdata.update({'_auth_time': int(time.time())})

  

          # create session login including all the userdata just gathered

          session.login(username, userdata)

empty or binary file added
The added file is too large to be shown here, see it at: ipsilon/providers/openidc/auth.py
empty or binary file added
@@ -0,0 +1,69 @@ 

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

+ 

+ from __future__ import absolute_import

+ 

+ from ipsilon.util.plugin import PluginLoader

+ from ipsilon.util.log import Log

+ 

+ 

+ class OpenidCExtensionBase(Log):

+ 

+     def __init__(self, provider, name, display_name, scopes):

+         self.name = name

+         self.display_name = display_name

+         # A mapping of scope to display string for supported scopes

+         self.scopes = scopes

+         self.enabled = False

+         self.provider = None

+ 

+     def get_scopes(self):

+         if not self.enabled:

+             return []

+ 

+         return self.scopes.keys()

+ 

+     def get_display_name(self):

+         return self.display_name

+ 

+     def get_display_data(self, scopes):

+         if not self.enabled:

+             return {}

+ 

+         display_data = {}

+         for scope in scopes:

+             if scope in self.scopes:

+                 display_data[scope] = self.scopes[scope]

+         return display_data

+ 

+     def enable(self, provider):

+         self.enabled = True

+         self.provider = provider

+ 

+     def disable(self):

+         self.enabled = False

+         self.provider = None

+ 

+ 

+ FACILITY = 'openidc_extensions'

+ 

+ 

+ class LoadExtensions(Log):

+ 

+     def __init__(self):

+         self.plugins = PluginLoader(LoadExtensions,

+                                     FACILITY, 'OpenidCExtension', False)

+         self.plugins.get_plugin_data()

+ 

+         available = self.plugins.available.keys()

+         self.debug('Available Extensions: %s' % str(available))

+ 

+     def enable(self, enabled, provider):

+         for item in enabled:

+             if item not in self.plugins.available:

+                 self.debug('<%s> not available' % item)

+                 continue

+             self.debug('Enable OpenId Connect extension: %s' % item)

+             self.plugins.available[item].enable(provider)

+ 

+     def available(self):

+         return self.plugins.available

@@ -0,0 +1,20 @@ 

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

+ 

+ from __future__ import absolute_import

+ 

+ from ipsilon.providers.openidc.plugins.common import OpenidCExtensionBase

+ 

+ 

+ class OpenidCExtension(OpenidCExtensionBase):

+ 

+     def __init__(self, provider, *pargs):

+         name = 'ipsilon'

+         display_name = 'Ipsilon Token API'

+         scopes = {

+             'ipsilon_token': 'Ipsilon token verification'

+         }

+ 

+         super(OpenidCExtension, self).__init__(provider,

+                                                name,

+                                                display_name,

+                                                scopes)

@@ -0,0 +1,297 @@ 

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

+ 

+ from ipsilon.util.security import (generate_random_secure_string,

+                                    constant_time_string_comparison)

+ from ipsilon.util.data import Store, UNIQUE_DATA_TABLE

+ 

+ import json

+ import time

+ 

+ 

+ class OpenIDCStore(Store):

+     def __init__(self, database_url):

+         Store.__init__(self, database_url=database_url)

+ 

+     def registerDynamicClient(self, client):

+         data = {}

+ 

+         for key in client:

+             data[key] = json.dumps(client[key])

+ 

+         client_id = self.new_unique_data('client', data)

+ 

+         # Prepend client ID with D- to indicate that this is a dynamic client

+         return 'D-%s' % client_id

+ 

+     def registerStaticClient(self, client):

+         # TODO: Implement static client

+ 

+         client_id = None

+ 

+         # Prepend client ID with S- to indicate that this is a static client

+         return 'S-%s' % client_id

+ 

+     def getClient(self, client_id):

+         if client_id.startswith('D-'):

+             # This is a dynamically registered client

+             client_id = client_id[2:]

+             data = self.get_unique_data('client', client_id)

+         elif client_id.startswith('S-'):

+             # This is a statically configured client

+             client_id = client_id[2:]

+             # TODO: Get the configured client data

+             return None

+         else:

+             # No idea what this is

+             self.debug('Invalid client ID request: %s' % client_id)

+             return None

+ 

+         if len(data) < 1:

+             return None

+ 

+         datum = data[client_id]

+ 

+         for key in datum:

+             datum[key] = json.loads(datum[key])

+ 

+         return datum

+ 

+     def lookupToken(self, token, expected_type, return_expired=False):

+         if '_' not in token:

+             return None

+ 

+         checkfield = 'security_check'

+         if expected_type == 'Refresh' and token.startswith('R_'):

+             checkfield = 'refresh_security_check'

+             token = token[len('R_'):]

+ 

+         token_id, security_check = token.split('_', 1)

+ 

+         data = self.get_unique_data('token', token_id)

+ 

+         if len(data) < 1:

+             return None

+ 

+         datum = data[token_id]

+ 

+         if not constant_time_string_comparison(security_check,

+                                                datum[checkfield]):

+             return None

+ 

+         if not return_expired and \

+                 datum['expires_at'] <= int(time.time()):

+             return None

+ 

+         if expected_type and expected_type != 'Refresh' and \

+                 datum['type'] != expected_type:

+             return None

+ 

+         datum['scope'] = json.loads(datum['scope'])

+         datum['token_id'] = token_id

+ 

+         return datum

+ 

+     def storeAuthorizationIDToken(self, authz_code, signed_id_token):

+         token = self.lookupToken(authz_code, 'Authorization')

+         if not token:

+             return None

+         token['id_token'] = signed_id_token

+         self.update_token(token)

+ 

+     def update_token(self, token):

+         token_id = token['token_id']

+         del token['token_id']

+         token['scope'] = json.dumps(token['scope'])

+ 

+         self.save_unique_data('token', {token_id: token})

+ 

+     def refreshToken(self, refresh_token, client_id):

+         token = self.lookupToken(refresh_token, 'Refresh', True)

+ 

+         if not token:

+             return None

+ 

+         if not constant_time_string_comparison(token['client_id'],

+                                                client_id):

+             return None

+ 

+         if token['type'] != 'Bearer':

+             # Only Bearer tokens are supported

+             return None

+ 

+         if not token['refreshable']:

+             return None

+ 

+         if token['refreshable_until'] and \

+                 token['refreshable_until'] >= int(time.time()):

+             return None

+ 

+         token_security_check = generate_random_secure_string()

+         refresh_security_check = generate_random_secure_string(128)

+         expires_in = 3600

+         # TODO: Figure out values for this

+         refreshable_until = None

+ 

+         token['security_check'] = token_security_check

+         token['refresh_security_check'] = refresh_security_check

+         token['expires_at'] = int(time.time()) + expires_in

+         token['refreshable_until'] = refreshable_until

+ 

+         self.update_token(token)

+ 

+         token = '%s_%s' % (token['token_id'], token_security_check)

+         refresh_token = 'R_%s_%s' % (token['token_id'], refresh_security_check)

+ 

+         return {

+             'access_token': token,

+             'refresh_token': refresh_token,

+             'expires_in': expires_in

+         }

+ 

+     def issueToken(self, client_id, username, scope, issue_refresh,

+                    userinfocode):

+         token_security_check = generate_random_secure_string()

+ 

+         expires_in = 3600

+ 

+         token = {

+             'type': 'Bearer',

+             'security_check': token_security_check,

+             'client_id': client_id,

+             'username': username,

+             'scope': json.dumps(scope),

+             'expires_at': int(time.time()) + expires_in,

+             'issued_at': int(time.time()),

+             'refreshable': False,

+             'userinfocode': userinfocode

+         }

+ 

+         if issue_refresh:

+             token['refreshable'] = True

+             # TODO: Figure out time for this

+             token['refreshable_until'] = None

+             token['refresh_security_check'] = \

+                 generate_random_secure_string(128)

+ 

+         token_id = self.new_unique_data('token', token)

+ 

+         # The refresh token also has a prefix of R_ to make it distinguishable

+         if issue_refresh:

+             refresh_token = 'R_%s_%s' % (token_id,

+                                          token['refresh_security_check'])

+         else:

+             refresh_token = None

+ 

+         # The returned token is the token ID with appended to it the security

+         # check value.

+         # The token ID is used to lookup the token in the database, and the

+         # security check value is used to make the string slightly more

+         # random

+         token = '%s_%s' % (token_id, token_security_check)

+ 

+         return {

+             "token_id": token_id,

+             'access_token': token,

+             'refresh_token': refresh_token,

+             'expires_in': expires_in,

+         }

+ 

+     def invalidateToken(self, token):

+         self.del_unique_data('token', token)

+ 

+     def storeUserInfo(self, userinfo):

+         to_store = {}

+         for key in userinfo:

+             to_store[key] = json.dumps(userinfo[key])

+ 

+         return self.new_unique_data('userinfo', to_store)

+ 

+     def getUserInfo(self, userinfocode):

+         data = self.get_unique_data('userinfo', userinfocode)

+         if len(data) < 1:

+             return None

+ 

+         data = data[userinfocode]

+ 

+         userinfo = {}

+         for key in data:

+             userinfo[key] = json.loads(data[key])

+         return userinfo

+ 

+     def exchangeAuthorizationCode(self, authz_code):

+         token = self.lookupToken(authz_code, 'Authorization')

+         if not token:

+             return None

+ 

+         if 'issued_token' in token:

+             # This authorization code was already used before... We don't know

+             # whether this is a malfunctional client or if the authorization

+             # code got stolen, so let's just revoke the old key and refuse this

+             # request.

+             self.invalidateToken(token['issued_token'])

+             return None

+ 

+         new_token = self.issueToken(token['client_id'], token['username'],

+                                     token['scope'], True,

+                                     token['userinfocode'])

+         if not new_token:

+             return None

+ 

+         if 'id_token' in token:

+             new_token['id_token'] = token['id_token']

+ 

+         token['issued_token'] = new_token['token_id']

+         del new_token['token_id']

+ 

+         self.update_token(token)

+ 

+         return new_token

+ 

+     def issueAuthorizationCode(self, client_id, username, scope, userinfo,

+                                redirect_uri, userinfocode):

+         token_security_check = generate_random_secure_string()

+ 

+         expires_in = 600

+ 

+         token = {

+             'type': 'Authorization',

+             'security_check': token_security_check,

+             'client_id': client_id,

+             'username': username,

+             'scope': json.dumps(scope),

+             'expires_at': int(time.time()) + expires_in,

+             'userinfocode': userinfocode,

+             'redirect_uri': redirect_uri

+         }

+ 

+         token_id = self.new_unique_data('token', token)

+ 

+         # The returned token is the token ID with appended to it the security

+         # check value.

+         # The token ID is used to lookup the token in the database, and the

+         # security check value is used to make the string slightly more

+         # random

+         token = '%s_%s' % (token_id, token_security_check)

+ 

+         return token

+ 

+     def _cleanup(self):

+         # TODO: Clean up any tokens with expiry <= time.time()

+         return 0

+ 

+     def _initialize_schema(self):

+         q = self._query(self._db, 'client', UNIQUE_DATA_TABLE,

+                         trans=False)

+         q.create()

+         q._con.close()  # pylint: disable=protected-access

+         q = self._query(self._db, 'token', UNIQUE_DATA_TABLE,

+                         trans=False)

+         q.create()

+         q._con.close()  # pylint: disable=protected-access

+         q = self._query(self._db, 'userinfo', UNIQUE_DATA_TABLE,

+                         trans=False)

+         q.create()

+         q._con.close()  # pylint: disable=protected-access

+ 

+     def _upgrade_schema(self, old_version):

+         raise NotImplementedError()

@@ -0,0 +1,254 @@ 

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

+ 

+ from __future__ import absolute_import

+ 

+ from ipsilon.providers.common import ProviderBase, ProviderInstaller

+ from ipsilon.providers.openidc.plugins.common import LoadExtensions

+ from ipsilon.providers.openidc.store import OpenIDCStore

+ from ipsilon.providers.openidc.auth import OpenIDC

+ from ipsilon.util.plugin import PluginObject

+ from ipsilon.util import config as pconfig

+ from ipsilon.info.common import InfoMapping

+ 

+ import json

+ from jwcrypto.jwk import JWK, JWKSet

+ import os

+ import time

+ import uuid

+ 

+ 

+ class IdpProvider(ProviderBase):

+ 

+     def __init__(self, *pargs):

+         super(IdpProvider, self).__init__('openidc', 'openidc', *pargs)

+         self.mapping = InfoMapping()

+         self.keyset = None

+         self.page = None

+         self.datastore = None

+         self.server = None

+         self.basepath = None

+         self.extensions = LoadExtensions()

+         self.description = """

+ Provides OpenID Connect authentication infrastructure. """

+ 

+         self.new_config(

+             self.name,

+             pconfig.String(

+                 'database url',

+                 'Database URL for OpenID Connect storage',

+                 'openidc.sqlite'),

+             pconfig.Choice(

+                 'enabled extensions',

+                 'Choose the extensions to enable',

+                 self.extensions.available().keys()),

+             pconfig.String(

+                 'endpoint url',

+                 'The Absolute URL of the OpenID Connect provider',

+                 'http://localhost:8080/idp/openidc/'),

+             pconfig.String(

+                 'documentation url',

+                 'The Absolute URL of the OpenID Connect documentation',

+                 'https://ipsilonproject.org/doc/openidc/'),

+             pconfig.String(

+                 'policy url',

+                 'The Absolute URL of the OpenID Connect policy',

+                 'http://www.example.com/'),

+             pconfig.String(

+                 'tos url',

+                 'The Absolute URL of the OpenID Connect terms of service',

+                 'http://www.example.com/'),

+             pconfig.String(

+                 'idp key file',

+                 'The file where the OpenIDC keyset is stored.',

+                 'openidc.key'),

+             pconfig.String(

+                 'idp sig key id',

+                 'The key to use for signing.',

+                 ''),

+             pconfig.String(

+                 'idp subject salt',

+                 'The salt used for pairwise subjects.',

+                 None),

+             pconfig.MappingList(

+                 'default attribute mapping',

+                 'Defines how to map attributes',

+                 [['*', '*']]),

+             pconfig.ComplexList(

+                 'default allowed attributes',

+                 'Defines a list of allowed attributes, applied after mapping',

+                 ['*']),

+         )

+ 

+     @property

+     def endpoint_url(self):

+         url = self.get_config_value('endpoint url')

+         if url.endswith('/'):

+             return url

+         else:

+             return url+'/'

+ 

+     @property

+     def documentation_url(self):

+         url = self.get_config_value('documentation url')

+         if url.endswith('/'):

+             return url

+         else:

+             return url+'/'

+ 

+     @property

+     def policy_url(self):

+         url = self.get_config_value('policy url')

+         if url.endswith('/'):

+             return url

+         else:

+             return url+'/'

+ 

+     @property

+     def tos_url(self):

+         url = self.get_config_value('tos url')

+         if url.endswith('/'):

+             return url

+         else:

+             return url+'/'

+ 

+     @property

+     def enabled_extensions(self):

+         return self.get_config_value('enabled extensions')

+ 

+     @property

+     def idp_key_file(self):

+         return self.get_config_value('idp key file')

+ 

+     @property

+     def idp_sig_key_id(self):

+         return self.get_config_value('idp sig key id')

+ 

+     @property

+     def idp_subject_salt(self):

+         return self.get_config_value('idp subject salt')

+ 

+     @property

+     def default_attribute_mapping(self):

+         return self.get_config_value('default attribute mapping')

+ 

+     @property

+     def default_allowed_attributes(self):

+         return self.get_config_value('default allowed attributes')

+ 

+     @property

+     def supported_scopes(self):

+         supported = ['openid']

+         # Default scopes used in OpenID Connect claims

+         supported.extend(['profile', 'email', 'address', 'phone'])

+         for _, ext in self.extensions.available().items():

+             supported.extend(ext.get_scopes())

+         return supported

+ 

+     def get_tree(self, site):

+         self.page = OpenIDC(site, self)

+         # self.admin = AdminPage(site, self)

+ 

+         return self.page

+ 

+     def used_datastores(self):

+         return [self.datastore]

+ 

+     def init_idp(self):

+         self.keyset = JWKSet()

+         with open(self.idp_key_file, 'r') as keyfile:

+             loaded_keys = json.loads(keyfile.read())

+             for key in loaded_keys['keys']:

+                 self.keyset.add(JWK(**key))

+ 

+         self.datastore = OpenIDCStore(self.get_config_value('database url'))

+ 

+     def openid_connect_issuer_wf_rel(self, resource):

+         link = {

+             'rel': 'http://openid.net/specs/connect/1.0/issuer',

+             'href': self.endpoint_url

+         }

+         return {'links': [link]}

+ 

+     def on_enable(self):

+         super(IdpProvider, self).on_enable()

+         self.init_idp()

+         self.extensions.enable(self._config['enabled extensions'].get_value(),

+                                self)

+         self._root.webfinger.register_rel(

+             'http://openid.net/specs/connect/1.0/issuer',

+             self.openid_connect_issuer_wf_rel

+         )

+ 

+     def on_disable(self):

+         super(IdpProvider, self).on_enable()

+         self._root.webfinger.unregister_rel(

+             'http://openid.net/specs/connect/1.0/issuer'

+         )

+ 

+ 

+ class Installer(ProviderInstaller):

+ 

+     def __init__(self, *pargs):

+         super(Installer, self).__init__()

+         self.name = 'openidc'

+         self.pargs = pargs

+ 

+     def install_args(self, group):

+         group.add_argument('--openidc', choices=['yes', 'no'], default='yes',

+                            help='Configure OpenID Connect Provider')

+         group.add_argument('--openidc-dburi',

+                            help='OpenID Connect database URI')

+         group.add_argument('--openidc-subject-salt', default=None,

+                            help='Salt to use for pairwise subject subjects')

+         group.add_argument('--openidc-extensions', default='',

+                            help='List of OpenID Connect Extensions to enable')

+ 

+     def configure(self, opts, changes):

+         if opts['openidc'] != 'yes':

+             return

+ 

+         path = os.path.join(opts['data_dir'], 'openidc')

+         if not os.path.exists(path):

+             os.makedirs(path, 0700)

+ 

+         keyfile = os.path.join(path, 'openidc.key')

+         keyid = int(time.time())

+         keyset = JWKSet()

+         # We generate one RSA2048 signing key

+         rsasig = JWK(generate='RSA', size=2048, use='sig',

+                      kid='%s-sig' % keyid)

+         keyset.add(rsasig)

+         # We generate one RSA2048 encryption key

+         rsasig = JWK(generate='RSA', size=2048, use='enc',

+                      kid='%s-enc' % keyid)

+         keyset.add(rsasig)

+ 

+         with open(keyfile, 'w') as m:

+             m.write(keyset.export())

+ 

+         proto = 'https'

+         url = '%s://%s/%s/openidc/' % (

+             proto, opts['hostname'], opts['instance'])

+ 

+         subject_salt = uuid.uuid4().hex

+         if opts['openidc_subject_salt']:

+             subject_salt = opts['openidc_subject_salt']

+ 

+         # Add configuration data to database

+         po = PluginObject(*self.pargs)

+         po.name = 'openidc'

+         po.wipe_data()

+         po.wipe_config_values()

+         config = {'endpoint url': url,

+                   'database url': opts['openidc_dburi'] or

+                   opts['database_url'] % {

+                       'datadir': opts['data_dir'], 'dbname': 'openidc'},

+                   'enabled extensions': opts['openidc_extensions'],

+                   'idp key file': keyfile,

+                   'idp sig key id': '%s-sig' % keyid,

+                   'idp subject salt': subject_salt}

+         po.save_plugin_config(config)

+ 

+         # Update global config to add login plugin

+         po.is_enabled = True

+         po.save_enabled_state()

file modified
+5 -1
@@ -1,6 +1,7 @@ 

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

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

  

  from ipsilon.util.page import Page

+ from ipsilon.util.webfinger import WebFinger

  from ipsilon.util import errors

  from ipsilon.login.common import Login

  from ipsilon.login.common import Logout
@@ -33,6 +34,9 @@ 

          cherrypy.config['error_page.404'] = errors.Error_404(self._site)

          cherrypy.config['error_page.500'] = errors.Errors(self._site)

  

+         # set up WebFinger endpoint

+         self.webfinger = WebFinger(self._site)

+ 

          # now set up the default login plugins

          self.login = Login(self._site)

          self.logout = Logout(self._site)

@@ -0,0 +1,13 @@ 

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

+ 

+ import base64

+ from cryptography.hazmat.primitives.constant_time import bytes_eq

+ import os

+ 

+ 

+ def generate_random_secure_string(size=32):

+     return base64.urlsafe_b64encode(os.urandom(size))[:size]

+ 

+ 

+ def constant_time_string_comparison(stra, strb):

+     return bytes_eq(str(stra), str(strb))

@@ -0,0 +1,77 @@ 

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

+ 

+ import cherrypy

+ from ipsilon.util.page import Page

+ from ipsilon.util.log import Log

+ from ipsilon.util.endpoint import allow_iframe

+ 

+ import json

+ 

+ 

+ class WebFinger(Page, Log):

+ 

+     def __init__(self, site):

+         super(WebFinger, self).__init__(site)

+         self.supported_rels = {}

+ 

+     @allow_iframe

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

+         cherrypy.response.headers.update({

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

+             'Access-Control-Allow-Origin': '*'

+         })

+ 

+         if 'resource' not in kwargs:

+             raise cherrypy.HTTPError(400, 'Missing resource parameter')

+ 

+         resource = kwargs['resource']

+         self.debug('WebFinger request for %s' % resource)

+ 

+         response = {'subject': resource,

+                     'links': [],

+                     'properties': {}}

+         found = False

+ 

+         if 'rel' in kwargs:

+             rels = kwargs['rel']

+             if isinstance(rels, basestring):

+                 rels = [rels]

+         else:

+             rels = self.supported_rels.keys()

+ 

+         for rel in rels:

+             if rel in self.supported_rels:

+                 func = self.supported_rels[rel]

+                 rel_resp = func(resource)

+                 self.debug('Rel %s returned %s' % (rel, rel_resp))

+                 if 'links' in rel_resp:

+                     if len(rel_resp['links']) > 0:

+                         found = True

+                         response['links'].extend(rel_resp['links'])

+                 if 'properties' in rel_resp:

+                     if len(rel_resp['properties']) > 0:

+                         found = True

+                         response['properties'].update(rel_resp['properties'])

+ 

+         if not found:

+             # None of the plugins had any info, we don't know this resource

+             raise cherrypy.HTTPError(404, 'No info about resource found')

+ 

+         response['subject'] = resource

+ 

+         return json.dumps(response)

+ 

+     def register_rel(self, rel, function):

+         if rel in self.supported_rels:

+             raise KeyError('Rel %s already registered' % rel)

+ 

+         self.debug('WebFinger rel %s registered as %s'

+                    % (rel, function))

+         self.supported_rels[rel] = function

+ 

+     def unregister_rel(self, rel):

+         if rel not in self.supported_rels:

+             raise KeyError('Rel %s not registered' % rel)

+ 

+         self.debug('WebFinger rel %s unregistered' % rel)

+         del self.supported_rels[rel]

file modified
+1 -1
@@ -47,7 +47,7 @@ 

  Where saml2 authdata is available (default: /)

  .TP

  \fB\-\-saml\-auth\fR \fISAML_AUTH\fR

- Where saml2 authentication is enforced. The default is /saml2protected. This only applies when configuring Apache.

+ Where saml2 authentication is enforced. The default is /protected. This only applies when configuring Apache.

  .TP

  \fB\-\-saml\-sp\fR \fISAML_SP\fR

  Where saml communication happens. The default is /saml2.

file modified
+17 -1
@@ -12,6 +12,8 @@ 

  from ipsilon.tools.certs import Certificate

  from ipsilon.providers.saml2idp import IdpMetadataGenerator

  

+ from jwcrypto.jwk import JWK, JWKSet

+ 

  

  logger = None

  
@@ -32,9 +34,13 @@ 

  CREATE TABLE login_config (name TEXT,option TEXT,value TEXT);

  INSERT INTO login_config VALUES('global', 'enabled', 'testauth');

  CREATE TABLE provider_config (name TEXT,option TEXT,value TEXT);

- INSERT INTO provider_config VALUES('global', 'enabled', 'saml2');

+ INSERT INTO provider_config VALUES('global', 'enabled', 'saml2,openidc');

  INSERT INTO provider_config VALUES('saml2', 'idp storage path',

                                     '${workdir}/saml2');

+ INSERT INTO provider_config VALUES('openidc', 'idp key file',

+                                    '${workdir}/openidc.key');

+ INSERT INTO provider_config VALUES('openidc', 'idp sig key id',

+                                    'quickstart');

  '''

  

  USERS_TEMPLATE='''
@@ -97,6 +103,16 @@ 

                                  timedelta(validity))

      meta.output(os.path.join(workdir, 'saml2', 'metadata.xml'))

  

+     # Also initalize OpenID Connect

+     keyfile = os.path.join(workdir, 'openidc.key')

+     keyset = JWKSet()

+     # We generate one RSA2048 signing key

+     rsasig = JWK(generate='RSA', size=2048, use='sig', kid='quickstart')

+     keyset.add(rsasig)

+     with open(keyfile, 'w') as m:

+ 	m.write(keyset.export())

+ 

+ 

  if __name__ == '__main__':

  

      args = parse_args()

@@ -1,8 +1,15 @@ 

  Alias /${instance}/ui ${staticdir}/ui

  Alias /.well-known ${wellknowndir}

  Alias /${instance}/cache /var/cache/ipsilon

+ Redirect /${instance}/.well-known/webfinger /${instance}/webfinger

+ 

  WSGIScriptAlias /${instance} ${ipsilondir}/ipsilon

  WSGIDaemonProcess ${instance} user=${sysuser} group=${sysuser} home=${datadir} display-name=ipsilon-${instance}

+ # This header is required to be passed for OIDC client_secret_basic

+ WSGIPassAuthorization On

+ # Without this, getting the private key in jwcrypto/jwk.py, line 430, fails

+ # Fix from https://github.com/pyca/cryptography/issues/2299#issuecomment-197075190

+ WSGIApplicationGroup %{GLOBAL}

  ${wsgi_socket}

  

  <Location /${instance}>

@@ -0,0 +1,12 @@ 

+ OIDCRedirectURI "${redirect_uri}"

+ OIDCCryptoPassphrase "${crypto_passphrase}"

+ OIDCProviderMetadataURL "${idp_metadata_url}"

+ OIDCClientID "${client_id}"

+ OIDCClientSecret "${client_secret}"

+ OIDCSSLValidateServer ${validate_server}

+ OIDCResponseType "${response_type}"

+ 

+ <Location ${auth_location}>

+     AuthType openid-connect

+     Require valid-user

+ </Location>

@@ -26,7 +26,7 @@ 

  

  ${saml_auth}

  

- ${sp}Alias /saml2protected /usr/share/ipsilon/ui/saml2sp

+ ${sp}Alias /protected /usr/share/ipsilon/ui/saml2sp

  ${sp}

  ${sp}<Directory /usr/share/ipsilon/ui/saml2sp>

  ${sp}    <IfModule mod_nss.c>

@@ -0,0 +1,58 @@ 

+ {% extends "master.html" %}

+ {% block main %}

+ 

+ <div class="col-sm-12">

+   <p>The OpenID Connect client <b>

+ {%- if client['homepage'] %}

+     <a href="{{ client['homepage'] }}">{{ client['name'] }}</a>

+ {% else %}

+   {{ client['name'] }}

+ {%- endif %}

+   </b> is asking

+      to authorize access for <b>{{ username }}</b>.</p>

+   <p>Please review the authorization details</p>

+ 

+ {%- if client['policy'] %}

+   <p><a href="{{ client['policy'] }}">Client privacy policy</a></p>

+ {% endif %}

+ {%- if client['tos'] %}

+   <p><a href="{{ client['tos'] }}">Client terms of service</a></p>

+ {%- endif %}

+ </div>

+ 

+ <div class="col-sm-7 col-md-6 col-lg-5 login">

+   <form class="form-horizontal" role="form" id="consent_form" action="{{ action }}" method="post" enctype="application/x-www-form-urlencoded">

+     <input type="hidden" name="ipsilon_transaction_id" id="ipsilon_transaction_id" value="{{ ipsilon_transaction_id }}">

+ 

+     <div class="alert alert-danger">

+ {%- for item in claim_requests|dictsort %}

+         <div class="form-group">

+             <div class="col-sm-10 col-md-10">

+               {{ item[1]['display_name'] }}

+             </div>

+             <div class="col-sm-10 col-md-10">{{ item[1]['value'] }}</div>

+         </div>

+ {%- endfor %}

+ {%- for item in scopes|dictsort %}

+       <b>{{ item[0] }}</b>

+ {%- for item in item[1]|dictsort %}

+         <div class="form-group">

+             <div class="col-sm-10 col-md-10">

+               <!-- Empty, so that values come on the right hand -->

+             </div>

+             <div class="col-sm-10 col-md-10">{{ item[1] }}</div>

+         </div>

+ {%- endfor %}

+ {%- endfor %}

+     </div>

+ 

+     <div class="form-group">

+       <div class="col-sm-offset-2 col-md-offset-2 col-xs-12 col-sm-10 col-md-10 submit">

+         <button type="submit" name="decided_deny" value="Reject" class="btn btn-primary btn-lg" tabindex="3">Reject</button>

+         <button type="submit" name="decided_allow" value="Allow" class="btn btn-primary btn-lg" tabindex="3">Allow</button>

+       </div>

+     </div>

+   </form>

+ </div>

+ 

+ {% endblock %}

@@ -0,0 +1,23 @@ 

+ {% extends "master.html" %}

+ {% block main %}

+ <div class="col-sm-12">

+   <p>If you are not redirected automatically, please click Continue.</p>

+ </div>

+ 

+ <div class="col-sm-7 col-md-6 col-lg-5 login">

+ 

+   <div class="form-group">

+     <form id="openidc_response_form" method="POST" action="{{ redirect_url }}">

+ {%- for item in response_info|dictsort %}

+       <input type="hidden" name="{{ item[0] }}" value="{{ item[1] }}">

+ {%- endfor %}

+       <button type="submit" value="Continue" class="btn btn-primary btn-lg" tabindex="1">Continue</button>

+     </form>

+   </div>

+ </div>

+ 

+ <script type="text/javascript">

+   // TODO: Uncomment this, this is used for testing

+   //document.getElementById('openidc_response_form').submit();

+ </script>

+ {% endblock %}

file modified
+2 -2
@@ -33,8 +33,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',

file modified
+2 -2
@@ -61,8 +61,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',

file modified
+27
@@ -235,6 +235,27 @@ 

          return [method, self.new_url(referer, action_url),

                  {'headers': headers, 'data': payload}]

  

+     def handle_openidc_form(self, page):

+         if not isinstance(page, PageTree):

+             raise TypeError("Expected PageTree object")

+ 

+         if not page.first_value('//title/text()') == \

+                 'Submitting...':

+             raise WrongPage('Not OpenIDC autosubmit form')

+ 

+         url = page.make_referer()

+         if '#' not in url:

+             raise WrongPage('Not OpenIDC fragment submit page')

+         url, arguments = url.split('#', 1)

+ 

+         arguments = arguments.split('&')

+         params = {'response_mode': 'fragment'}

+         for argument in arguments:

+             key, value = argument.split('=')

+             params[key] = value

+ 

+         return ['post', url, {'data': params}]

+ 

      def fetch_page(self, idp, target_url, follow_redirect=True, krb=False):

          """

          Fetch a page and parse the response code to determine what to do
@@ -294,6 +315,12 @@ 

                  except WrongPage:

                      pass

  

+                 try:

+                     (action, url, args) = self.handle_openidc_form(page)

+                     continue

+                 except WrongPage:

+                     pass

+ 

                  # Either we got what we wanted, or we have to stop anyway

                  return page

              else:

file modified
+2
@@ -64,6 +64,8 @@ 

  LoadModule mpm_prefork_module modules/mod_mpm_prefork.so

  LoadModule wsgi_module modules/mod_wsgi.so

  LoadModule auth_gssapi_module modules/mod_auth_gssapi.so

+ # openidc needs to be before mellon: https://bugzilla.redhat.com/show_bug.cgi?id=1332729

+ LoadModule auth_openidc_module modules/mod_auth_openidc.so

  LoadModule auth_mellon_module modules/mod_auth_mellon.so

  

  Listen ${HTTPADDR}:${HTTPPORT} https

file modified
+2 -2
@@ -36,8 +36,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',

file modified
+2 -2
@@ -38,8 +38,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',

file added
+349
@@ -0,0 +1,349 @@ 

+ #!/usr/bin/python

+ #

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

+ 

+ from helpers.common import IpsilonTestBase  # pylint: disable=relative-import

+ from helpers.http import HttpSessions  # pylint: disable=relative-import

+ import os

+ import json

+ import pwd

+ import sys

+ import requests

+ import hashlib

+ from string import Template

+ 

+ idp_g = {'TEMPLATES': '${TESTDIR}/templates/install',

+          'CONFDIR': '${TESTDIR}/etc',

+          'DATADIR': '${TESTDIR}/lib',

+          'CACHEDIR': '${TESTDIR}/cache',

+          'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

+          'STATICDIR': '${ROOTDIR}',

+          'BINDIR': '${ROOTDIR}/ipsilon',

+          'WSGI_SOCKET_PREFIX': '${TESTDIR}/${NAME}/logs/wsgi'}

+ 

+ 

+ idp_a = {'hostname': '${ADDRESS}:${PORT}',

+          'admin_user': '${TEST_USER}',

+          'system_user': '${TEST_USER}',

+          'instance': '${NAME}',

+          'testauth': 'yes',

+          'pam': 'no',

+          'gssapi': 'no',

+          'ipa': 'no',

+          'openidc': 'yes',

+          'openidc_subject_salt': 'testcase',

+          'server_debugging': 'True'}

+ 

+ 

+ sp1_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

+          'OPENIDC_TEMPLATE': '${TESTDIR}/templates/install/openidc/rp.conf',

+          'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+          'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

+ 

+ 

+ sp1_a = {'hostname': '${ADDRESS}',

+          'auth_location': '/sp',

+          'openidc': 'yes',

+          'openidc_idp_url': 'https://127.0.0.10:45080/idp1',

+          'openidc_response_type': 'code',

+          'openidc_skip_ssl_validation': 'yes',

+          'httpd_user': '${TEST_USER}'}

+ 

+ 

+ sp2_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

+          'OPENIDC_TEMPLATE': '${TESTDIR}/templates/install/openidc/rp.conf',

+          'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+          'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

+ 

+ 

+ sp2_a = {'hostname': '${ADDRESS}',

+          'auth_location': '/sp',

+          'openidc': 'yes',

+          'openidc_idp_url': 'https://127.0.0.10:45080/idp1',

+          'openidc_response_type': 'id_token',

+          'openidc_subject_type': 'public',

+          'openidc_skip_ssl_validation': 'yes',

+          'httpd_user': '${TEST_USER}'}

+ 

+ 

+ sp3_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

+          'OPENIDC_TEMPLATE': '${TESTDIR}/templates/install/openidc/rp.conf',

+          'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+          'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

+ 

+ 

+ sp3_a = {'hostname': '${ADDRESS}',

+          'auth_location': '/sp',

+          'openidc': 'yes',

+          'openidc_idp_url': 'https://127.0.0.10:45080/idp1',

+          'openidc_response_type': 'id_token token',

+          'openidc_skip_ssl_validation': 'yes',

+          'httpd_user': '${TEST_USER}'}

+ 

+ 

+ def fixup_sp_httpd(httpdir):

+     location = """

+ AddOutputFilter INCLUDES .html

+ 

+ Alias /sp ${HTTPDIR}/sp

+ 

+ <Directory ${HTTPDIR}/sp>

+     Options +Includes

+     Require all granted

+ </Directory>

+ """

+     t = Template(location)

+     text = t.substitute({'HTTPDIR': httpdir})

+     with open(httpdir + '/conf.d/ipsilon-openidc.conf', 'a') as f:

+         f.write(text)

+ 

+     index = """<!--#printenv -->"""

+     os.mkdir(httpdir + '/sp')

+     with open(httpdir + '/sp/index.html', 'w') as f:

+         f.write(index)

+ 

+ 

+ def convert_to_dict(envlist):

+     values = {}

+     for pair in envlist.split('\n'):

+         if pair.find('=') > 0:

+             (key, value) = pair.split('=', 1)

+             if key.startswith('OIDC_') and not key.endswith('_0'):

+                 values[key] = value

+     return values

+ 

+ 

+ def check_info_results(text, expected):

+     """

+     Logout, login, fetch RP page to get the info variables and

+     compare the OIDC_CLAIM_ ones to what we expect.

+     """

+ 

+     # Confirm that the expected values are in the output and that there

+     # are no unexpected OIDC_CLAIM_ vars, and drop the _0 version.

+     data = convert_to_dict(text)

+ 

+     toreturn = {}

+     toreturn['access_token'] = data.pop('OIDC_access_token', None)

+     toreturn['access_token_expires'] = data.pop('OIDC_access_token_expires',

+                                                 None)

+ 

+     for key in expected:

+         item = data.pop('OIDC_CLAIM_' + key)

+         if item != expected[key]:

+             raise ValueError('Expected %s, got %s' % (expected[key], item))

+ 

+     # Ignore a couple of attributes

+     ignored = ['exp', 'c_hash', 'at_hash', 'aud', 'nonce', 'iat', 'auth_time',

+                'azp']

+     for attr in ignored:

+         data.pop('OIDC_CLAIM_%s' % attr, None)

+ 

+     if len(data) > 0:

+         raise ValueError('Unexpected values %s' % data)

+ 

+     return toreturn

+ 

+ 

+ class IpsilonTest(IpsilonTestBase):

+ 

+     def __init__(self):

+         super(IpsilonTest, self).__init__('openidc', __file__)

+ 

+     def setup_servers(self, env=None):

+         print "Installing IDP server"

+         name = 'idp1'

+         addr = '127.0.0.10'

+         port = '45080'

+         idp = self.generate_profile(idp_g, idp_a, name, addr, port)

+         conf = self.setup_idp_server(idp, name, addr, port, env)

+ 

+         print "Starting IDP's httpd server"

+         self.start_http_server(conf, env)

+ 

+         print "Installing first SP server"

+         name = 'sp1'

+         addr = '127.0.0.11'

+         port = '45081'

+         sp = self.generate_profile(sp1_g, sp1_a, name, addr, port)

+         conf = self.setup_sp_server(sp, name, addr, port, env)

+         fixup_sp_httpd(os.path.dirname(conf))

+ 

+         print "Starting first SP's httpd server"

+         self.start_http_server(conf, env)

+ 

+         print "Installing second SP server"

+         name = 'sp2'

+         addr = '127.0.0.12'

+         port = '45082'

+         sp = self.generate_profile(sp2_g, sp2_a, name, addr, port)

+         conf = self.setup_sp_server(sp, name, addr, port, env)

+         fixup_sp_httpd(os.path.dirname(conf))

+ 

+         print "Starting second SP's httpd server"

+         self.start_http_server(conf, env)

+ 

+         print "Installing third SP server"

+         name = 'sp3'

+         addr = '127.0.0.13'

+         port = '45083'

+         sp = self.generate_profile(sp3_g, sp3_a, name, addr, port)

+         conf = self.setup_sp_server(sp, name, addr, port, env)

+         fixup_sp_httpd(os.path.dirname(conf))

+ 

+         print "Starting third SP's httpd server"

+         self.start_http_server(conf, env)

+ 

+ 

+ if __name__ == '__main__':

+ 

+     idpname = 'idp1'

+     sp1name = 'sp1'

+     sp2name = 'sp2'

+     sp3name = 'sp3'

+     user = pwd.getpwuid(os.getuid())[0]

+ 

+     sess = HttpSessions()

+     sess.add_server(idpname, 'https://127.0.0.10:45080', user, 'ipsilon')

+     sess.add_server(sp1name, 'https://127.0.0.11:45081')

+     sess.add_server(sp2name, 'https://127.0.0.12:45082')

+     sess.add_server(sp3name, 'https://127.0.0.13:45083')

+ 

+     print "openidc: Authenticate to IDP ...",

+     try:

+         sess.auth_to_idp(idpname)

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

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "openidc: Registering test client ...",

+     try:

+         client_info = {

+             'redirect_uris': ['https://invalid/'],

+             'response_types': ['code'],

+             'grant_types': ['authorization_code'],

+             'application_type': 'web',

+             'client_name': 'Test suite client',

+             'client_uri': 'https://invalid/',

+             'token_endpoint_auth_method': 'client_secret_post'

+         }

+         r = requests.post('https://127.0.0.10:45080/idp1/openidc/Registration',

+                           json=client_info)

+         r.raise_for_status()

+         reg_resp = r.json()

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

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "openidc: Access first SP Protected Area ...",

+     try:

+         page = sess.fetch_page(idpname, 'https://127.0.0.11:45081/sp/')

+         h = hashlib.sha256()

+         h.update('127.0.0.11')

+         h.update(user)

+         h.update('testcase')

+         expect = {

+             'sub': h.hexdigest(),

+             'iss': 'https://127.0.0.10:45080/idp1/openidc/',

+             'amr': json.dumps([]),

+             'acr': '0'

+         }

+         token = check_info_results(page.text, expect)

+     except ValueError, e:

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "openidc: Retrieving token info ...",

+     try:

+         # Testing token without client auth

+         r = requests.post('https://127.0.0.10:45080/idp1/openidc/TokenInfo',

+                           data={'token': token['access_token']})

+         if r.status_code != 401:

+             raise Exception('No 401 provided')

+ 

+         # Testing token where we removed part of token ID

+         r = requests.post('https://127.0.0.10:45080/idp1/openidc/TokenInfo',

+                           data={'token': token['access_token'][1:],

+                                 'client_id': reg_resp['client_id'],

+                                 'client_secret': reg_resp['client_secret']})

+         r.raise_for_status()

+         info = r.json()

+         if info['active']:

+             raise Exception('Token active')

+ 

+         # Testing token where we rempoved part of check string

+         r = requests.post('https://127.0.0.10:45080/idp1/openidc/TokenInfo',

+                           data={'token': token['access_token'][:-1],

+                                 'client_id': reg_resp['client_id'],

+                                 'client_secret': reg_resp['client_secret']})

+         r.raise_for_status()

+         info = r.json()

+         if info['active']:

+             raise Exception('Token active')

+ 

+         # Testing valid token

+         r = requests.post('https://127.0.0.10:45080/idp1/openidc/TokenInfo',

+                           data={'token': token['access_token'],

+                                 'client_id': reg_resp['client_id'],

+                                 'client_secret': reg_resp['client_secret']})

+         r.raise_for_status()

+         info = r.json()

+         if 'error' in info:

+             raise Exception('Token introspection returned error: %s'

+                             % info['error'])

+         if not info['active']:

+             raise Exception('Token not active')

+         if info['username'] != user:

+             raise Exception('Token for different user?')

+         if info['token_type'] != 'Bearer':

+             raise Exception('Unexpected token type: %s' % info['token_type'])

+ 

+         scopes_needed = ['openid']

+         info['scope'] = info['scope'].split(' ')

+         for scope in scopes_needed:

+             if scope not in info['scope']:

+                 raise Exception('Missing scope: %s' % scope)

+             info['scope'].remove(scope)

+         if len(info['scope']) != 0:

+             raise Exception('Unexpected scopes found: %s' % info['scope'])

+     except ValueError, e:

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "openidc: Access second SP Protected Area ...",

+     try:

+         page = sess.fetch_page(idpname, 'https://127.0.0.12:45082/sp/')

+         expect = {

+             'sub': user,

+             'iss': 'https://127.0.0.10:45080/idp1/openidc/',

+             'amr': json.dumps([]),

+             'acr': '0'

+         }

+         check_info_results(page.text, expect)

+     except ValueError, e:

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

+ 

+     print "openidc: Access third SP Protected Area ...",

+     try:

+         page = sess.fetch_page(idpname, 'https://127.0.0.13:45083/sp/')

+         h = hashlib.sha256()

+         h.update('127.0.0.13')

+         h.update(user)

+         h.update('testcase')

+         expect = {

+             'sub': h.hexdigest(),

+             'iss': 'https://127.0.0.10:45080/idp1/openidc/',

+             'amr': json.dumps([]),

+             'acr': '0'

+         }

+         check_info_results(page.text, expect)

+     except ValueError, e:

+         print >> sys.stderr, " ERROR: %s" % repr(e)

+         sys.exit(1)

+     print " SUCCESS"

file modified
+3 -2
@@ -29,6 +29,7 @@ 

           'system_user': '${TEST_USER}',

           'instance': '${NAME}',

           'openid': 'False',

+          'openidc': 'False',

           'testauth': 'yes',

           'pam': 'no',

           'gssapi': 'no',
@@ -38,8 +39,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',

file modified
+4 -4
@@ -32,8 +32,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',
@@ -43,8 +43,8 @@ 

  

  sp2_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

           'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-          'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-          'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+          'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+          'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  sp2_a = {'hostname': '${ADDRESS}',

           'saml_idp_url': 'https://127.0.0.10:45080/idp1',

file modified
+4 -4
@@ -34,8 +34,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',
@@ -46,8 +46,8 @@ 

  

  sp2_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

           'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-          'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-          'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+          'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+          'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  sp2_a = {'hostname': '${ADDRESS}',

           'saml_idp_url': 'https://idp.ipsilon.dev:45080/idp1',

file modified
+2 -2
@@ -33,8 +33,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',

file modified
+2 -2
@@ -36,8 +36,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',

file modified
+2 -2
@@ -38,8 +38,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',

file modified
+6 -6
@@ -33,8 +33,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',
@@ -45,8 +45,8 @@ 

  

  sp2_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

           'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-          'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-          'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+          'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+          'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp2_a = {'hostname': '${ADDRESS}',
@@ -56,8 +56,8 @@ 

  

  sp3_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

           'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-          'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-          'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+          'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+          'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp3_a = {'hostname': '${ADDRESS}',

file modified
+2 -2
@@ -33,8 +33,8 @@ 

  

  sp_g = {'HTTPDCONFD': '${TESTDIR}/${NAME}/conf.d',

          'SAML2_TEMPLATE': '${TESTDIR}/templates/install/saml2/sp.conf',

-         'SAML2_CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-saml.conf',

-         'SAML2_HTTPDIR': '${TESTDIR}/${NAME}/saml2'}

+         'CONFFILE': '${TESTDIR}/${NAME}/conf.d/ipsilon-%s.conf',

+         'HTTPDIR': '${TESTDIR}/${NAME}/%s'}

  

  

  sp_a = {'hostname': '${ADDRESS}',

no initial comment

The review was mostly done out-of-band in e-mail and IRC which is a bit of a loss to history but this change was rather massive.

The in-tree functional tests are ok.

The external certification tests pass, https://op.certification.openid.net:60584

The remaining FIXME are going to have tickets generated so they can be fixed in the future. They are generally (hopefully) minor in nature, like supporting multiple signing algorithms.

ACK

rebased

7 years ago

Pull-Request has been merged by puiterwijk

7 years ago
Metadata
Changes Summary 37
+1 -0
file changed
Makefile
+1 -1
file changed
README.md
+1 -1
file changed
doc/saml-integration.rst
+230 -112
file changed
ipsilon/install/ipsilon-client-install
+4 -4
file changed
ipsilon/login/common.py
+0
file added
ipsilon/providers/openidc/__init__.py
+1199
file added
ipsilon/providers/openidc/auth.py
+0
file added
ipsilon/providers/openidc/plugins/__init__.py
+69
file added
ipsilon/providers/openidc/plugins/common.py
+20
file added
ipsilon/providers/openidc/plugins/ipsilon.py
+297
file added
ipsilon/providers/openidc/store.py
+254
file added
ipsilon/providers/openidcp.py
+5 -1
file changed
ipsilon/root.py
+13
file added
ipsilon/util/security.py
+77
file added
ipsilon/util/webfinger.py
+1 -1
file changed
man/ipsilon-client-install.1
+17 -1
file changed
quickrun.py
+7 -0
file changed
templates/install/idp.conf
+12
file added
templates/install/openidc/rp.conf
+1 -1
file changed
templates/install/saml2/sp.conf
+58
file added
templates/openidc/consent_form.html
+23
file added
templates/openidc/form_response.html
+2 -2
file changed
tests/attrs.py
+2 -2
file changed
tests/fconf.py
+27 -0
file changed
tests/helpers/http.py
+2 -0
file changed
tests/httpd.conf
+2 -2
file changed
tests/ldap.py
+2 -2
file changed
tests/ldapdown.py
+349
file added
tests/openidc.py
+3 -2
file changed
tests/pgdb.py
+4 -4
file changed
tests/test1.py
+4 -4
file changed
tests/testgssapi.py
+2 -2
file changed
tests/testlogout.py
+2 -2
file changed
tests/testmapping.py
+2 -2
file changed
tests/testnameid.py
+6 -6
file changed
tests/testrest.py
+2 -2
file changed
tests/trans.py