Projet

Général

Profil

Télécharger (18,6 ko) Statistiques
| Branche: | Tag: | Révision:

root / mandaye / auth / saml2.py @ bdff7b72

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
        return default_mapping
111

    
112
    def local_logout(self, env, values, request, response):
113
        logger.info('SP logout initiated by Mandaye')
114
        # Mandaye logout
115
        self.logout(env, values, request, response)
116

    
117
        next_url = None
118
        qs = parse_qs(env['QUERY_STRING'])
119
        if qs.has_key('RelayState'):
120
            next_url = qs['RelayState'][0]
121
        elif qs.has_key('next_url'):
122
            next_url = qs['next_url'][0]
123
        elif values.has_key('next_url'):
124
            next_url = values['next_url']
125

    
126
        req_cookies = request.cookies
127
        if not self.config['saml2_sp_logout_url']:
128
            logger.warning('saml2_sp_logout_url not set into vhost configuration only removing cookies')
129
            for cookie in req_cookies.values():
130
                cookie['expires'] = 'Thu, 01 Jan 1970 00:00:01 GMT'
131
                cookie['path'] = '/'
132
            if next_url:
133
                return _302(next_url, req_cookies)
134
            else:
135
                return _302('/', req_cookies)
136
        return _302(self.config['saml2_sp_logout_url'], req_cookies)
137

    
138
    def _get_idp_metadata_file_path(self):
139
        metadata_file_path = None
140
        if self.config['saml2_idp_metadata']:
141
            metadata_file_path = os.path.join(config.data_dir,
142
                    self.config['saml2_idp_metadata'].\
143
                    replace('://', '_').\
144
                    replace('/', '_')
145
                    )
146
            if not os.path.isfile(metadata_file_path):
147
                try:
148
                    response = urllib2.urlopen(self.config['saml2_idp_metadata'])
149
                    metadata = response.read()
150
                    response.close()
151
                except Exception, e:
152
                    logger.error("Unable to fetch metadata %r: %r",
153
                            self.config['saml2_idp_metadata'], str(e))
154
                    raise MandayeSamlException("Unable to find metadata: %s" % str(e))
155
                metadata_file = open(metadata_file_path, 'w')
156
                metadata_file.write(metadata)
157
                metadata_file.close()
158
        return metadata_file_path
159

    
160
    def _get_metadata(self, env):
161
        url_prefix = env['mandaye.scheme'] + '://' + env['HTTP_HOST']
162
        metadata_path = END_POINTS_PATH['metadata']
163
        single_sign_on_post_path = \
164
                END_POINTS_PATH['single_sign_on_post']
165
        metagen = saml2utils.Saml2Metadata(url_prefix + metadata_path,
166
                url_prefix = url_prefix)
167
        metagen.add_sp_descriptor(self.metadata_map, self.metadata_options)
168
        return str(metagen)
169

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

    
217
    def single_sign_on_post(self, env, values, request, response):
218
        if self.urls.get('connection_failed_url'):
219
            failed_url = self.urls['connection_failed_url']
220
        else:
221
            failed_url = '/'
222
        metadata_file_path = self._get_idp_metadata_file_path()
223
        if not metadata_file_path:
224
            raise MandayeSamlException("single_sign_on_post: Unable to load provider")
225
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
226
            self.config['saml2_signature_private_key'])
227
        if not server:
228
            raise MandayeSamlException("singleSignOnPost: error creating server object")
229
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
230
        login = lasso.Login(server)
231
        if not login:
232
            raise MandayeSamlException("singleSignOnPost: Error creating login object")
233

    
234
        if env['REQUEST_METHOD'] != 'POST':
235
            raise MandayeSamlException("singleSignOnPost: Not a POST request")
236

    
237
        msg = env['wsgi.input']
238
        params = parse_qs(msg.read())
239
        if not params or not lasso.SAML2_FIELD_RESPONSE in params.keys():
240
            raise MandayeSamlException("singleSignOnPost: Missing response")
241
        message = params[lasso.SAML2_FIELD_RESPONSE][0]
