Projet

Général

Profil

Télécharger (19,2 ko) Statistiques
| Branche: | Tag: | Révision:

root / mandaye / auth / saml2.py @ b99b38b4

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, HTTPRequest
16
from mandaye.server import get_response
17

    
18
"""
19
Mandaye saml2 authentification support
20

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

    
27
Optional options :
28
 * saml2_sp_logout_url: the url to logout the service provider
29
 * saml2_authnresp_binding: only post is supported for now
30
 * saml2_authnreq_http_method: only http_redirect at the moment
31
 * saml2_name_identifier_format: only persistent at the moment
32
"""
33

    
34
END_POINTS_PATH = {
35
        'metadata': '/mandaye/metadata',
36
        'single_sign_on_post': '/mandaye/singleSignOnPost',
37
        'single_logout': '/mandaye/singleLogout',
38
        'single_logout_return': '/mandaye/singleLogoutReturn',
39
}
40

    
41
class SAML2Auth(AuthForm):
42
    """ SAML 2 authentification
43
    """
44

    
45
    def __init__(self, env, mapper):
46
        """ saml2_config: saml 2 config module
47
        env: WSGI environment
48
        mapper: mapper's module like mandaye.mappers.linuxfr
49
        """
50
        self.env = env
51
        for param in ('saml2_idp_metadata',
52
                'saml2_signature_public_key',
53
                'saml2_signature_private_key'):
54
            if not self.env['mandaye.config'].has_key(param):
55
                err = 'you must set %s option in vhost : %s' % \
56
                        (param, self.env['mandaye.vhost'])
57
                logger.error(err)
58
                raise ImproperlyConfigured, err
59
        public_key = self._get_file_content(
60
                self.env['mandaye.config']['saml2_signature_public_key']
61
                )
62
        private_key = self._get_file_content(
63
                self.env['mandaye.config']['saml2_signature_private_key']
64
                )
65
        self.config = {
66
                'saml2_idp_metadata': self.env['mandaye.config']['saml2_idp_metadata'],
67
                'saml2_signature_public_key': public_key,
68
                'saml2_signature_private_key': private_key,
69
                'saml2_sp_logout_url': self.env['mandaye.config'].get('saml2_sp_logout_url'),
70
                'saml2_authnresp_binding': lasso.SAML2_METADATA_BINDING_POST,
71
                'saml2_authnreq_http_method': lasso.HTTP_METHOD_REDIRECT,
72
                'saml2_name_identifier_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
73
                }
74

    
75
        self.metadata_map = (
76
            ('AssertionConsumerService',
77
                lasso.SAML2_METADATA_BINDING_POST ,
78
                END_POINTS_PATH['single_sign_on_post']
79
            ),
80
            ('SingleLogoutService',
81
                lasso.SAML2_METADATA_BINDING_REDIRECT,
82
                END_POINTS_PATH['single_logout'],
83
                END_POINTS_PATH['single_logout_return']),
84
            )
85
        self.metadata_options = { 'key': public_key }
86
        super(SAML2Auth, self).__init__(env, mapper)
87

    
88
    def _get_file_content(self, path):
89
        if not os.path.isabs(path):
90
            path = os.path.join(config.config_root,
91
                    path)
92
        if not os.path.exists(path):
93
            err = "%s: file %s doesn't exist" % \
94
                    (path, self.env['mandaye.vhost'])
95
            logger.error(err)
96
            raise ImproperlyConfigured, err
97
        with open(path, 'r') as f:
98
            content = f.read()
99
        return content
100

    
101
    def get_default_mapping(self):
102
        default_mapping = super(SAML2Auth, self).get_default_mapping()
103
        default_mapping.extend([
104
                {
105
                    'path': r'%s$' % END_POINTS_PATH['metadata'],
106
                    'method': 'GET',
107
                    'response': {'filter': self.metadata,}
108
                    },
109
                {
110
                    'path': r'%s$' % END_POINTS_PATH['single_sign_on_post'],
111
                    'method': 'POST',
112
                    'response': {'auth': 'single_sign_on_post'}
113
                    },
114
                {
115
                    'path': r'%s$' % END_POINTS_PATH['single_logout'],
116
                    'method': 'GET',
117
                    'response': {'auth': 'single_logout',}
118
                    },
119
                {
120
                    'path': r'%s$' % END_POINTS_PATH['single_logout_return'],
121
                    'method': 'GET',
122
                    'response': {'auth': 'single_logout_return',}
123
                    },
124
                ])
