Projet

Général

Profil

Télécharger (20,1 ko) Statistiques
| Branche: | Tag: | Révision:

root / mandaye / auth / saml2.py @ 42af34ad

1
import datetime
2
import os
3
import urllib2
4

    
5
import lasso
6

    
7
from urlparse import parse_qs
8

    
9
from mandaye import config, utils
10
from mandaye.saml import saml2utils
11
from mandaye.auth.authform import AuthForm
12
from mandaye.exceptions import MandayeSamlException, ImproperlyConfigured
13
from mandaye.response import _302, _401
14
from mandaye.log import logger
15
from mandaye.http import HTTPResponse, HTTPHeader
16

    
17
"""
18
Mandaye saml2 authentification support
19

    
20
To use it you must set the following options into your
21
virtual host :
22
 * saml2_idp_metadata: a link to your idp metadata
23
 * saml2_signature_public_key: a path to your public key
24
 * saml2_signature_private_key: a path to your private key
25

    
26
Optional options :
27
 * saml2_sp_logout_url: the url to logout the service provider
28
 * saml2_authnresp_binding: only post is supported for now
29
 * saml2_authnreq_http_method: only http_redirect at the moment
30
 * saml2_name_identifier_format: only persistent at the moment
31
 * saml2_metadata_url: saml end point of the metadata
32
 * saml2_single_sign_on_post_url: saml end point of single sign on post
33
 * saml2_single_logout_url: saml end point of logout
34
 * saml2_single_logout_return_url: saml end point of the single logout return
35
"""
36

    
37
# XXX: remove this for the 1.0. Keep it only for compability reasons.
38
END_POINTS_PATH = {
39
        'metadata': '/mandaye/metadata',
40
        'single_sign_on_post': '/mandaye/singleSignOnPost',
41
        'single_logout': '/mandaye/singleLogout',
42
        'single_logout_return': '/mandaye/singleLogoutReturn',
43
}
44

    
45
class SAML2Auth(AuthForm):
46
    """ SAML 2 authentification
47
    """
48

    
49
    def __init__(self, env, mapper):
50
        """ saml2_config: saml 2 config module
51
        env: WSGI environment
52
        mapper: mapper's module like mandaye.mappers.linuxfr
53
        """
54
        self.env = env
55
        self.END_POINTS_PATH = {
56
                'metadata': str(self.env['mandaye.config'].\
57
                        get('saml2_metadata_url', '/mandaye/metadata')),
58
                'single_sign_on_post': str(self.env['mandaye.config'].\
59
                        get('saml2_single_sign_on_post_url', '/mandaye/singleSignOnPost')),
60
                'single_logout': str(self.env['mandaye.config'].\
61
                        get('saml2_single_logout_url', '/mandaye/singleLogout')),
62
                'single_logout_return': str(self.env['mandaye.config'].\
63
                        get('saml2_single_logout_return_url', '/mandaye/singleLogoutReturn')),
64
        }
65
        for param in ('saml2_idp_metadata',
66
                'saml2_signature_public_key',
67
                'saml2_signature_private_key'):
68
            if not self.env['mandaye.config'].has_key(param):
69
                err = 'you must set %s option in vhost : %s' % \
70
                        (param, self.env['mandaye.vhost'])
71
                logger.error(err)
72
                raise ImproperlyConfigured, err
73
        public_key = self._get_file_content(
74
                self.env['mandaye.config']['saml2_signature_public_key']
75
                )
76
        private_key = self._get_file_content(
77
                self.env['mandaye.config']['saml2_signature_private_key']
78
                )
79
        self.config = {
80
                'saml2_idp_metadata': self.env['mandaye.config']['saml2_idp_metadata'],
81
                'saml2_signature_public_key': public_key,
82
                'saml2_signature_private_key': private_key,
83
                'saml2_sp_logout_url': self.env['mandaye.config'].get('saml2_sp_logout_url'),
84
                'saml2_authnresp_binding': lasso.SAML2_METADATA_BINDING_POST,
85
                'saml2_authnreq_http_method': lasso.HTTP_METHOD_REDIRECT,
86
                'saml2_name_identifier_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
87
                }
