~ddellav/ubuntu/wily/python-pysaml2/debian-merge

« back to all changes in this revision

Viewing changes to example/idp2/idp.py

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2014-09-08 16:11:53 UTC
  • Revision ID: package-import@ubuntu.com-20140908161153-vms9r4gu0oz4v4ai
Tags: upstream-2.0.0
ImportĀ upstreamĀ versionĀ 2.0.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
import argparse
 
3
import base64
 
4
import xmldsig as ds
 
5
import re
 
6
import logging
 
7
import time
 
8
from hashlib import sha1
 
9
 
 
10
from urlparse import parse_qs
 
11
from Cookie import SimpleCookie
 
12
import os
 
13
 
 
14
from saml2 import server
 
15
from saml2 import BINDING_HTTP_ARTIFACT
 
16
from saml2 import BINDING_URI
 
17
from saml2 import BINDING_PAOS
 
18
from saml2 import BINDING_SOAP
 
19
from saml2 import BINDING_HTTP_REDIRECT
 
20
from saml2 import BINDING_HTTP_POST
 
21
from saml2 import time_util
 
22
 
 
23
from saml2.authn_context import AuthnBroker
 
24
from saml2.authn_context import PASSWORD
 
25
from saml2.authn_context import UNSPECIFIED
 
26
from saml2.authn_context import authn_context_class_ref
 
27
from saml2.extension import pefim
 
28
from saml2.httputil import Response
 
29
from saml2.httputil import NotFound
 
30
from saml2.httputil import geturl
 
31
from saml2.httputil import get_post
 
32
from saml2.httputil import Redirect
 
33
from saml2.httputil import Unauthorized
 
34
from saml2.httputil import BadRequest
 
35
from saml2.httputil import ServiceError
 
36
from saml2.ident import Unknown
 
37
from saml2.metadata import create_metadata_string
 
38
from saml2.s_utils import rndstr, exception_trace
 
39
from saml2.s_utils import UnknownPrincipal
 
40
from saml2.s_utils import UnsupportedBinding
 
41
from saml2.s_utils import PolicyError
 
42
from saml2.sigver import verify_redirect_signature, cert_from_instance, encrypt_cert_from_item
 
43
 
 
44
logger = logging.getLogger("saml2.idp")
 
45
 
 
46
 
 
47
class Cache(object):
 
48
    def __init__(self):
 
49
        self.user2uid = {}
 
50
        self.uid2user = {}
 
51
 
 
52
 
 
53
def _expiration(timeout, tformat="%a, %d-%b-%Y %H:%M:%S GMT"):
 
54
    """
 
55
 
 
56
    :param timeout:
 
57
    :param tformat:
 
58
    :return:
 
59
    """
 
60
    if timeout == "now":
 
61
        return time_util.instant(tformat)
 
62
    elif timeout == "dawn":
 
63
        return time.strftime(tformat, time.gmtime(0))
 
64
    else:
 
65
        # validity time should match lifetime of assertions
 
66
        return time_util.in_a_while(minutes=timeout, format=tformat)
 
67
 
 
68
 
 
69
def get_eptid(idp, req_info, session):
 
70
    return idp.eptid.get(idp.config.entityid,
 
71
                         req_info.sender(), session["permanent_id"],
 
72
                         session["authn_auth"])
 
73
 
 
74
# -----------------------------------------------------------------------------
 
75
 
 
76
 
 
77
def dict2list_of_tuples(d):
 
78
    return [(k, v) for k, v in d.items()]
 
79
 
 
80
# -----------------------------------------------------------------------------
 
81
 
 
82
 
 
83
class Service(object):
 
84
    def __init__(self, environ, start_response, user=None):
 
85
        self.environ = environ
 
86
        logger.debug("ENVIRON: %s" % environ)
 
87
        self.start_response = start_response
 
88
        self.user = user
 
89
 
 
90
    def unpack_redirect(self):
 
91
        if "QUERY_STRING" in self.environ:
 
92
            _qs = self.environ["QUERY_STRING"]
 
93
            return dict([(k, v[0]) for k, v in parse_qs(_qs).items()])
 
94
        else:
 
95
            return None
 
96
    
 
97
    def unpack_post(self):
 
98
        _dict = parse_qs(get_post(self.environ))
 
99
        logger.debug("unpack_post:: %s" % _dict)
 
100
        try:
 
101
            return dict([(k, v[0]) for k, v in _dict.items()])
 
102
        except Exception:
 
103
            return None
 
104
    
 
105
    def unpack_soap(self):
 
106
        try:
 
107
            query = get_post(self.environ)
 
108
            return {"SAMLRequest": query, "RelayState": ""}
 
109
        except Exception:
 
110
            return None
 
111
    
 
112
    def unpack_either(self):
 
113
        if self.environ["REQUEST_METHOD"] == "GET":
 
114
            _dict = self.unpack_redirect()
 
115
        elif self.environ["REQUEST_METHOD"] == "POST":
 
116
            _dict = self.unpack_post()
 
117
        else:
 
118
            _dict = None
 
119
        logger.debug("_dict: %s" % _dict)
 
120
        return _dict
 
121
 
 
122
    def operation(self, _dict, binding):
 
123
        logger.debug("_operation: %s" % _dict)
 
124
        if not _dict or not 'SAMLRequest' in _dict:
 
125
            resp = BadRequest('Error parsing request or no request')
 
126
            return resp(self.environ, self.start_response)
 