125
        return default_mapping
126

    
127
    def local_logout(self, env, values, request, response):
128
        logger.info('SP logout initiated by Mandaye')
129
        # Mandaye logout
130
        self.logout(env, values, request, response)
131

    
132
        next_url = None
133
        qs = parse_qs(env['QUERY_STRING'])
134
        if qs.has_key('RelayState'):
135
            next_url = qs['RelayState'][0]
136
        elif qs.has_key('next_url'):
137
            next_url = qs['next_url'][0]
138
        elif values.has_key('next_url'):
139
            next_url = values['next_url']
140

    
141
        req_cookies = request.cookies
142
        if not self.config['saml2_sp_logout_url']:
143
            logger.warning('saml2_sp_logout_url not set into vhost configuration only removing cookies')
144
            for cookie in req_cookies.values():
145
                cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
146
                cookie['path'] = '/'
147
            if next_url:
148
                return _302(next_url, req_cookies)
149
            else:
150
                return _302('/', req_cookies)
151
        return _302(self.config['saml2_sp_logout_url'], req_cookies)
152

    
153
    def _get_idp_metadata_file_path(self):
154
        metadata_file_path = None
155
        if self.config['saml2_idp_metadata']:
156
            metadata_file_path = os.path.join(config.data_dir,
157
                    self.config['saml2_idp_metadata'].\
158
                    replace('://', '_').\
159
                    replace('/', '_')
160
                    )
161
            if not os.path.isfile(metadata_file_path):
162
                try:
163
                    response = urllib2.urlopen(self.config['saml2_idp_metadata'])
164
                    metadata = response.read()
165
                    response.close()
166
                except Exception, e:
167
                    logger.error("Unable to fetch metadata %r: %r",
168
                            self.config['saml2_idp_metadata'], str(e))
169
                    raise MandayeSamlException("Unable to find metadata: %s" % str(e))
170
                metadata_file = open(metadata_file_path, 'w')
171
                metadata_file.write(metadata)
172
                metadata_file.close()
173
        return metadata_file_path
174

    
175
    def _get_metadata(self, env):
176
        url_prefix = env['mandaye.scheme'] + '://' + env['HTTP_HOST']
177
        metadata_path = END_POINTS_PATH['metadata']
178
        single_sign_on_post_path = \
179
                END_POINTS_PATH['single_sign_on_post']
180
        metagen = saml2utils.Saml2Metadata(url_prefix + metadata_path,
181
                url_prefix = url_prefix)
182
        metagen.add_sp_descriptor(self.metadata_map, self.metadata_options)
183
        return str(metagen)
184

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

    
232
    def single_sign_on_post(self, env, values, request, response):
233
        if self.urls.get('connection_failed_url'):
234
            failed_url = self.urls['connection_failed_url']
235
        else:
236
            failed_url = '/'
237
        metadata_file_path = self._get_idp_metadata_file_path()
238
        if not metadata_file_path:
239
            raise MandayeSamlException("single_sign_on_post: Unable to load provider")
240
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
241
            self.config['saml2_signature_private_key'])
242
        if not server:
243
            raise MandayeSamlException("singleSignOnPost: error creating server object")
244
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
245
        login = lasso.Login(server)
246
        if not login:
247
            raise MandayeSamlException("singleSignOnPost: Error creating login object")
248

    
249
        if env['REQUEST_METHOD'] != 'POST':
250
            raise MandayeSamlException("singleSignOnPost: Not a POST request")
251

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

    
277
        try:
278
            login.acceptSso()
279
        except lasso.Error, error:
280
            raise MandayeSamlException("singleSignOnPost: Error validating\
281
                    sso (%s)" % lasso.strError(error[0]))
282
        logger.debug('sso: sso accepted, session validation')
283

    
284
        env['beaker.session']['validated'] = True
285
        attributes = saml2utils.get_attributes_from_assertion(login.assertion,
286
            logger)
287
        env['beaker.session']['attributes'] = attributes
288
        env['beaker.session']['unique_id'] = login.nameIdentifier.content
289
        env['beaker.session']['liberty_session'] = login.session.dump()
290
        env['beaker.session'].save()
291

    
292
        next_url = None
293
        if params.has_key('RelayState'):
294
            next_url = params['RelayState'][0]
295
        elif values.has_key('next_url'):
296
            next_url = values['next_url']
297

    
298
        if next_url:
299
            return _302("%s?next_url=%s" % (self.urls.get('login_url'), next_url))
300
        else:
301
            return _302(self.urls.get('login_url'))
302

    
303
    def slo(self, env, values, request, response):
304
        """
305
            Single Logout SP initiated by redirected
306
        """
307
        logger.debug('slo: new slo request')
308
        target_idp = self.config['saml2_idp_metadata']
309
        metadata_file_path = self._get_idp_metadata_file_path()
310
        if not metadata_file_path:
311
            raise MandayeSamlException("slo: Unable to load provider.")
312
        logger.debug('slo: target idp %s' % target_idp)
313
        logger.debug('slo: metadata file path %s' % metadata_file_path)
314

    
315
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
316
            self.config['saml2_signature_private_key'])
317
        if not server:
318
            raise MandayeSamlException("slo: Error creating server object")
319
        logger.debug('slo: mandaye server object created')
320
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
321
        logout = lasso.Logout(server)
322

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

    
350
    def slo_return_response(self, logout, cookies=None):
351
        try:
352
            logout.buildResponseMsg()
353
        except lasso.Error, error:
354
            logger.warning('saml2_slo_return_response: %s' % lasso.strError(error[0]))
355
            return _401('saml2_slo_return_response: %s' % lasso.strError(error[0]))
356
        else:
357
            logger.info('saml2_slo_return_response: redirect to %s' % logout.msgUrl)
358
            return _302(logout.msgUrl, cookies)
359

    
360
    def single_logout(self, env, values, request, response):
361
        """
362
            Single Logout IdP initiated by Redirect
363
        """
364
        qs = env['QUERY_STRING']
365
        if not env['QUERY_STRING']:
366
            raise MandayeSamlException("single_logout: Single Logout by \
367
                    Redirect without query string")
368

    
369
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
370
            self.config['saml2_signature_private_key'])
371
        if not server:
372
            logger.warning('single_logout: Service provider not configured')
373
            return _401('single_logout: Service provider not configured')
374
        metadata_file_path = self._get_idp_metadata_file_path()
375
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
376
        logout = lasso.Logout(server)
377
        if not logout:
378
            logger.error('single_logout: Unable to create Logout object')
379
            raise MandayeSamlException("single_logout: Unable to create Logout object")
380
        try:
381
            logout.processRequestMsg(qs)
382
        except lasso.Error, error:
383
            logger.error('saml2_slo: %s' % lasso.strError(error[0]))
384
            return self.slo_return_response(logout)
385

    
386
        logger.info('single_logout: slo from %s' % logout.remoteProviderId)
387
        # Load liberty session
388
        if not env['beaker.session'].has_key('liberty_session'):
389
            logger.error('single_logout: no liberty session in the session')
390
            raise MandayeSamlException("single_logout: no liberty session in the session")
391
        logout.setSessionFromDump(env['beaker.session']['liberty_session'])
392

    
393
        try:
394
            logout.validateRequest()
395
        except lasso.Error, error:
396
            logger.error('single_logout: %s' % lasso.strError(error[0]))
397
            return self.slo_return_response(logout)
398

    
399
        # SP logout
400
        response = self.local_logout(env, values, request, response)
401
        return self.slo_return_response(logout, response.cookies)
402

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

    
434

    
435
    def metadata(self, env, values, request, response):
436
        headers = HTTPHeader({'Content-Type': ['text/xml']})
437
        return HTTPResponse(200, 'Found', headers,
438
                self._get_metadata(env))
439

    
(3-3/3)