88

    
89
        self.metadata_map = (
90
            ('AssertionConsumerService',
91
                lasso.SAML2_METADATA_BINDING_POST ,
92
                self.END_POINTS_PATH['single_sign_on_post']
93
            ),
94
            ('SingleLogoutService',
95
                lasso.SAML2_METADATA_BINDING_REDIRECT,
96
                self.END_POINTS_PATH['single_logout'],
97
                self.END_POINTS_PATH['single_logout_return']),
98
            )
99
        self.metadata_options = { 'key': public_key }
100
        super(SAML2Auth, self).__init__(env, mapper)
101

    
102
    def _get_file_content(self, path):
103
        if not os.path.isabs(path):
104
            path = os.path.join(config.config_root,
105
                    path)
106
        if not os.path.exists(path):
107
            err = "%s: file %s doesn't exist" % \
108
                    (path, self.env['mandaye.vhost'])
109
            logger.error(err)
110
            raise ImproperlyConfigured, err
111
        with open(path, 'r') as f:
112
            content = f.read()
113
        return content
114

    
115
    def get_default_mapping(self):
116
        default_mapping = super(SAML2Auth, self).get_default_mapping()
117
        default_mapping.extend([
118
                {
119
                    'path': r'%s$' % self.END_POINTS_PATH['metadata'],
120
                    'method': 'GET',
121
                    'response': {'filter': self.metadata,}
122
                    },
123
                {
124
                    'path': r'%s$' % self.END_POINTS_PATH['single_sign_on_post'],
125
                    'method': 'POST',
126
                    'response': {'auth': 'single_sign_on_post'}
127
                    },
128
                {
129
                    'path': r'%s$' % self.END_POINTS_PATH['single_logout'],
130
                    'method': 'GET',
131
                    'response': {'auth': 'single_logout',}
132
                    },
133
                {
134
                    'path': r'%s$' % self.END_POINTS_PATH['single_logout_return'],
135
                    'method': 'GET',
136
                    'response': {'auth': 'single_logout_return',}
137
                    },
138
                ])
139
        return default_mapping
140

    
141
    def local_logout(self, env, values, request, response):
142
        logger.info('SP logout initiated by Mandaye')
143
        # Mandaye logout
144
        self.logout(env, values, request, response)
145

    
146
        next_url = None
147
        qs = parse_qs(env['QUERY_STRING'])
148
        if qs.has_key('RelayState'):
149
            next_url = qs['RelayState'][0]
150
        elif qs.has_key('next_url'):
151
            next_url = qs['next_url'][0]
152
        elif values.has_key('next_url'):
153
            next_url = values['next_url']
154

    
155
        req_cookies = request.cookies
156
        if not self.config['saml2_sp_logout_url']:
157
            logger.warning('saml2_sp_logout_url not set into vhost configuration only removing cookies')
158
            for cookie in req_cookies.values():
159
                cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
160
            if next_url:
161
                return _302(next_url, req_cookies)
162
            else:
163
                return _302('/', req_cookies)
164
        return _302(self.config['saml2_sp_logout_url'], req_cookies)
165

    
166
    def _get_idp_metadata_file_path(self):
167
        metadata_file_path = None
168
        if self.config['saml2_idp_metadata']:
169
            metadata_file_path = os.path.join(config.data_dir,
170
                    self.config['saml2_idp_metadata'].\
171
                    replace('://', '_').\
172
                    replace('/', '_')
173
                    )
174
            if not os.path.isfile(metadata_file_path):
175
                try:
176
                    response = urllib2.urlopen(self.config['saml2_idp_metadata'])
177
                    metadata = response.read()
178
                    response.close()
179
                except Exception, e:
180
                    logger.error("Unable to fetch metadata %r: %r",
181
                            self.config['saml2_idp_metadata'], str(e))
182
                    raise MandayeSamlException("Unable to find metadata: %s" % str(e))
183
                metadata_file = open(metadata_file_path, 'w')
184
                metadata_file.write(metadata)
185
                metadata_file.close()