242
        logger.debug('singleSignOnPost message: %r', message)
243
        if not env['beaker.session'].has_key('request_id'):
244
            logger.warning("singleSignOnPost: Unable to find request_id in session")
245
            return _302(failed_url)
246
        saml_request_id = env['beaker.session']['request_id']
247
        try:
248
            login.processAuthnResponseMsg(message)
249
        except lasso.ProfileRequestDeniedError, e:
250
            logger.warning('singleSignOnPost: request denied for nameid %r', saml_request_id)
251
            logger.warning('singleSignOnPost: request denied error: %r', e)
252
            return _302(failed_url)
253
        subject_confirmation = utils.get_absolute_uri(env)
254
        check = saml2utils.authnresponse_checking(login, subject_confirmation,
255
            logger, saml_request_id=saml_request_id)
256
        if not check:
257
            logger.warning("singleSignOnPost: error checking authn response for %r",
258
                    saml_request_id)
259
            return _302(failed_url)
260
        logger.debug('sso: response successfully checked')
261

    
262
        try:
263
            login.acceptSso()
264
        except lasso.Error, error:
265
            raise MandayeSamlException("singleSignOnPost: Error validating\
266
                    sso (%s)" % lasso.strError(error[0]))
267
        logger.debug('sso: sso accepted, session validation')
268

    
269
        env['beaker.session']['validated'] = True
270
        attributes = saml2utils.get_attributes_from_assertion(login.assertion,
271
            logger)
272
        env['beaker.session']['attributes'] = attributes
273
        env['beaker.session']['unique_id'] = login.nameIdentifier.content
274
        env['beaker.session']['liberty_session'] = login.session.dump()
275
        env['beaker.session'].save()
276

    
277
        next_url = None
278
        if params.has_key('RelayState'):
279
            next_url = params['RelayState'][0]
280
        elif values.has_key('next_url'):
281
            next_url = values['next_url']
282

    
283
        if next_url:
284
            return _302("%s?next_url=%s" % (self.urls.get('login_url'), next_url))
285
        else:
286
            return _302(self.urls.get('login_url'))
287

    
288
    def slo(self, env, values, request, response):
289
        """
290
            Single Logout SP initiated by redirected
291
        """
292
        logger.debug('slo: new slo request')
293
        target_idp = self.config['saml2_idp_metadata']
294
        metadata_file_path = self._get_idp_metadata_file_path()
295
        if not metadata_file_path:
296
            raise MandayeSamlException("slo: Unable to load provider.")
297
        logger.debug('slo: target idp %s' % target_idp)
298
        logger.debug('slo: metadata file path %s' % metadata_file_path)
299

    
300
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
301
            self.config['saml2_signature_private_key'])
302
        if not server:
303
            raise MandayeSamlException("slo: Error creating server object")
304
        logger.debug('slo: mandaye server object created')
305
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
306
        logout = lasso.Logout(server)
307

    
308
        # Load liberty session
309
        if not env['beaker.session'].has_key('liberty_session'):
310
            logger.warning('slo: no liberty session in the session')
311
            return self.local_logout(env, values, request, response)
312
        logout.setSessionFromDump(env['beaker.session']['liberty_session'])
313
        if not logout:
314
            logger.warning('slo: error creating logout object')
315
            return self.local_logout(env, values, request, response)
316
        try:
317
            logout.initRequest(None, lasso.HTTP_METHOD_REDIRECT)
318
        except Exception, e:
319
            logger.warning('sp_slo: init request error')
320
            logger.warning('sp_slo error : %s' % str(e))
321
            return self.local_logout(env, values, request, response)
322
        # Manage RelayState
323
        qs = parse_qs(env['QUERY_STRING'])
324
        if qs.has_key('next_url'):
325
            logout.msgRelayState = qs['next_url'][0]
326
        try:
327
            logout.buildRequestMsg()
328
        except Exception, e:
329
            logger.warning('slo: build request error')
330
            logger.warning('slo error : %s' % e)
331
            return self.local_logout(env, values, request, response)
332
        logger.info('slo: sp_slo by redirect')
