Projet

Général

Profil

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

root / mandaye / auth / saml2.py @ 009e7c0e

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': self.env['mandaye.config'].
57
                        get('saml2_metadata_url', '/mandaye/metadata'),
58
                'single_sign_on_post': self.env['mandaye.config'].\
59
                        get('saml2_single_sign_on_post_url', '/mandaye/singleSignOnPost'),
60
                'single_logout': self.env['mandaye.config'].\
61
                        get('saml2_single_logout_url', '/mandaye/singleLogout'),
62
                'single_logout_return': 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
                cookie['path'] = '/'
161
            if next_url:
162
                return _302(next_url, req_cookies)
163
            else:
164
                return _302('/', req_cookies)
165
        return _302(self.config['saml2_sp_logout_url'], req_cookies)
166

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
448

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

    
(3-3/3)