186
        return metadata_file_path
187

    
188
    def _get_metadata(self, env):
189
        url_prefix = env['mandaye.scheme'] + '://' + env['HTTP_HOST']
190
        metadata_path = self.END_POINTS_PATH['metadata']
191
        single_sign_on_post_path = \
192
                self.END_POINTS_PATH['single_sign_on_post']
193
        metagen = saml2utils.Saml2Metadata(url_prefix + metadata_path,
194
                url_prefix = url_prefix)
195
        metagen.add_sp_descriptor(self.metadata_map, self.metadata_options)
196
        return str(metagen)
197

    
198
    def sso(self, env, values, request, response):
199
        req_cookies = request.cookies
200
        if env['beaker.session'].has_key('unique_id'):
201
            env['beaker.session']['unique_id'] = None
202
            for cookie in req_cookies.values():
203
                if cookie.key != 'beaker.session.id':
204
                    cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
205
                    cookie['path'] = '/'
206
        qs = parse_qs(env['QUERY_STRING'])
207
        target_idp = self.config['saml2_idp_metadata']
208
        metadata_file_path = self._get_idp_metadata_file_path()
209
        if not metadata_file_path:
210
            raise MandayeSamlException("sso: unable to load provider")
211
        logger.debug('sso: target_idp is %r', target_idp)
212
        logger.debug('sso: metadata url is %r', self.config['saml2_idp_metadata'])
213
        logger.debug('sso: mandaye metadata are %r', self._get_metadata(env))
214
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
215
            self.config['saml2_signature_private_key'])
216
        if not server:
217
            raise MandayeSamlException("sso: error creating server object.")
218
        logger.debug('sso: mandaye server object created')
219
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
220
        login = lasso.Login(server)
221
        if not login:
222
            raise MandayeSamlException("sso: error creating login object.")
223
        http_method = self.config['saml2_authnreq_http_method']
224
        try:
225
            login.initAuthnRequest(target_idp, http_method)
226
        except lasso.Error, error:
227
            raise MandayeSamlException("sso: Error initiating request %s" % lasso.strError(error[0]))
228
        login.request.nameIDPolicy.format = self.config['saml2_name_identifier_format']
229
        login.request.nameIDPolicy.allowCreate = True
230
        login.request.nameIDPolicy.spNameQualifier = None
231
        login.request.protocolBinding = self.config['saml2_authnresp_binding']
232
        if qs.has_key('next_url'):
233
            login.msgRelayState = qs['next_url'][0]
234
        try:
235
            login.buildAuthnRequestMsg()
236
        except lasso.Error, error:
237
            raise MandayeSamlException("sso: Error initiating request %s" % lasso.strError(error[0]))
238
        logger.debug('sso: set request id in session %s' % login.request.iD)
239
        env['beaker.session']['request_id'] = login.request.iD
240
        env['beaker.session'].save()
241
        if not login.msgUrl:
242
            raise MandayeSamlException("sso: Unable to perform sso by redirection")
243
        return _302(login.msgUrl, req_cookies)
244

    
245
    def single_sign_on_post(self, env, values, request, response):
246
        if self.urls.get('connection_failed_url'):
247
            failed_url = self.urls['connection_failed_url']
248
        else:
249
            failed_url = '/'
250
        metadata_file_path = self._get_idp_metadata_file_path()
251
        if not metadata_file_path:
252
            raise MandayeSamlException("single_sign_on_post: Unable to load provider")
253
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
254
            self.config['saml2_signature_private_key'])
255
        if not server:
256
            raise MandayeSamlException("singleSignOnPost: error creating server object")
257
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
258
        login = lasso.Login(server)
259
        if not login:
260
            raise MandayeSamlException("singleSignOnPost: Error creating login object")
261

    
262
        if env['REQUEST_METHOD'] != 'POST':
263
            raise MandayeSamlException("singleSignOnPost: Not a POST request")
264

    
265
        msg = env['wsgi.input']
266
        params = parse_qs(msg.read())
267
        if not params or not lasso.SAML2_FIELD_RESPONSE in params.keys():