127
        else:
 
128
            try:
 
129
                _encrypt_cert = encrypt_cert_from_item(_dict["req_info"].message)
 
130
                return self.do(_dict["SAMLRequest"], binding,
 
131
                               _dict["RelayState"], encrypt_cert=_encrypt_cert)
 
132
            except KeyError:
 
133
                # Can live with no relay state
 
134
                return self.do(_dict["SAMLRequest"], binding)
 
135
 
 
136
    def artifact_operation(self, _dict):
 
137
        if not _dict:
 
138
            resp = BadRequest("Missing query")
 
139
            return resp(self.environ, self.start_response)
 
140
        else:
 
141
            # exchange artifact for request
 
142
            request = IDP.artifact2message(_dict["SAMLart"], "spsso")
 
143
            try:
 
144
                return self.do(request, BINDING_HTTP_ARTIFACT,
 
145
                               _dict["RelayState"])
 
146
            except KeyError:
 
147
                return self.do(request, BINDING_HTTP_ARTIFACT)
 
148
 
 
149
    def response(self, binding, http_args):
 
150
        if binding == BINDING_HTTP_ARTIFACT:
 
151
            resp = Redirect()
 
152
        else:
 
153
            resp = Response(http_args["data"], headers=http_args["headers"])
 
154
        return resp(self.environ, self.start_response)
 
155
 
 
156
    def do(self, query, binding, relay_state="", encrypt_cert=None):
 
157
        pass
 
158
 
 
159
    def redirect(self):
 
160
        """ Expects a HTTP-redirect request """
 
161
 
 
162
        _dict = self.unpack_redirect()
 
163
        return self.operation(_dict, BINDING_HTTP_REDIRECT)
 
164
 
 
165
    def post(self):
 
166
        """ Expects a HTTP-POST request """
 
167
 
 
168
        _dict = self.unpack_post()
 
169
        return self.operation(_dict, BINDING_HTTP_POST)
 
170
 
 
171
    def artifact(self):
 
172
        # Can be either by HTTP_Redirect or HTTP_POST
 
173
        _dict = self.unpack_either()
 
174
        return self.artifact_operation(_dict)
 
175
 
 
176
    def soap(self):
 
177
        """
 
178
        Single log out using HTTP_SOAP binding
 
179
        """
 
180
        logger.debug("- SOAP -")
 
181
        _dict = self.unpack_soap()
 
182
        logger.debug("_dict: %s" % _dict)
 
183
        return self.operation(_dict, BINDING_SOAP)
 
184
 
 
185
    def uri(self):
 
186
        _dict = self.unpack_either()
 
187
        return self.operation(_dict, BINDING_SOAP)
 
188
 
 
189
    # def not_authn(self, key):
 
190
    #     """
 
191
    #
 
192
    #
 
193
    #     :return:
 
194
    #     """
 
195
    #     loc = "http://%s/login" % (self.environ["HTTP_HOST"])
 
196
    #     loc += "?%s" % urllib.urlencode({"came_from": self.environ[
 
197
    #         "PATH_INFO"], "key": key})
 
198
    #     headers = [('Content-Type', 'text/plain')]
 
199
    #
 
200
    #     logger.debug("location: %s" % loc)
 
201
    #     logger.debug("headers: %s" % headers)
 
202
    #
 
203
    #     resp = Redirect(loc, headers=headers)
 
204
    #
 
205
    #     return resp(self.environ, self.start_response)
 
206
 
 
207
    def not_authn(self, key, requested_authn_context):
 
208
        ruri = geturl(self.environ, query=False)
 
209
        return do_authentication(self.environ, self.start_response,
 
210
                                 authn_context=requested_authn_context,
 
211
                                 key=key, redirect_uri=ruri)
 
212
 
 
213
 
 
214
# -----------------------------------------------------------------------------
 
215
 
 
216
REPOZE_ID_EQUIVALENT = "uid"
 
217
FORM_SPEC = """<form name="myform" method="post" action="%s">
 
218
   <input type="hidden" name="SAMLResponse" value="%s" />
 
219
   <input type="hidden" name="RelayState" value="%s" />
 
220
</form>"""
 
221
 
 
222
# -----------------------------------------------------------------------------
 
223
# === Single log in ====
 
224
# -----------------------------------------------------------------------------
 
225
 
 
226
 
 
227
class AuthenticationNeeded(Exception):
 
228
    def __init__(self, authn_context=None, *args, **kwargs):
 
229
        Exception.__init__(*args, **kwargs)
 
230
        self.authn_context = authn_context
 
231
 
 
232
 
 
233
class SSO(Service):
 
234
    def __init__(self, environ, start_response, user=None):
 
235
        Service.__init__(self, environ, start_response, user)
 
236
        self.binding = ""
 
237
        self.response_bindings = None
 
238
        self.resp_args = {}
 
239
        self.binding_out = None
 
240
        self.destination = None
 
241
        self.req_info = None
 
242
 
 
243
    def verify_request(self, query, binding):
 
244
        """
 
245
        :param query: The SAML query, transport encoded
 
246
        :param binding: Which binding the query came in over
 
247
        """
 
248
        resp_args = {}
 
249
        if not query:
 
250
            logger.info("Missing QUERY")
 
251
            resp = Unauthorized('Unknown user')
 
252
            return resp_args, resp(self.environ, self.start_response)
 
253
 
 
254
        if not self.req_info:
 
