1 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
3 from ipsilon.providers.common import ProviderBase, ProviderPageBase, \
5 from ipsilon.providers.saml2.auth import AuthenticateRequest
6 from ipsilon.providers.saml2.logout import LogoutRequest
7 from ipsilon.providers.saml2.admin import Saml2AdminPage
8 from ipsilon.providers.saml2.rest import Saml2RestBase
9 from ipsilon.providers.saml2.provider import IdentityProvider
10 from ipsilon.tools.certs import Certificate
11 from ipsilon.tools import saml2metadata as metadata
12 from ipsilon.tools import files
13 from ipsilon.util.user import UserSession
14 from ipsilon.util.plugin import PluginObject
15 from ipsilon.util import config as pconfig
17 from datetime import timedelta
24 class Redirect(AuthenticateRequest):
26 def GET(self, *args, **kwargs):
28 query = cherrypy.request.query_string
30 login = self.saml2login(query)
31 return self.auth(login)
34 class POSTAuth(AuthenticateRequest):
36 def POST(self, *args, **kwargs):
38 request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
39 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
41 login = self.saml2login(request)
42 login.set_msgRelayState(relaystate)
43 return self.auth(login)
46 class Continue(AuthenticateRequest):
48 def GET(self, *args, **kwargs):
50 session = UserSession()
51 user = session.get_user()
52 transdata = self.trans.retrieve()
53 self.stage = transdata['saml2_stage']
56 self.debug("User is marked anonymous?!")
57 # TODO: Return to SP with auth failed error
58 raise cherrypy.HTTPError(401)
60 self.debug('Continue auth for %s' % user.name)
62 if 'saml2_request' not in transdata:
63 self.debug("Couldn't find Request dump?!")
64 # TODO: Return to SP with auth failed error
65 raise cherrypy.HTTPError(400)
66 dump = transdata['saml2_request']
69 login = self.cfg.idp.get_login_handler(dump)
70 except Exception, e: # pylint: disable=broad-except
71 self.debug('Failed to load status from dump: %r' % e)
74 self.debug("Empty Request dump?!")
75 # TODO: Return to SP with auth failed error
76 raise cherrypy.HTTPError(400)
78 return self.auth(login)
81 class RedirectLogout(LogoutRequest):
83 def GET(self, *args, **kwargs):
84 query = cherrypy.request.query_string
86 relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
87 response = kwargs.get(lasso.SAML2_FIELD_RESPONSE)
89 return self.logout(query,
90 relaystate=relaystate,
91 samlresponse=response)
94 class SSO(ProviderPageBase):
96 def __init__(self, *args, **kwargs):
97 super(SSO, self).__init__(*args, **kwargs)
98 self.Redirect = Redirect(*args, **kwargs)
99 self.POST = POSTAuth(*args, **kwargs)
100 self.Continue = Continue(*args, **kwargs)
103 class SLO(ProviderPageBase):
105 def __init__(self, *args, **kwargs):
106 super(SLO, self).__init__(*args, **kwargs)
107 self.debug('SLO init')
108 self.Redirect = RedirectLogout(*args, **kwargs)
112 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
113 # five years (approximately)
114 METADATA_DEFAULT_VALIDITY_PERIOD = 365 * 5
117 class Metadata(ProviderPageBase):
118 def GET(self, *args, **kwargs):
120 body = self._get_metadata()
121 cherrypy.response.headers["Content-Type"] = "text/xml"
122 cherrypy.response.headers["Content-Disposition"] = \
123 'attachment; filename="metadata.xml"'
126 def _get_metadata(self):
127 if os.path.isfile(self.cfg.idp_metadata_file):
128 s = os.stat(self.cfg.idp_metadata_file)
129 if s.st_mtime > time.time() - METADATA_RENEW_INTERVAL:
130 with open(self.cfg.idp_metadata_file) as m:
133 # Otherwise generate and save
134 idp_cert = Certificate()
135 idp_cert.import_cert(self.cfg.idp_certificate_file,
136 self.cfg.idp_key_file)
138 validity = int(self.cfg.idp_metadata_validity)
139 meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
142 with open(self.cfg.idp_metadata_file, 'w+') as m:
147 class SAML2(ProviderPageBase):
149 def __init__(self, *args, **kwargs):
150 super(SAML2, self).__init__(*args, **kwargs)
151 self.metadata = Metadata(*args, **kwargs)
152 self.SSO = SSO(*args, **kwargs)
153 self.SLO = SLO(*args, **kwargs)
156 class IdpProvider(ProviderBase):
158 def __init__(self, *pargs):
159 super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
164 self.description = """
165 Provides SAML 2.0 authentication infrastructure. """
171 'Path to data storage accessible by the IdP.',
172 '/var/lib/ipsilon/saml2'),
175 'The IdP Metadata file generated at install time.',
178 'idp metadata validity',
179 'The IdP Metadata validity period (in days) to use when '
180 'generating new metadata.',
181 METADATA_DEFAULT_VALIDITY_PERIOD),
183 'idp certificate file',
184 'The IdP PEM Certificate generated at install time.',
188 'The IdP Certificate Key generated at install time.',
192 'The salt used for persistent Name IDs.',
195 'allow self registration',
196 'Allow authenticated users to register applications.',
199 'default allowed nameids',
200 'Default Allowed NameIDs for Service Providers.',
201 metadata.SAML2_NAMEID_MAP.keys(),
202 ['unspecified', 'persistent', 'transient', 'email',
203 'kerberos', 'x509']),
206 'Default NameID used by Service Providers.',
207 metadata.SAML2_NAMEID_MAP.keys(),
210 'default email domain',
211 'Used for users missing the email property.',
214 'default attribute mapping',
215 'Defines how to map attributes before returning them to SPs',
218 'default allowed attributes',
219 'Defines a list of allowed attributes, applied after mapping',
222 if cherrypy.config.get('debug', False):
225 logger = logging.getLogger('lasso')
226 lh = logging.StreamHandler(sys.stderr)
227 logger.addHandler(lh)
228 logger.setLevel(logging.DEBUG)
231 def allow_self_registration(self):
232 return self.get_config_value('allow self registration')
235 def idp_storage_path(self):
236 return self.get_config_value('idp storage path')
239 def idp_metadata_file(self):
240 return os.path.join(self.idp_storage_path,
241 self.get_config_value('idp metadata file'))
244 def idp_metadata_validity(self):
245 return self.get_config_value('idp metadata validity')
248 def idp_certificate_file(self):
249 return os.path.join(self.idp_storage_path,
250 self.get_config_value('idp certificate file'))
253 def idp_key_file(self):
254 return os.path.join(self.idp_storage_path,
255 self.get_config_value('idp key file'))
258 def idp_nameid_salt(self):
259 return self.get_config_value('idp nameid salt')
262 def default_allowed_nameids(self):
263 return self.get_config_value('default allowed nameids')
266 def default_nameid(self):
267 return self.get_config_value('default nameid')
270 def default_email_domain(self):
271 return self.get_config_value('default email domain')
274 def default_attribute_mapping(self):
275 return self.get_config_value('default attribute mapping')
278 def default_allowed_attributes(self):
279 return self.get_config_value('default allowed attributes')
281 def get_tree(self, site):
282 self.idp = self.init_idp()
283 self.page = SAML2(site, self)
284 self.admin = Saml2AdminPage(site, self)
285 self.rest = Saml2RestBase(site, self)
292 idp = IdentityProvider(self)
293 except Exception, e: # pylint: disable=broad-except
294 self.debug('Failed to init SAML2 provider: %r' % e)
297 self._root.logout.add_handler(self.name, self.idp_initiated_logout)
299 # Import all known applications
300 data = self.get_data()
303 if 'type' not in sp or sp['type'] != 'SP':
305 if 'name' not in sp or 'metadata' not in sp:
309 except Exception, e: # pylint: disable=broad-except
310 self.debug('Failed to add SP %s: %r' % (sp['name'], e))
315 super(IdpProvider, self).on_enable()
316 self.idp = self.init_idp()
317 if hasattr(self, 'admin'):
321 def idp_initiated_logout(self):
323 Logout all SP sessions when the logout comes from the IdP.
325 For the current user only.
327 self.debug("IdP-initiated SAML2 logout")
330 saml_sessions = us.get_provider_data('saml2')
331 if saml_sessions is None:
332 self.debug("No SAML2 sessions to logout")
334 session = saml_sessions.get_next_logout(remove=False)
338 # Add a fake session to indicate where the user should
339 # be redirected to when all SP's are logged out.
340 idpurl = self._root.instance_base_url()
341 saml_sessions.add_session("_idp_initiated_logout",
344 init_session = saml_sessions.find_session_by_provider(idpurl)
345 init_session.set_logoutstate(idpurl, "idp_initiated_logout", None)
346 saml_sessions.start_logout(init_session)
348 logout = self.idp.get_logout_handler()
349 logout.setSessionFromDump(session.session.dump())
350 logout.initRequest(session.provider_id)
352 logout.buildRequestMsg()
353 except lasso.Error, e:
354 self.error('failure to build logout request msg: %s' % e)
355 raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
358 raise cherrypy.HTTPRedirect(logout.msgUrl)
361 class IdpMetadataGenerator(object):
363 def __init__(self, url, idp_cert, expiration=None):
364 self.meta = metadata.Metadata(metadata.IDP_ROLE, expiration)
365 self.meta.set_entity_id('%s/saml2/metadata' % url)
366 self.meta.add_certs(idp_cert, idp_cert)
367 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-post'],
368 '%s/saml2/SSO/POST' % url)
369 self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
370 '%s/saml2/SSO/Redirect' % url)
371 self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
372 '%s/saml2/SLO/Redirect' % url)
373 self.meta.add_allowed_name_format(
374 lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
375 self.meta.add_allowed_name_format(
376 lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
377 self.meta.add_allowed_name_format(
378 lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
380 def output(self, path=None):
381 return self.meta.output(path)
384 class Installer(ProviderInstaller):
386 def __init__(self, *pargs):
387 super(Installer, self).__init__()
391 def install_args(self, group):
392 group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
393 help='Configure SAML2 Provider')
394 group.add_argument('--saml2-metadata-validity',
395 default=METADATA_DEFAULT_VALIDITY_PERIOD,
396 help=('Metadata validity period in days '
398 METADATA_DEFAULT_VALIDITY_PERIOD))
400 def configure(self, opts):
401 if opts['saml2'] != 'yes':
404 # Check storage path is present or create it
405 path = os.path.join(opts['data_dir'], 'saml2')
406 if not os.path.exists(path):
407 os.makedirs(path, 0700)
409 # Use the same cert for signing and ecnryption for now
410 cert = Certificate(path)
411 cert.generate('idp', opts['hostname'])
413 # Generate Idp Metadata
415 if opts['secure'].lower() == 'no':
417 url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
418 validity = int(opts['saml2_metadata_validity'])
419 meta = IdpMetadataGenerator(url, cert,
421 if 'gssapi' in opts and opts['gssapi'] == 'yes':
422 meta.meta.add_allowed_name_format(
423 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
425 meta.output(os.path.join(path, 'metadata.xml'))
427 # Add configuration data to database
428 po = PluginObject(*self.pargs)
431 po.wipe_config_values()
432 config = {'idp storage path': path,
433 'idp metadata file': 'metadata.xml',
434 'idp certificate file': cert.cert,
435 'idp key file': cert.key,
436 'idp nameid salt': uuid.uuid4().hex,
437 'idp metadata validity': opts['saml2_metadata_validity']}
438 po.save_plugin_config(config)
440 # Update global config to add login plugin
442 po.save_enabled_state()
444 # Fixup permissions so only the ipsilon user can read these files
445 files.fix_user_dirs(path, opts['system_user'])