268
            raise MandayeSamlException("singleSignOnPost: Missing response")
269
        message = params[lasso.SAML2_FIELD_RESPONSE][0]
270
        logger.debug('singleSignOnPost message: %r', message)
271
        if not env['beaker.session'].has_key('request_id'):
272
            logger.warning("singleSignOnPost: Unable to find request_id in session")
273
            return _302(failed_url)
274
        saml_request_id = env['beaker.session']['request_id']
275
        try:
276
            login.processAuthnResponseMsg(message)
277
        except lasso.ProfileRequestDeniedError, e:
278
            logger.warning('singleSignOnPost: request denied for nameid %r', saml_request_id)
279
            logger.warning('singleSignOnPost: request denied error: %r', e)
280
            return _302(failed_url)
281
        subject_confirmation = utils.get_absolute_uri(env)
282
        check = saml2utils.authnresponse_checking(login, subject_confirmation,
283
            logger, saml_request_id=saml_request_id)
284
        if not check:
285
            logger.warning("singleSignOnPost: error checking authn response for %r",
286
                    saml_request_id)
287
            return _302(failed_url)
288
        logger.debug('sso: response successfully checked')
289

    
290
        try:
291
            login.acceptSso()
292
        except lasso.Error, error:
293
            raise MandayeSamlException("singleSignOnPost: Error validating\
294
                    sso (%s)" % lasso.strError(error[0]))
295
        logger.debug('sso: sso accepted, session validation')
296

    
297
        env['beaker.session']['validated'] = True
298
        attributes = saml2utils.get_attributes_from_assertion(login.assertion,
299
            logger)
300
        env['beaker.session']['attributes'] = attributes
301
        env['beaker.session']['unique_id'] = login.nameIdentifier.content
302
        env['beaker.session']['liberty_session'] = login.session.dump()
303
        env['beaker.session'].save()
304

    
305
        next_url = None
306
        if params.has_key('RelayState'):
307
            next_url = params['RelayState'][0]
308
        elif values.has_key('next_url'):
309
            next_url = values['next_url']
310

    
311
        if next_url:
312
            return _302("%s?next_url=%s" % (self.urls.get('login_url'), next_url))
313
        else:
314
            return _302(self.urls.get('login_url'))
315

    
316
    def slo(self, env, values, request, response):
317
        """
318
            Single Logout SP initiated by redirected
319
        """
320
        logger.debug('slo: new slo request')
321
        target_idp = self.config['saml2_idp_metadata']
322
        metadata_file_path = self._get_idp_metadata_file_path()
323
        if not metadata_file_path:
324
            raise MandayeSamlException("slo: Unable to load provider.")
325
        logger.debug('slo: target idp %s' % target_idp)
326
        logger.debug('slo: metadata file path %s' % metadata_file_path)
327

    
328
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
329
            self.config['saml2_signature_private_key'])
330
        if not server:
331
            raise MandayeSamlException("slo: Error creating server object")
332
        logger.debug('slo: mandaye server object created')
333
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
334
        logout = lasso.Logout(server)
335

    
336
        # Load liberty session
337
        if not env['beaker.session'].has_key('liberty_session'):
338
            logger.warning('slo: no liberty session in the session')
339
            return self.local_logout(env, values, request, response)
340
        logout.setSessionFromDump(env['beaker.session']['liberty_session'])
341
        if not logout:
342
            logger.warning('slo: error creating logout object')
343
            return self.local_logout(env, values, request, response)
344
        try:
345
            logout.initRequest(None, lasso.HTTP_METHOD_REDIRECT)
346
        except Exception, e:
347
            logger.warning('sp_slo: init request error')
348
            logger.warning('sp_slo error : %s' % str(e))
349
            return self.local_logout(env, values, request, response)
350
        # Manage RelayState
351
        qs = parse_qs(env['QUERY_STRING'])
352
        if qs.has_key('next_url'):
353
            logout.msgRelayState = qs['next_url'][0]
354
        try:
355
            logout.buildRequestMsg()
356
        except Exception, e:
357
            logger.warning('slo: build request error')