255
            self.req_info = IDP.parse_authn_request(query, binding)
 
256
 
 
257
        logger.info("parsed OK")
 
258
        _authn_req = self.req_info.message
 
259
        logger.debug("%s" % _authn_req)
 
260
 
 
261
        self.binding_out, self.destination = IDP.pick_binding(
 
262
            "assertion_consumer_service",
 
263
            bindings=self.response_bindings,
 
264
            entity_id=_authn_req.issuer.text)
 
265
 
 
266
        logger.debug("Binding: %s, destination: %s" % (self.binding_out,
 
267
                                                       self.destination))
 
268
 
 
269
        resp_args = {}
 
270
        try:
 
271
            resp_args = IDP.response_args(_authn_req)
 
272
            _resp = None
 
273
        except UnknownPrincipal, excp:
 
274
            _resp = IDP.create_error_response(_authn_req.id,
 
275
                                              self.destination, excp)
 
276
        except UnsupportedBinding, excp:
 
277
            _resp = IDP.create_error_response(_authn_req.id,
 
278
                                              self.destination, excp)
 
279
 
 
280
        return resp_args, _resp
 
281
 
 
282
    def do(self, query, binding_in, relay_state="", encrypt_cert=None):
 
283
        try:
 
284
            resp_args, _resp = self.verify_request(query, binding_in)
 
285
        except UnknownPrincipal, excp:
 
286
            logger.error("UnknownPrincipal: %s" % (excp,))
 
287
            resp = ServiceError("UnknownPrincipal: %s" % (excp,))
 
288
            return resp(self.environ, self.start_response)
 
289
        except UnsupportedBinding, excp:
 
290
            logger.error("UnsupportedBinding: %s" % (excp,))
 
291
            resp = ServiceError("UnsupportedBinding: %s" % (excp,))
 
292
            return resp(self.environ, self.start_response)
 
293
 
 
294
        if not _resp:
 
295
            identity = USERS[self.user].copy()
 
296
            #identity["eduPersonTargetedID"] = get_eptid(IDP, query, session)
 
297
            logger.info("Identity: %s" % (identity,))
 
298
 
 
299
            if REPOZE_ID_EQUIVALENT:
 
300
                identity[REPOZE_ID_EQUIVALENT] = self.user
 
301
            try:
 
302
                _resp = IDP.create_authn_response(
 
303
                    identity, userid=self.user,
 
304
                    authn=AUTHN_BROKER[self.environ["idp.authn_ref"]], encrypt_cert=encrypt_cert,
 
305
                    **resp_args)
 
306
            except Exception, excp:
 
307
                logging.error(exception_trace(excp))
 
308
                resp = ServiceError("Exception: %s" % (excp,))
 
309
                return resp(self.environ, self.start_response)
 
310
 
 
311
        logger.info("AuthNResponse: %s" % _resp)
 
312
        http_args = IDP.apply_binding(self.binding_out,
 
313
                                      "%s" % _resp, self.destination,
 
314
                                      relay_state, response=True)
 
315
        logger.debug("HTTPargs: %s" % http_args)
 
316
        return self.response(self.binding_out, http_args)
 
317
 
 
318
    def _store_request(self, _dict):
 
319
        logger.debug("_store_request: %s" % _dict)
 
320
        key = sha1(_dict["SAMLRequest"]).hexdigest()
 
321
        # store the AuthnRequest
 
322
        IDP.ticket[key] = _dict
 
323
        return key
 
324
 
 
325
    def redirect(self):
 
326
        """ This is the HTTP-redirect endpoint """
 
327
 
 
328
        logger.info("--- In SSO Redirect ---")
 
329
        _info = self.unpack_redirect()
 
330
 
 
331
        try:
 
332
            _key = _info["key"]
 
333
            _info = IDP.ticket[_key]
 
334
            self.req_info = _info["req_info"]
 
335
            del IDP.ticket[_key]
 
336
        except KeyError:
 
337
            try:
 
338
                self.req_info = IDP.parse_authn_request(_info["SAMLRequest"],
 
339
                                                        BINDING_HTTP_REDIRECT)
 
340
            except KeyError:
 
341
                resp = BadRequest("Message signature verification failure")
 
342
                return resp(self.environ, self.start_response)
 
343
 
 
344
            _req = self.req_info.message
 
345
 
 
346
            if "SigAlg" in _info and "Signature" in _info:  # Signed request
 
347
                issuer = _req.issuer.text
 
348
                _certs = IDP.metadata.certs(issuer, "any", "signing")
 
349
                verified_ok = False
 
350
                for cert in _certs:
 
351
                    if verify_redirect_signature(_info, cert):
 
352
                        verified_ok = True
 
353
                        break
 
354
                if not verified_ok:
 
355
                    resp = BadRequest("Message signature verification failure")
 
356
                    return resp(self.environ, self.start_response)
 
357
 
 
358
            if self.user:
 
359
                if _req.force_authn:
 
360
                    _info["req_info"] = self.req_info
 
361
                    key = self._store_request(_info)
 
362
                    return self.not_authn(key, _req.requested_authn_context)
 
363
                else:
 
364
                    return self.operation(_info, BINDING_HTTP_REDIRECT)
 
365
            else:
 
366
                _info["req_info"] = self.req_info
 
367
                key = self._store_request(_info)
 
368
                return self.not_authn(key, _req.requested_authn_context)
 
369
        else:
 
