Projet

Général

Profil

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

root / mandaye / auth / saml2.py @ 8533fdab

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
 * metadata_url: saml end point of the metadata
32
 * single_sign_on_post_url: saml end point of single sign on post
33
 * single_logout_url: saml end point of logout
34
 * 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': self.env['mandaye.config'].get('metadata_url', '/mandaye/metadata'),
57
                'single_sign_on_post': self.env['mandaye.config'].get('single_sign_on_post_url', '/mandaye/singleSignOnPost'),
58
                'single_logout': self.env['mandaye.config'].get('single_logout_url', '/mandaye/singleLogout'),
59
                'single_logout_return': self.env['mandaye.config'].get('single_logout_return_url', '/mandaye/singleLogoutReturn'),
60
        }
61
        for param in ('saml2_idp_metadata',
62
                'saml2_signature_public_key',
63
                'saml2_signature_private_key'):
64
            if not self.env['mandaye.config'].has_key(param):
65
                err = 'you must set %s option in vhost : %s' % \
66
                        (param, self.env['mandaye.vhost'])
67
                logger.error(err)
68
                raise ImproperlyConfigured, err
69
        public_key = self._get_file_content(
70
                self.env['mandaye.config']['saml2_signature_public_key']
71
                )
72
        private_key = self._get_file_content(
73
                self.env['mandaye.config']['saml2_signature_private_key']
74
                )
75
        self.config = {
76
                'saml2_idp_metadata': self.env['mandaye.config']['saml2_idp_metadata'],
77
                'saml2_signature_public_key': public_key,
78
                'saml2_signature_private_key': private_key,
79
                'saml2_sp_logout_url': self.env['mandaye.config'].get('saml2_sp_logout_url'),
80
                'saml2_authnresp_binding': lasso.SAML2_METADATA_BINDING_POST,
81
                'saml2_authnreq_http_method': lasso.HTTP_METHOD_REDIRECT,
82
                'saml2_name_identifier_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
83
                }
84

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

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

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

    
137
    def local_logout(self, env, values, request, response):
138
        logger.info('SP logout initiated by Mandaye')
139
        # Mandaye logout
140
        self.logout(env, values, request, response)
141

    
142
        next_url = None
143
        qs = parse_qs(env['QUERY_STRING'])
144
        if qs.has_key('RelayState'):
145
            next_url = qs['RelayState'][0]
146
        elif qs.has_key('next_url'):
147
            next_url = qs['next_url'][0]
148
        elif values.has_key('next_url'):
149
            next_url = values['next_url']
150

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

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

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

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

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

    
259
        if env['REQUEST_METHOD'] != 'POST':
260
            raise MandayeSamlException("singleSignOnPost: Not a POST request")
261

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

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

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

    
302
        next_url = None
303
        if params.has_key('RelayState'):
304
            next_url = params['RelayState'][0]
305
        elif values.has_key('next_url'):
306
            next_url = values['next_url']
307

    
308
        if next_url:
309
            return _302("%s?next_url=%s" % (self.urls.get('login_url'), next_url))
310
        else:
311
            return _302(self.urls.get('login_url'))
312

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

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

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

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

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

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

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

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

    
409
        # SP logout
410
        response = self.local_logout(env, values, request, response)
411
        return self.slo_return_response(logout, response.cookies)
412

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

    
444

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

    
(3-3/3)