333
        return _302(logout.msgUrl)
334

    
335
    def slo_return_response(self, logout, cookies=None):
336
        try:
337
            logout.buildResponseMsg()
338
        except lasso.Error, error:
339
            logger.warning('saml2_slo_return_response: %s' % lasso.strError(error[0]))
340
            return _401('saml2_slo_return_response: %s' % lasso.strError(error[0]))
341
        else:
342
            logger.info('saml2_slo_return_response: redirect to %s' % logout.msgUrl)
343
            return _302(logout.msgUrl, cookies)
344

    
345
    def single_logout(self, env, values, request, response):
346
        """
347
            Single Logout IdP initiated by Redirect
348
        """
349
        qs = env['QUERY_STRING']
350
        if not env['QUERY_STRING']:
351
            raise MandayeSamlException("single_logout: Single Logout by \
352
                    Redirect without query string")
353

    
354
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
355
            self.config['saml2_signature_private_key'])
356
        if not server:
357
            logger.warning('single_logout: Service provider not configured')
358
            return _401('single_logout: Service provider not configured')
359
        metadata_file_path = self._get_idp_metadata_file_path()
360
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
361
        logout = lasso.Logout(server)
362
        if not logout:
363
            logger.error('single_logout: Unable to create Logout object')
364
            raise MandayeSamlException("single_logout: Unable to create Logout object")
365
        try:
366
            logout.processRequestMsg(qs)
367
        except lasso.Error, error:
368
            logger.error('saml2_slo: %s' % lasso.strError(error[0]))
369
            return self.slo_return_response(logout)
370

    
371
        logger.info('single_logout: slo from %s' % logout.remoteProviderId)
372
        # Load liberty session
373
        if not env['beaker.session'].has_key('liberty_session'):
374
            logger.error('single_logout: no liberty session in the session')
375
            raise MandayeSamlException("single_logout: no liberty session in the session")
376
        logout.setSessionFromDump(env['beaker.session']['liberty_session'])
377

    
378
        try:
379
            logout.validateRequest()
380
        except lasso.Error, error:
381
            logger.error('single_logout: %s' % lasso.strError(error[0]))
382
            return self.slo_return_response(logout)
383

    
384
        # SP logout
385
        response = self.local_logout(env, values, request, response)
386
        return self.slo_return_response(logout, response.cookies)
387

    
388
    def single_logout_return(self, env, values, request, response):
389
        """
390
            Return point of the slo
391
        """
392
        metadata_file_path = self._get_idp_metadata_file_path()
393
        server = lasso.Server.newFromBuffers(self._get_metadata(env),
394
            self.config['saml2_signature_private_key'])
395
        server.addProvider(lasso.PROVIDER_ROLE_IDP, metadata_file_path)
396
        if not server:
397
            logger.error('single_logout_return: Error creating server object.')
398
            raise MandayeSamlException("single_logout_return: Error creating server object")
399
        qs = env['QUERY_STRING']
400
        if not env['QUERY_STRING']:
401
            logger.error('single_logout_return: Single Logout \
402
                    by Redirect without query string')
403
            raise MandayeSamlException("single_logout_return: Single Logout \
404
                    by Redirect without query string")
405
        logout = lasso.Logout(server)
406
        if not logout:
407
            logger.error('single_logout_return: Unable to create Logout object')
408
            raise MandayeSamlException("single_logout_return: Unable to create Logout object")
409
        # Load liberty session
410
        if not env['beaker.session'].has_key('liberty_session'):
411
            logger.warning('single_logout_return: no liberty session found.')
412
        try:
413
            logout.processResponseMsg(qs)
414
        except lasso.Error, e:
415
            logger.warning("single_logout_return: %s" % lasso.strError(e[0]))
416
        # Local logout
417
        return self.local_logout(env, values, request, response)
418

    
419

    
420
    def metadata(self, env, values, request, response):
421
        headers = HTTPHeader({'Content-Type': ['text/xml']})
422
        return HTTPResponse(200, 'Found', headers,
423
                self._get_metadata(env))
424

    
(3-3/3)