370
            return self.operation(_info, BINDING_HTTP_REDIRECT)
 
371
 
 
372
    def post(self):
 
373
        """
 
374
        The HTTP-Post endpoint
 
375
        """
 
376
        logger.info("--- In SSO POST ---")
 
377
        _info = self.unpack_either()
 
378
        self.req_info = IDP.parse_authn_request(
 
379
            _info["SAMLRequest"], BINDING_HTTP_POST)
 
380
        _req = self.req_info.message
 
381
        if self.user:
 
382
            if _req.force_authn:
 
383
                _info["req_info"] = self.req_info
 
384
                key = self._store_request(_info)
 
385
                return self.not_authn(key, _req.requested_authn_context)
 
386
            else:
 
387
                return self.operation(_info, BINDING_HTTP_POST)
 
388
        else:
 
389
            _info["req_info"] = self.req_info
 
390
            key = self._store_request(_info)
 
391
            return self.not_authn(key, _req.requested_authn_context)
 
392
 
 
393
    # def artifact(self):
 
394
    #     # Can be either by HTTP_Redirect or HTTP_POST
 
395
    #     _req = self._store_request(self.unpack_either())
 
396
    #     if isinstance(_req, basestring):
 
397
    #         return self.not_authn(_req)
 
398
    #     return self.artifact_operation(_req)
 
399
 
 
400
    def ecp(self):
 
401
        # The ECP interface
 
402
        logger.info("--- ECP SSO ---")
 
403
        resp = None
 
404
 
 
405
        try:
 
406
            authz_info = self.environ["HTTP_AUTHORIZATION"]
 
407
            if authz_info.startswith("Basic "):
 
408
                _info = base64.b64decode(authz_info[6:])
 
409
                logger.debug("Authz_info: %s" % _info)
 
410
                try:
 
411
                    (user, passwd) = _info.split(":")
 
412
                    if PASSWD[user] != passwd:
 
413
                        resp = Unauthorized()
 
414
                    self.user = user
 
415
                except ValueError:
 
416
                    resp = Unauthorized()
 
417
            else:
 
418
                resp = Unauthorized()
 
419
        except KeyError:
 
420
            resp = Unauthorized()
 
421
 
 
422
        if resp:
 
423
            return resp(self.environ, self.start_response)
 
424
 
 
425
        _dict = self.unpack_soap()
 
426
        self.response_bindings = [BINDING_PAOS]
 
427
        # Basic auth ?!
 
428
        return self.operation(_dict, BINDING_SOAP)
 
429
 
 
430
# -----------------------------------------------------------------------------
 
431
# === Authentication ====
 
432
# -----------------------------------------------------------------------------
 
433
 
 
434
 
 
435
def do_authentication(environ, start_response, authn_context, key,
 
436
                      redirect_uri):
 
437
    """
 
438
    Display the login form
 
439
    """
 
440
    logger.debug("Do authentication")
 
441
    auth_info = AUTHN_BROKER.pick(authn_context)
 
442
 
 
443
    if len(auth_info):
 
444
        method, reference = auth_info[0]
 
445
        logger.debug("Authn chosen: %s (ref=%s)" % (method, reference))
 
446
        return method(environ, start_response, reference, key, redirect_uri)
 
447
    else:
 
448
        resp = Unauthorized("No usable authentication method")
 
449
        return resp(environ, start_response)
 
450
 
 
451
 
 
452
# -----------------------------------------------------------------------------
 
453
 
 
454
PASSWD = {"haho0032": "qwerty",
 
455
          "roland": "dianakra",
 
456
          "babs": "howes",
 
457
          "upper": "crust"}
 
458
 
 
459
 
 
460
def username_password_authn(environ, start_response, reference, key,
 
461
                            redirect_uri):
 
462
    """
 
463
    Display the login form
 
464
    """
 
465
    logger.info("The login page")
 
466
    headers = []
 
467
 
 
468
    resp = Response(mako_template="login.mako", template_lookup=LOOKUP,
 
469
                    headers=headers)
 
470
 
 
471
    argv = {
 
472
        "action": "/verify",
 
473
        "login": "",
 
474
        "password": "",
 
475
        "key": key,
 
476
        "authn_reference": reference,
 
477
        "redirect_uri": redirect_uri
 
478
    }
 
479
    logger.info("do_authentication argv: %s" % argv)
 
480
    return resp(environ, start_response, **argv)
 
481
 
 
482
 
 
483
def verify_username_and_password(dic):
 
484
    global PASSWD
 
485
    # verify username and password
 
486
    if PASSWD[dic["login"][0]] == dic["password"][0]:
 
487
        return True, dic["login"][0]
 
488
    else:
 
489
        return False, ""
 
490
 
 
491
 
 
492
def do_verify(environ, start_response, _):
 
493
    query = parse_qs(get_post(environ))
 
494
 
 
495
    logger.debug("do_verify: %s" % query)
 
496
 
 
497
    try:
 
498
        _ok, user = verify_username_and_password(query)
 
499
    except KeyError:
 
500
        _ok = False
 
501
        user = None
 
502
 
 
503
    if not _ok:
 
504
        resp = Unauthorized("Unknown user or wrong password")
 
505
    else:
 
506
        uid = rndstr(24)
 
507
        IDP.cache.uid2user[uid] = user
 
508
        IDP.cache.user2uid[user] = uid
 
509
        logger.debug("Register %s under '%s'" % (user, uid))
 