358
            logger.warning('slo error : %s' % e)
359
            return self.local_logout(env, values, request, response)
360
        logger.info('slo: sp_slo by redirect')
361
        return _302(logout.msgUrl)
362

    
363
    def slo_return_response(self, logout, cookies=None):
364
        try:
365
            logout.buildResponseMsg()
366
        except lasso.Error, error:
367
            logger.warning('saml2_slo_return_response: %s' % lasso.strError(error[0]))
368
            return _401('saml2_slo_return_response: %s' % lasso.strError(error[0]))
369
        else:
370
            logger.info('saml2_slo_return_response: redirect to %s' % logout.msgUrl)
371
            return _302(logout.msgUrl, cookies)
372

    
373
    def single_logout(self, env, values, request, response):
374
        """
375
            Single Logout IdP initiated by Redirect
376
        """
377
        qs = env['QUERY_STRING']
378
        if not env['QUERY_STRING']:
379
            raise MandayeSamlException("single_logout: Single Logout by \
380
                    Redirect without query string")
381

    
382
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
383
            self.config['saml2_signature_private_key'])
384
        if not server:
385
            logger.warning('single_logout: Service provider not configured')
386
            return _401('single_logout: Service provider not configured')
387
        metadata_file_path = self._get_idp_metadata_file_path()
388
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
389
        logout = lasso.Logout(server)
390
        if not logout:
391
            logger.error('single_logout: Unable to create Logout object')
392
            raise MandayeSamlException("single_logout: Unable to create Logout object")
393
        try:
394
            logout.processRequestMsg(qs)
395
        except lasso.Error, error:
396
            logger.error('saml2_slo: %s' % lasso.strError(error[0]))
397
            return self.slo_return_response(logout)
398

    
399
        logger.info('single_logout: slo from %s' % logout.remoteProviderId)
400
        # Load liberty session
401
        if not env['beaker.session'].has_key('liberty_session'):
402
            logger.error('single_logout: no liberty session in the session')
403
            raise MandayeSamlException("single_logout: no liberty session in the session")
404
        logout.setSessionFromDump(env['beaker.session']['liberty_session'])
405

    
406
        try:
407
            logout.validateRequest()
408
        except lasso.Error, error:
409
            logger.error('single_logout: %s' % lasso.strError(error[0]))
410
            return self.slo_return_response(logout)
411

    
412
        # SP logout
413
        response = self.local_logout(env, values, request, response)
414
        return self.slo_return_response(logout, response.cookies)
415

    
416
    def single_logout_return(self, env, values, request, response):
417
        """
418
            Return point of the slo
419
        """
420
        metadata_file_path = self._get_idp_metadata_file_path()
421
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
422
            self.config['saml2_signature_private_key'])
423
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
424
        if not server:
425
            logger.error('single_logout_return: Error creating server object.')
426
            raise MandayeSamlException("single_logout_return: Error creating server object")
427
        qs = env['QUERY_STRING']
428
        if not env['QUERY_STRING']:
429
            logger.error('single_logout_return: Single Logout \
430
                    by Redirect without query string')
431
            raise MandayeSamlException("single_logout_return: Single Logout \
432
                    by Redirect without query string")
433
        logout = lasso.Logout(server)
434
        if not logout:
435
            logger.error('single_logout_return: Unable to create Logout object')
436
            raise MandayeSamlException("single_logout_return: Unable to create Logout object")
437
        # Load liberty session
438
        if not env['beaker.session'].has_key('liberty_session'):
439
            logger.warning('single_logout_return: no liberty session found.')
440
        try:
441
            logout.processResponseMsg(qs)
442
        except lasso.Error, e:
443
            logger.warning("single_logout_return: %s" % lasso.strError(e[0]))
444
        # Local logout
445
        return self.local_logout(env, values, request, response)
446

    
447

    
448
    def metadata(self, env, values, request, response):
449
        headers = HTTPHeader({'Content-Type': ['text/xml']})
450
        return HTTPResponse(200, 'Found', headers,
451
                self._get_metadata(env))
452

    
(3-3/3)