510
 
 
511
        kaka = set_cookie("idpauthn", "/", uid, query["authn_reference"][0])
 
512
 
 
513
        lox = "%s?id=%s&key=%s" % (query["redirect_uri"][0], uid,
 
514
                                   query["key"][0])
 
515
        logger.debug("Redirect => %s" % lox)
 
516
        resp = Redirect(lox, headers=[kaka], content="text/html")
 
517
 
 
518
    return resp(environ, start_response)
 
519
 
 
520
 
 
521
def not_found(environ, start_response):
 
522
    """Called if no URL matches."""
 
523
    resp = NotFound()
 
524
    return resp(environ, start_response)
 
525
 
 
526
 
 
527
# -----------------------------------------------------------------------------
 
528
# === Single log out ===
 
529
# -----------------------------------------------------------------------------
 
530
 
 
531
#def _subject_sp_info(req_info):
 
532
#    # look for the subject
 
533
#    subject = req_info.subject_id()
 
534
#    subject = subject.text.strip()
 
535
#    sp_entity_id = req_info.message.issuer.text.strip()
 
536
#    return subject, sp_entity_id
 
537
 
 
538
class SLO(Service):
 
539
    def do(self, request, binding, relay_state="", encrypt_cert=None):
 
540
        logger.info("--- Single Log Out Service ---")
 
541
        try:
 
542
            _, body = request.split("\n")
 
543
            logger.debug("req: '%s'" % body)
 
544
            req_info = IDP.parse_logout_request(body, binding)
 
545
        except Exception, exc:
 
546
            logger.error("Bad request: %s" % exc)
 
547
            resp = BadRequest("%s" % exc)
 
548
            return resp(self.environ, self.start_response)
 
549
    
 
550
        msg = req_info.message
 
551
        if msg.name_id:
 
552
            lid = IDP.ident.find_local_id(msg.name_id)
 
553
            logger.info("local identifier: %s" % lid)
 
554
            if lid in IDP.cache.user2uid:
 
555
                uid = IDP.cache.user2uid[lid]
 
556
                if uid in IDP.cache.uid2user:
 
557
                    del IDP.cache.uid2user[uid]
 
558
                del IDP.cache.user2uid[lid]
 
559
            # remove the authentication
 
560
            try:
 
561
                IDP.session_db.remove_authn_statements(msg.name_id)
 
562
            except KeyError, exc:
 
563
                logger.error("ServiceError: %s" % exc)
 
564
                resp = ServiceError("%s" % exc)
 
565
                return resp(self.environ, self.start_response)
 
566
    
 
567
        resp = IDP.create_logout_response(msg, [binding])
 
568
    
 
569
        try:
 
570
            hinfo = IDP.apply_binding(binding, "%s" % resp, "", relay_state)
 
571
        except Exception, exc:
 
572
            logger.error("ServiceError: %s" % exc)
 
573
            resp = ServiceError("%s" % exc)
 
574
            return resp(self.environ, self.start_response)
 
575
    
 
576
        #_tlh = dict2list_of_tuples(hinfo["headers"])
 
577
        delco = delete_cookie(self.environ, "idpauthn")
 
578
        if delco:
 
579
            hinfo["headers"].append(delco)
 
580
        logger.info("Header: %s" % (hinfo["headers"],))
 
581
        resp = Response(hinfo["data"], headers=hinfo["headers"])
 
582
        return resp(self.environ, self.start_response)
 
583
    
 
584
# ----------------------------------------------------------------------------
 
585
# Manage Name ID service
 
586
# ----------------------------------------------------------------------------
 
587
 
 
588
 
 
589
class NMI(Service):
 
590
    
 
591
    def do(self, query, binding, relay_state="", encrypt_cert=None):
 
592
        logger.info("--- Manage Name ID Service ---")
 
593
        req = IDP.parse_manage_name_id_request(query, binding)
 
594
        request = req.message
 
595
    
 
596
        # Do the necessary stuff
 
597
        name_id = IDP.ident.handle_manage_name_id_request(
 
598
            request.name_id, request.new_id, request.new_encrypted_id,
 
599
            request.terminate)
 
600
    
 
601
        logger.debug("New NameID: %s" % name_id)
 
602
    
 
603
        _resp = IDP.create_manage_name_id_response(request)
 
604
    
 
605
        # It's using SOAP binding
 
606
        hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % _resp, "",
 
607
                                  relay_state, response=True)
 
608
    
 
609
        resp = Response(hinfo["data"], headers=hinfo["headers"])
 
610
        return resp(self.environ, self.start_response)
 
611
    
 
612
# ----------------------------------------------------------------------------
 
613
# === Assertion ID request ===
 
614
# ----------------------------------------------------------------------------
 
615
 
 
616
 
 
617
# Only URI binding
 
618
class AIDR(Service):
 
619
    def do(self, aid, binding, relay_state="", encrypt_cert=None):
 
620
        logger.info("--- Assertion ID Service ---")
 
621
 
 
622
        try:
 
623
            assertion = IDP.create_assertion_id_request_response(aid)
 
624
        except Unknown:
 
625
            resp = NotFound(aid)
 
626
            return resp(self.environ, self.start_response)
 
627
    
 
628
        hinfo = IDP.apply_binding(BINDING_URI, "%s" % assertion, response=True)
 
629
    
 
630
        logger.debug("HINFO: %s" % hinfo)
 
631
        resp = Response(hinfo["data"], headers=hinfo["headers"])
 
632
        return resp(self.environ, self.start_response)
 
633
 
 
634
    def operation(self, _dict, binding, **kwargs):
 
635
        logger.debug("_operation: %s" % _dict)
 
636
        if not _dict or "ID" not in _dict:
 
637
            resp = BadRequest('Error parsing request or no request')
 
638
            return resp(self.environ, self.start_response)
 
639
 
 
640
        return self.do(_dict["ID"], binding, **kwargs)
 
641
 
 
642
 
 
643
# ----------------------------------------------------------------------------
 
644
# === Artifact resolve service ===
 
645
# ----------------------------------------------------------------------------
 
646
 
 
647
class ARS(Service):
 
648
    def do(self, request, binding, relay_state="", encrypt_cert=None):
 
649
        _req = IDP.parse_artifact_resolve(request, binding)
 
650
 
 
651
        msg = IDP.create_artifact_response(_req, _req.artifact.text)
 
652
 
 
653
        hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % msg, "", "",
 
654
                                  response=True)
 
655
 
 
656
        resp = Response(hinfo["data"], headers=hinfo["headers"])
 
657
        return resp(self.environ, self.start_response)
 
658
 
 
659
# ----------------------------------------------------------------------------
 
660
# === Authn query service ===
 
661
# ----------------------------------------------------------------------------
 
662
 
 
663
 
 
664
# Only SOAP binding
 
665
class AQS(Service):
 
666
    def do(self, request, binding, relay_state="", encrypt_cert=None):
 
667
        logger.info("--- Authn Query Service ---")
 
668
        _req = IDP.parse_authn_query(request, binding)
 
669
        _query = _req.message
 
670
 
 
671
        msg = IDP.create_authn_query_response(_query.subject,
 
672
                                              _query.requested_authn_context,
 
673
                                              _query.session_index)
 
674
 
 
675
        logger.debug("response: %s" % msg)
 
676
        hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % msg, "", "",
 
677
                                  response=True)
 
678
 
 
679
        resp = Response(hinfo["data"], headers=hinfo["headers"])
 
680
        return resp(self.environ, self.start_response)
 
681
 
 
682
 
 
683
# ----------------------------------------------------------------------------
 
684
# === Attribute query service ===
 
685
# ----------------------------------------------------------------------------
 
686
 
 
687
 
 
688
# Only SOAP binding
 
689
class ATTR(Service):
 
690
    def do(self, request, binding, relay_state="", encrypt_cert=None):
 
691
        logger.info("--- Attribute Query Service ---")
 
692
 
 
693
        _req = IDP.parse_attribute_query(request, binding)
 
694
        _query = _req.message
 
695
 
 
696
        name_id = _query.subject.name_id
 
697
        uid = name_id.text
 
698
        logger.debug("Local uid: %s" % uid)
 
699
        identity = EXTRA[uid]
 
700
 
 
701
        # Comes in over SOAP so only need to construct the response
 
702
        args = IDP.response_args(_query, [BINDING_SOAP])
 
703
        msg = IDP.create_attribute_response(identity,
 
704
                                            name_id=name_id, **args)
 
705
 
 
706
        logger.debug("response: %s" % msg)
 
707
        hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % msg, "", "",
 
708
                                  response=True)
 
709
 
 
710
        resp = Response(hinfo["data"], headers=hinfo["headers"])
 
711
        return resp(self.environ, self.start_response)
 
712
 
 
713
# ----------------------------------------------------------------------------
 
714
# Name ID Mapping service
 
715
# When an entity that shares an identifier for a principal with an identity
 
716
# provider wishes to obtain a name identifier for the same principal in a
 
717
# particular format or federation namespace, it can send a request to
 
718
# the identity provider using this protocol.
 
719
# ----------------------------------------------------------------------------
 
720
 
 
721
 
 
722
class NIM(Service):
 
723
    def do(self, query, binding, relay_state="", encrypt_cert=None):
 
724
        req = IDP.parse_name_id_mapping_request(query, binding)
 
725
        request = req.message
 
726
        # Do the necessary stuff
 
727
        try:
 
728
            name_id = IDP.ident.handle_name_id_mapping_request(
 
729
                request.name_id, request.name_id_policy)
 
730
        except Unknown:
 
731
            resp = BadRequest("Unknown entity")
 
732
            return resp(self.environ, self.start_response)
 
733
        except PolicyError:
 
734
            resp = BadRequest("Unknown entity")
 
735
            return resp(self.environ, self.start_response)
 
736
    
 
737
        info = IDP.response_args(request)
 
738
        _resp = IDP.create_name_id_mapping_response(name_id, **info)
 
739
    
 
740
        # Only SOAP
 
741
        hinfo = IDP.apply_binding(BINDING_SOAP, "%s" % _resp, "", "",
 
742
                                  response=True)
 
743
    
 
744
        resp = Response(hinfo["data"], headers=hinfo["headers"])
 
745
        return resp(self.environ, self.start_response)
 
746
    
 
747
 
 
748
# ----------------------------------------------------------------------------
 
749
# Cookie handling
 
750
# ----------------------------------------------------------------------------
 
751
def info_from_cookie(kaka):
 
752
    logger.debug("KAKA: %s" % kaka)
 
753
    if kaka:
 
754
        cookie_obj = SimpleCookie(kaka)
 
755
        morsel = cookie_obj.get("idpauthn", None)
 
756
        if morsel:
 
757
            try:
 
758
                key, ref = base64.b64decode(morsel.value).split(":")
 
759
                return IDP.cache.uid2user[key], ref
 
760
            except KeyError:
 
761
                return None, None
 
762
        else:
 
763
            logger.debug("No idpauthn cookie")
 
764
    return None, None
 
765
 
 
766
 
 
767
def delete_cookie(environ, name):
 
768
    kaka = environ.get("HTTP_COOKIE", '')
 
769
    logger.debug("delete KAKA: %s" % kaka)
 
770
    if kaka:
 
771
        cookie_obj = SimpleCookie(kaka)
 
772
        morsel = cookie_obj.get(name, None)
 
773
        cookie = SimpleCookie()
 
774
        cookie[name] = ""
 
775
        cookie[name]['path'] = "/"
 
776
        logger.debug("Expire: %s" % morsel)
 
777
        cookie[name]["expires"] = _expiration("dawn")
 
778
        return tuple(cookie.output().split(": ", 1))
 
779
    return None
 
780
 
 
781
 
 
782
def set_cookie(name, _, *args):
 
783
    cookie = SimpleCookie()
 
784
    cookie[name] = base64.b64encode(":".join(args))
 
785
    cookie[name]['path'] = "/"
 
786
    cookie[name]["expires"] = _expiration(5)  # 5 minutes from now
 
787
    logger.debug("Cookie expires: %s" % cookie[name]["expires"])
 
788
    return tuple(cookie.output().split(": ", 1))
 
789
 
 
790
# ----------------------------------------------------------------------------
 
791
 
 
792
# map urls to functions
 
793
AUTHN_URLS = [
 
794
    # sso
 
795
    (r'sso/post$', (SSO, "post")),
 
796
    (r'sso/post/(.*)$', (SSO, "post")),
 
797
    (r'sso/redirect$', (SSO, "redirect")),
 
798
    (r'sso/redirect/(.*)$', (SSO, "redirect")),
 
799
    (r'sso/art$', (SSO, "artifact")),
 
800
    (r'sso/art/(.*)$', (SSO, "artifact")),
 
801
    # slo
 
802
    (r'slo/redirect$', (SLO, "redirect")),
 
803
    (r'slo/redirect/(.*)$', (SLO, "redirect")),
 
804
    (r'slo/post$', (SLO, "post")),
 
805
    (r'slo/post/(.*)$', (SLO, "post")),
 
806
    (r'slo/soap$', (SLO, "soap")),
 
807
    (r'slo/soap/(.*)$', (SLO, "soap")),
 
808
    #
 
809
    (r'airs$', (AIDR, "uri")),
 
810
    (r'ars$', (ARS, "soap")),
 
811
    # mni
 
812
    (r'mni/post$', (NMI, "post")),
 
813
    (r'mni/post/(.*)$', (NMI, "post")),
 
814
    (r'mni/redirect$', (NMI, "redirect")),
 
815
    (r'mni/redirect/(.*)$', (NMI, "redirect")),
 
816
    (r'mni/art$', (NMI, "artifact")),
 
817
    (r'mni/art/(.*)$', (NMI, "artifact")),
 
818
    (r'mni/soap$', (NMI, "soap")),
 
819
    (r'mni/soap/(.*)$', (NMI, "soap")),
 
820
    # nim
 
821
    (r'nim$', (NIM, "soap")),
 
822
    (r'nim/(.*)$', (NIM, "soap")),
 
823
    #
 
824
    (r'aqs$', (AQS, "soap")),
 
825
    (r'attr$', (ATTR, "soap"))
 
826
]
 
827
 
 
828
NON_AUTHN_URLS = [
 
829
    #(r'login?(.*)$', do_authentication),
 
830
    (r'verify?(.*)$', do_verify),
 
831
    (r'sso/ecp$', (SSO, "ecp")),
 
832
]
 
833
 
 
834
# ----------------------------------------------------------------------------
 
835
 
 
836
 
 
837
def metadata(environ, start_response):
 
838
    try:
 
839
        path = args.path
 
840
        if path is None or len(path) == 0:
 
841
            path = os.path.dirname(os.path.abspath( __file__ ))
 
842
        if path[-1] != "/":
 
843
            path += "/"
 
844
        metadata = create_metadata_string(path+args.config, IDP.config,
 
845
                                          args.valid, args.cert, args.keyfile,
 
846
                                          args.id, args.name, args.sign)
 
847
        start_response('200 OK', [('Content-Type', "text/xml")])
 
848
        return metadata
 
849
    except Exception as ex:
 
850
        logger.error("An error occured while creating metadata:" + ex.message)
 
851
        return not_found(environ, start_response)
 
852
 
 
853
def staticfile(environ, start_response):
 
854
    try:
 
855
        path = args.path
 
856
        if path is None or len(path) == 0:
 
857
            path = os.path.dirname(os.path.abspath(__file__))
 
858
        if path[-1] != "/":
 
859
            path += "/"
 
860
        path += environ.get('PATH_INFO', '').lstrip('/')
 
861
        start_response('200 OK', [('Content-Type', "text/xml")])
 
862
        return open(path, 'r').read()
 
863
    except Exception as ex:
 
864
        logger.error("An error occured while creating metadata:" + ex.message)
 
865
        return not_found(environ, start_response)
 
866
 
 
867
def application(environ, start_response):
 
868
    """
 
869
    The main WSGI application. Dispatch the current request to
 
870
    the functions from above and store the regular expression
 
871
    captures in the WSGI environment as  `myapp.url_args` so that
 
872
    the functions from above can access the url placeholders.
 
873
 
 
874
    If nothing matches call the `not_found` function.
 
875
    
 
876
    :param environ: The HTTP application environment
 
877
    :param start_response: The application to run when the handling of the 
 
878
        request is done
 
879
    :return: The response as a list of lines
 
880
    """
 
881
 
 
882
    path = environ.get('PATH_INFO', '').lstrip('/')
 
883
 
 
884
    if path == "metadata":
 
885
        return metadata(environ, start_response)
 
886
 
 
887
    kaka = environ.get("HTTP_COOKIE", None)
 
888
    logger.info("<application> PATH: %s" % path)
 
889
 
 
890
    if kaka:
 
891
        logger.info("= KAKA =")
 
892
        user, authn_ref = info_from_cookie(kaka)
 
893
        environ["idp.authn_ref"] = authn_ref
 
894
    else:
 
895
        try:
 
896
            query = parse_qs(environ["QUERY_STRING"])
 
897
            logger.debug("QUERY: %s" % query)
 
898
            user = IDP.cache.uid2user[query["id"][0]]
 
899
        except KeyError:
 
900
            user = None
 
901
 
 
902
    url_patterns = AUTHN_URLS
 
903
    if not user:
 
904
        logger.info("-- No USER --")
 
905
        # insert NON_AUTHN_URLS first in case there is no user
 
906
        url_patterns = NON_AUTHN_URLS + url_patterns
 
907
 
 
908
    for regex, callback in url_patterns:
 
909
        match = re.search(regex, path)
 
910
        if match is not None:
 
911
            try:
 
912
                environ['myapp.url_args'] = match.groups()[0]
 
913
            except IndexError:
 
914
                environ['myapp.url_args'] = path
 
915
 
 
916
            logger.debug("Callback: %s" % (callback,))
 
917
            if isinstance(callback, tuple):
 
918
                cls = callback[0](environ, start_response, user)
 
919
                func = getattr(cls, callback[1])
 
920
                return func()
 
921
            return callback(environ, start_response, user)
 
922
 
 
923
    if re.search(r'static/.*', path) is not None:
 
924
        return staticfile(environ, start_response)
 
925
    return not_found(environ, start_response)
 
926
 
 
927
# ----------------------------------------------------------------------------
 
928
 
 
929
# allow uwsgi or gunicorn mount
 
930
# by moving some initialization out of __name__ == '__main__' section.
 
931
# uwsgi -s 0.0.0.0:8088 --protocol http --callable application --module idp
 
932
 
 
933
args = type('Config', (object,), { })
 
934
args.config = 'idp_conf'
 
935
args.mako_root = './'
 
936
args.path = None
 
937
 
 
938
import socket
 
939
from idp_user import USERS
 
940
from idp_user import EXTRA
 
941
from mako.lookup import TemplateLookup
 
942
 
 
943
AUTHN_BROKER = AuthnBroker()
 
944
AUTHN_BROKER.add(authn_context_class_ref(PASSWORD),
 
945
                 username_password_authn, 10,
 
946
                 "http://%s" % socket.gethostname())
 
947
AUTHN_BROKER.add(authn_context_class_ref(UNSPECIFIED),
 
948
                 "", 0, "http://%s" % socket.gethostname())
 
949
 
 
950
IDP = server.Server(args.config, cache=Cache())
 
951
IDP.ticket = {}
 
952
 
 
953
# ----------------------------------------------------------------------------
 
954
 
 
955
if __name__ == '__main__':
 
956
    from wsgiref.simple_server import make_server
 
957
 
 
958
    parser = argparse.ArgumentParser()
 
959
    parser.add_argument('-p', dest='path', help='Path to configuration file.')
 
960
    parser.add_argument('-v', dest='valid',
 
961
                        help="How long, in days, the metadata is valid from the time of creation")
 
962
    parser.add_argument('-c', dest='cert', help='certificate')
 
963
    parser.add_argument('-i', dest='id',
 
964
                        help="The ID of the entities descriptor")
 
965
    parser.add_argument('-k', dest='keyfile',
 
966
                        help="A file with a key to sign the metadata with")
 
967
    parser.add_argument('-n', dest='name')
 
968
    parser.add_argument('-s', dest='sign', action='store_true',
 
969
                        help="sign the metadata")
 
970
    parser.add_argument('-m', dest='mako_root', default="./")
 
971
    parser.add_argument(dest="config")
 
972
    args = parser.parse_args()
 
973
 
 
974
    _rot = args.mako_root
 
975
    LOOKUP = TemplateLookup(directories=[_rot + 'templates', _rot + 'htdocs'],
 
976
                            module_directory=_rot + 'modules',
 
977
                            input_encoding='utf-8', output_encoding='utf-8')
 
978
 
 
979
    PORT = 8088
 
980
 
 
981
    SRV = make_server('', PORT, application)
 
982
    print "IdP listening on port: %s" % PORT
 
983
    SRV.serve_forever()
 
984
else:
 
985
    _rot = args.mako_root
 
986
    LOOKUP = TemplateLookup(directories=[_rot + 'templates', _rot + 'htdocs'],
 
987
                            module_directory=_rot + 'modules',
 
988
                            input_encoding='utf-8', output_encoding='utf-8')