Beispiel #1
0
    def testUpgrade(self):
        p = Portal(IRealm(self.store), [ICredentialsChecker(self.store)])

        def loggedIn((interface, avatarAspect, logout)):
            # if we can login, i guess everything is fine
            self.assertEquals(avatarAspect.garbage, GARBAGE_LEVEL)

        creds = UsernamePassword('@'.join(CREDENTIALS[:-1]), CREDENTIALS[-1])
        d = p.login(creds, None, IGarbage)
        return d.addCallback(loggedIn)
Beispiel #2
0
    def testUpgrade(self):
        p = Portal(IRealm(self.store),
                   [ICredentialsChecker(self.store)])

        def loggedIn((interface, avatarAspect, logout)):
            # if we can login, i guess everything is fine
            self.assertEquals(avatarAspect.garbage, GARBAGE_LEVEL)

        creds = UsernamePassword('@'.join(CREDENTIALS[:-1]), CREDENTIALS[-1])
        d = p.login(creds, None, IGarbage)
        return d.addCallback(loggedIn)
Beispiel #3
0
    def testUpgrade(self):
        p = Portal(IRealm(self.store),
                   [ICredentialsChecker(self.store)])

        def loggedIn((ifc, av, lgo)):
            assert av.garbage == 7
            # Bug in cooperator?  this triggers an exception.
            # return svc.stopService()
        d = p.login(
            UsernamePassword('*****@*****.**', SECRET), None, IGarbage)
        return d.addCallback(loggedIn)
    def testUpgrade(self):
        p = Portal(IRealm(self.store), [ICredentialsChecker(self.store)])

        def loggedIn((ifc, av, lgo)):
            assert av.garbage == 7
            # Bug in cooperator?  this triggers an exception.
            # return svc.stopService()

        d = p.login(UsernamePassword('*****@*****.**', SECRET), None,
                    IGarbage)
        return d.addCallback(loggedIn)
Beispiel #5
0
    def testUserCreation(self):
        dbdir = self.mktemp()
        axiomatic.main(
            ['-d', dbdir, 'userbase', 'create', 'alice', 'localhost', SECRET])

        s = Store(dbdir)
        cc = ICredentialsChecker(s)
        p = Portal(IRealm(s), [cc])

        def cb((interface, avatar, logout)):
            logout()

        return p.login(UsernamePassword('alice@localhost', SECRET), None,
                       lambda orig, default: orig).addCallback(cb)
Beispiel #6
0
 def _login(self, avatarId, password):
     cc = ICredentialsChecker(self.store)
     p = Portal(IRealm(self.store), [cc])
     return p.login(UsernamePassword(avatarId, password), None,
                    lambda orig, default: orig)
Beispiel #7
0
class ServerApp(object):
    app = Klein()
    cookie_name = 'tgc'

    def __init__(self, ticket_store, realm, checkers, validService=None,
                 requireSSL=True, page_views=None, validate_pgturl=True,
                 static=None, reactor=None):
        """
        Initialize an instance of the CAS server.

        @param ticket_store: The ticket store to use.
        @param realm: The t.c.p.Portal asks the realm for an avatar.
        @param checkers: A list of credential checkers to try (in order).
        @param validService: A callable that takes a service as an argument 
            and returns True if the server will authenticate for that service.
        @param requireSSL: Require SSL for Ticket Granting Cookie
        @param page_views: A mapping of functions that are used to render
            custom pages.
            - All views may either be synchronous or async (deferreds).
            - List of views:
                - login: rendered when credentials are requested.
                    - Should accept args (loginTicket, service, failed, request).
                    - Rendered page should POST to /login according to CAS protocol.
                - login_success: Rendered when no service is specified and a
                    valid SSO session already exists.
                    - Should accept args (avatar, request)
                - logout: Rendered on logout.
                    - Should accept args (request,).
                - invalid_service: Rendered when an invalid service is provided.
                    - Should accept args (service, request).
                - error5xx: Rendered on an internal error.
                    - Should accept args (err, request).
                    - `err` is a twisted.python.failure.Failure
                - not_found: Rendered when the requested resource is not found.
                    - Should accept `request`.
        @param validate_pgturl: If True, follow the protocol and validate the pgtUrl
            peer.  Useful to set to False for development purposes.
        @param static_dir: None or path to a static folder to serve content
            from at /static.
        """
        assert ticket_store is not None, "No Ticket Store was configured."
        assert realm is not None, "No Realm was configured."
        assert len(checkers) > 0, "No Credential Checkers were configured."
        if reactor is None:
            from twisted.internet import reactor
        self.reactor = reactor
        for n, checker in enumerate(checkers):
            assert checker is not None, "Credential Checker #%d was not configured." % n
        self.cookies = {}
        self.ticket_store = ticket_store
        self.cred_requestor_portal = Portal(realm)
        self.cred_acceptor_portal = Portal(realm)
        self.realm = realm
        self.checkers = checkers
        map(
            self.cred_requestor_portal.registerChecker, 
            [c for c in checkers 
                if (ICASAuthWhen.providedBy(c) and c.auth_when == 'cred_requestor')])
        map(
            self.cred_acceptor_portal.registerChecker, 
            [c for c in checkers
                if (ICASAuthWhen.providedBy(c) and c.auth_when == 'cred_acceptor')
                    or (not ICASAuthWhen.providedBy(c)) ])
        self.requireSSL = requireSSL
        self.validService = validService or (lambda x: True)
        self.validate_pgturl = validate_pgturl
        self._static = static
        default_page_views = {
                VIEW_LOGIN: self._renderLogin,
                VIEW_LOGIN_SUCCESS: self._renderLoginSuccess,
                VIEW_LOGOUT: self._renderLogout,
                VIEW_INVALID_SERVICE: self._renderInvalidService,
                VIEW_ERROR_5XX: self._renderError5xx,
                VIEW_NOT_FOUND: self._renderNotFound,
            }
        self._default_page_views = default_page_views
        if page_views is None:
            page_views = default_page_views
        else:
            temp = dict(default_page_views)
            temp.update(page_views)
            page_views = temp
            del temp
        self.page_views = page_views
        self.ticket_store.register_ticket_expiration_callback(log_ticket_expiration)
        self.show_login_page = self._isLoginPageRequired()

    def _isLoginPageRequired(self):
        # If no username/password cred checkers are present, showing the login
        # page does not make sense.
        checkers = self.checkers
        found = False
        for checker in checkers:
            supported_creds = checker.credentialInterfaces
            for supported_cred in supported_creds:
                if supported_cred.isOrExtends(IUsernamePassword): 
                    found = True
                    break
        return found

    def _set_response_code_filter(self, result, code, request, msg=None):
        """
        Set the response code during deferred chain processing.
        """
        request.setResponseCode(code, message=msg)
        return result
        
    def _log_failure_filter(self, err, request):
        """
        Log a failure.
        """
        log.msg('[ERROR] type="error" client_ip="%s" uri="%s"' % (request.getClientIP(), request.uri))
        log.err(err)
        return err

    def _get_page_view(self, symbol, *args):

        def eb(err, symbol, *args):
            err.trap(ViewNotImplementedError) 
            log.err(err)            
            return defer.maybeDeferred(self._default_page_views[symbol], *args)

        d = defer.maybeDeferred(self.page_views[symbol], *args)
        d.addErrback(eb, symbol, *args) 
        return d

    def _page_view_callback(self, _, symbol, *args):
        d = self._get_page_view(symbol, *args)
        return d

    def _page_view_result_callback(self, result, symbol, *args):
        d = self._get_page_view(symbol, result, *args)
        return d

    def _page_view_errback(self, err, symbol, *args):
        d = self._get_page_view(symbol, err, *args) 
        return d

    @app.route('/login', methods=['GET'])
    def login_GET(self, request):
        """
        Present a username/password login page to the browser.
        OR
        authenticate using an existing TGC.
        """
        log_http_event(request)
        service = get_single_param_or_default(request, 'service', "")
        renew = get_single_param_or_default(request, 'renew', "")
        portal = self.cred_requestor_portal

        def handle_trust_auth_failed(err, request):
            err.trap(UnhandledCredentials, UnauthorizedLogin)
            if err.check(UnauthorizedLogin):
                log_cas_event("Trust authentication failed", [('auth_fail_reason', err.getErrorMessage()),])
            if self.show_login_page:
                d = self._presentLogin(request)
                return d
            else:
                #TODO: How to respond if trust auth fails and there is no login auth?
                return err

        if renew != "":
            d = self._authenticateByTrust(portal, service, request)
            d.addCallbacks(
                self._authenticated, 
                handle_trust_auth_failed,
                (True, service, request), None,
                (request,), None)
            return d
            
        def service_err(err, service, request):
            err.trap(InvalidService)
            log.err(err)
            request.setResponseCode(403)
            return self._get_page_view(VIEW_INVALID_SERVICE, service, request)

        d = self._authenticateByCookie(request)
        d.addErrback(lambda _: self._authenticateByTrust(portal, service, request).addCallbacks(
            self._authenticated, 
            handle_trust_auth_failed,
            (True, service, request), None,
            (request,), None))
        d.addErrback(service_err, service, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX,  request)
        return d

    def _authenticateByTrust(self, portal, service, request):
        if hasattr(request, 'transport'):
            transport = request.transport
        elif hasattr(request, 'channel'):
            transport = request.channel.transport
        else:
            raise Unauthorized("Could not get transport from request!")
        mind = {'service': service}

        def log_auth(avatar_id, request):
            client_ip = request.getClientIP()
            log_cas_event("Authenticated via Trust", [
                        ('client_ip', client_ip), ('username', avatar_id)])
            return avatar_id

        d = portal.login(transport, mind, ICASUser)
        d.addCallback(lambda x: x[1].username)
        d.addCallback(log_auth, request)
        return d

    def _authenticateByCookie(self, request):
        tgc = request.getCookie(self.cookie_name)
        if not tgc:
            return defer.fail(CookieAuthFailed("No cookie"))
        service = get_single_param_or_default(request, 'service', "")

        def log_tgc_auth(result, request):
            client_ip = request.getClientIP()
            avatar_id = result['avatar_id']
            log_cas_event("Authenticated via TGC", [
                        ('client_ip', client_ip), ('username', avatar_id)])
            return result

        def extract_avatar_id(result):
            return result['avatar_id']

        def eb(err, request):
            err.trap(InvalidTicket, NotSSOService)
            # delete the cookie
            request.addCookie(self.cookie_name, '',
                              expires='Thu, 01 Jan 1970 00:00:00 GMT')
            return err

        d = self.ticket_store.useTicketGrantingCookie(tgc, service)
        d.addCallback(log_tgc_auth, request)
        d.addCallback(extract_avatar_id)
        d.addErrback(eb, request)
        return d.addCallback(self._authenticated, False, service, request)

    def _presentLogin(self, request, failed=False):
        # If the login is presented, the TGC should be removed and the TGT
        # should be expired.

        def expireTGT():
            tgc = request.getCookie(self.cookie_name)
            if tgc:
                #Delete the cookie.
                request.addCookie(
                    self.cookie_name, '', expires='Thu, 01 Jan 1970 00:00:00 GMT')
                #Expire the ticket.
                d = self.ticket_store.expireTGT(tgc)
                return d
            return None

        service = get_single_param_or_default(request, 'service', "")
        gateway = get_single_param_or_default(request, 'gateway', "")
        if gateway != "" and service != "":
            #Redirect to `service` with no ticket.
            request.redirect(service)            
            request.finish()
            return
       
        def service_err(err, service, request):
            err.trap(InvalidService)
            log.err(err)
            request.setResponseCode(403)
            return self._get_page_view(VIEW_INVALID_SERVICE, service, request)
 
        d = defer.maybeDeferred(expireTGT)
        d.addCallback(lambda x: service)
        d.addCallback(self.ticket_store.mkLoginTicket)
        if failed:
            d.addCallback(self._set_response_code_filter, 403, request)
        d.addCallback(self._page_view_result_callback, VIEW_LOGIN, service, failed, request)
        d.addErrback(service_err, service, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d
            
    def _authenticated(self, avatar_id, primaryCredentials, service, request):
        """
        Call this after authentication has succeeded to finish the request.
        """
        tgc = request.getCookie(self.cookie_name)
        
        @defer.inlineCallbacks
        def maybeAddCookie(avatar_id, service, request):
            ticket = request.getCookie(self.cookie_name)
            if not ticket:
                path = request.URLPath().sibling('').path
                ticket = yield self.ticket_store.mkTicketGrantingCookie(avatar_id)
                request.addCookie(self.cookie_name, ticket, path=path,
                                  secure=self.requireSSL)
                request.cookies[-1] += '; HttpOnly'
                attribs = [
                    ('client_ip', request.getClientIP()),
                    ('username', avatar_id),
                    ('TGC', ticket),]
                if service != "":
                    attribs.append(('service', service))
                log_cas_event("Created TGC", attribs)
            defer.returnValue(ticket)

        def mkServiceTicket(tgc, service, request):
            
            def log_service_ticket_created(ticket, service, tgc, request):
                client_ip = request.getClientIP()
                log_cas_event("Created service ticket", [
                            ('client_ip', client_ip), 
                            ('ticket', ticket),
                            ('service', service),
                            ('TGC', tgc),])
                return ticket
            
            def log_failed_to_create_ticket(err, service, tgc, request):
                client_ip = request.getClientIP()
                log_cas_event("Failed to create service ticket", [
                    ('client_ip', client_ip), 
                    ('service', service),
                    ('TGC', tgc)])
                return err
                
            return self.ticket_store.mkServiceTicket(service, tgc, primaryCredentials).addCallback(
                log_service_ticket_created, service, tgc, request).addErrback(
                log_failed_to_create_ticket, service, tgc, request)
            
        def redirect(ticket, service, avatar_id, request):
            p = urlparse.urlparse(service)
            query = urlparse.parse_qs(p.query)
            if 'ticket' in query:
                del qs_map['ticket']
                log.msg('''[WARN] warning="Removed 'ticket' parameter from service URL '%s'."  client_ip="%s" username="******"''' % (
                    service, request.getClientIP(), avatar_id))
            query['ticket'] = ticket
            param_str = urlencode(query, doseq=True)
            p = urlparse.ParseResult(*tuple(p[:4] + (param_str,) + p[5:]))
            service_url = urlparse.urlunparse(p)
            log.msg("[DEBUG] Redirecting to: '{0}'.".format(service_url))
            request.redirect(service_url)

        d = maybeAddCookie(avatar_id, service, request)
        if service != "":
            d.addCallback(mkServiceTicket, service, request)
            d.addCallback(redirect, service, avatar_id, request)
        else:
            d.addCallback(replace_result, avatar_id)
            mind = {'service': ""}
            d.addCallback(self.realm.requestAvatar, mind, ICASUser)
            d.addCallback(extract_avatar)
            d.addCallback(self._page_view_result_callback, VIEW_LOGIN_SUCCESS, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d

    def _renderLogin(self, ticket, service, failed, request):
        html_parts = []
        html_parts.append(dedent('''\
        <html>
            <body>
                <form method="post" action="">
                    Username: <input type="text" name="username" />
                    <br />Password: <input type="password" name="password" />
                    <input type="hidden" name="lt" value="%(lt)s" />
        ''') % {
            'lt': cgi.escape(ticket),
        })
        if service != "":
            html_parts.append(
                '            '
                '<input type="hidden" name="service" value="%(service)s" />' % {
                    'service': service
                })
        html_parts.append(dedent('''\
                    <input type="submit" value="Sign in" />
                </form>
            </body>
        </html>
        '''))
        return '\n'.join(html_parts)

    def _renderLoginSuccess(self, avatar, request):
        html = dedent("""\
            <html>
                <body>
                    <h1>A CAS Session Exists</h1>
                    <p>
                        A CAS session exists for account '%s'.
                    </p>
                </body>
            </html>
            """) % escape_html(avatar.username)
        return html
        
    def _renderLogout(self, request):
        return "You have been logged out."
        
    def _renderInvalidService(self, service, request):
        html = dedent("""\
            <html>
                <head>
                    <title>Invalid Service</title>
                </head>
                <body>
                    <h1>Invalid Service</h1>
                    <p>
                        The service provided is not authorized to use this CAS
                        implementation.
                    </p>
                </body>
            </html>
            """)
        request.setResponseCode(403)
        return html
        
    def _renderError5xx(self, err, request):
        html = dedent("""\
            <html>
                <head>
                    <title>Internal Error - 500</title>
                </head>
                <body>
                    <h1>HTTP 500 - Internal Error</h1>
                    <p>
                        Please contact your system administrator.
                    </p>
                </body>
            </html>
            """)
        request.setResponseCode(500)
        return html
        
    def _renderNotFound(self, request):
        return dedent("""\
            <html>
            <head>
                <title>Not Found</title>
            </head>
            <body>
                <h1>Not Found</h1>
                <p>
                The resource you were looking for was not found.
                </p>
            </body>
            </html>
            """)

    @app.route('/login', methods=['POST'])
    def login_POST(self, request):
        """
        Accept a username/password, verify the credentials and redirect them
        appropriately.
        """
        log_http_event(request, redact_args=['password'])
        service = get_single_param_or_default(request, 'service', "")
        renew = get_single_param_or_default(request, 'renew', "")
        username = get_single_param_or_default(request, 'username', None)
        password = get_single_param_or_default(request, 'password', None)
        ticket = get_single_param_or_default(request, 'lt', None)

        def log_trust_auth_failed(err, request):
            err.trap(UnhandledCredentials, UnauthorizedLogin)
            if err.check(UnauthorizedLogin):
                client_ip = request.getClientIP()
                log_cas_event("Trust authentication failed", [
                    ('auth_fail_reason', err.getErrorMessage()),
                    ('client_ip', client_ip)])
            elif err.check(UnhandledCredentials):
                return None
            return err
            
        def does_trust_avatar_match_username(trust_avatar_id, username, request):
            if trust_avatar_id.lower() != username.lower():
                raise UnauthorizedLogin(
                    "Trust-based avatar ID, '%s', does not match submitted username '%s'." % (
                        trust_avatar_id, username))
            return True

        def checkPassword(_, username, password, service):
            credentials = UsernamePassword(username, password)
            mind = {'service': service}
            return self.cred_acceptor_portal.login(credentials, mind, ICASUser)

        def log_auth_failed(err, username, request):
            err.trap(Unauthorized, InvalidTicket, InvalidService)
            client_ip = request.getClientIP()
            log_cas_event("Failed to authenticate using primary credentials: %s" %
                    err.getErrorMessage(), [
                    ('client_ip', client_ip), ('username', username)])
            return err

        def log_authentication(result, username, request):
            client_ip = request.getClientIP()
            log_cas_event("Authenticated using primary credentials", [
                        ('client_ip', client_ip), ('username', username)])
            return result

        def inject_avatar_id(_, avatar_id):
            return avatar_id

        def eb(err, service, request):
            if not err.check(Unauthorized):
                log.err(err)

            if self.show_login_page:
                d = self._presentLogin(request, failed=True)
                return d
            else:
                return err

        # check credentials
        portal = self.cred_acceptor_portal
        d = self.ticket_store.useLoginTicket(ticket, service)
        d.addCallback(lambda x: self._authenticateByTrust(portal, service, request))
        d.addCallback(does_trust_avatar_match_username, username, request)
        d.addErrback(log_trust_auth_failed, request)
        d.addCallback(checkPassword, username, password, service)
        d.addErrback(log_auth_failed, username, request)
        d.addCallback(log_authentication, username, request)
        d.addCallback(inject_avatar_id, username)
        d.addCallback(self._authenticated, True, service, request)
        d.addErrback(eb, service, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX,  request)
        return d


    @app.route('/logout', methods=['GET'])
    def logout_GET(self, request):
        log_http_event(request)
        service = get_single_param_or_default(request, 'service', "")

        def _validService(_, service):
            def eb(err):
                err.trap(InvalidService)
                return self._get_page_view(VIEW_INVALID_SERVICE, service, request)
            return defer.maybeDeferred(
                self.validService, service).addErrback(eb)
                
        tgc = request.getCookie(self.cookie_name)
        if tgc:
            #Delete the cookie.
            request.addCookie(
                self.cookie_name, '', expires='Thu, 01 Jan 1970 00:00:00 GMT')
            #Expire the ticket.

            def log_ticket_expired(result, tgc, request):
                log_cas_event("Explicitly logged out of SSO", [
                    ('client_ip', request.getClientIP()),
                    ('TGC', tgc)])
                return result

            d = self.ticket_store.expireTGT(tgc)
            d.addCallback(log_ticket_expired, tgc, request)
        else:
            d = defer.maybeDeferred(lambda : None)

        if service != "":
            def redirect(_):
                request.redirect(service)
            d.addCallback(_validService, service)
            d.addCallback(redirect)
        else:
            d.addCallback(self._page_view_callback, VIEW_LOGOUT, request)

        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d

    @app.route('/validate', methods=['GET'])
    def validate_GET(self, request):
        """
        Validate a service ticket, consuming the ticket in the process.
        """
        log_http_event(request)
        ticket = get_single_param_or_default(request, 'ticket', "")
        service = get_single_param_or_default(request, 'service', "")
        renew = get_single_param_or_default(request, 'renew', "")
        if service == "" or ticket == "":
            request.setResponseCode(403)
            return 'no\n\n'
        if renew != "":
            require_pc = True
        else:
            require_pc = False        
        d = self.ticket_store.useServiceTicket(ticket, service, require_pc)

        def renderUsername(data, ticket, service, request):
            avatar_id = data['avatar_id']
            
            def successResult(result, ticket_info, ticket, service, request):
                iface, avatarAspect, logout = result
                attribs = [
                    ('client_ip', request.getClientIP()),
                    ('user', avatarAspect.username),
                    ('ticket', ticket),
                    ('service', service),
                    ('TGT', ticket_info['tgt']),
                    ('primary_credentials', ticket_info['primary_credentials']),]
                if 'pgt' in ticket_info:
                    attribs.append(("PGT", ticket_info['pgt']))
                if 'proxy_chain' in ticket_info:
                    attribs.append(("proxy_chain", ', '.join(ticket_info['proxy_chain'])))
                log_cas_event("Validated service ticket (/validate)", attribs)
                return 'yes\n' + avatarAspect.username + '\n'
            
            mind = {'service': service}
            return self.realm.requestAvatar(avatar_id, mind, ICASUser).addCallback(
                successResult, data, ticket, service, request)
            
        def renderFailure(err, ticket, service, request):
            log_cas_event("Failed to validate service ticket (/validate).", [
                ('client_ip', request.getClientIP()),
                ('ticket', ticket),
                ('service', service),])
            err.trap(InvalidTicket, InvalidService, Unauthorized)
            request.setResponseCode(403)
            return 'no\n\n'

        d.addCallback(renderUsername, ticket, service, request)
        d.addErrback(renderFailure, ticket, service, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d        

    @app.route('/serviceValidate', methods=['GET'])
    def serviceValidate_GET(self, request):
        log_http_event(request)
        return self._serviceOrProxyValidate(request, False)
        
    @app.route('/proxyValidate', methods=['GET'])
    def proxyValidate_GET(self, request):
        log_http_event(request)
        return self._serviceOrProxyValidate(request, True)
    
    def _serviceOrProxyValidate(self, request, proxyValidate=True):
        """
        Validate a service ticket or proxy ticket, consuming the ticket in the process.
        """
        ticket = get_single_param_or_default(request, 'ticket', None)
        service = get_single_param_or_default(request, 'service', None)
        pgturl = get_single_param_or_default(request, 'pgtUrl', "")
        renew = get_single_param_or_default(request, 'renew', "")
        if service is None or ticket is None:
            request.setResponseCode(400)
            return dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                   <cas:authenticationFailure code="INVALID_REQUEST">
                      Missing parameters.
                   </cas:authenticationFailure>
                </cas:serviceResponse>
                """)
        if renew != "":
            require_pc = True
        else:
            require_pc = False
        if proxyValidate: 
            d = self.ticket_store.useServiceOrProxyTicket(ticket, service, require_pc)
        else:
            d = self.ticket_store.useServiceTicket(ticket, service, require_pc)

        def getAvatar(ticket_data, service):
            def avatarResult(result, ticket_data):
                """
                Append the avatarAspect to the ticket data.
                """
                iface, avatarAspect, logout = result
                ticket_data['avatar'] = avatarAspect
                return ticket_data
            mind = {'service': service}
            return self.realm.requestAvatar(ticket_data['avatar_id'], mind, ICASUser).addCallback(
                avatarResult, ticket_data) 

        def renderSuccess(results, ticket, request):
            avatar = results['avatar']
            attribs = [
                ('client_ip', request.getClientIP()),
                ('user', avatar.username),
                ('ticket', ticket),
                ('service', service),
                ('TGT', results['tgt']),
                ('primary_credentials', results['primary_credentials']),]
            if 'pgt' in results:
                attribs.append(("PGT", results['pgt']))
            if 'proxy_chain' in results:
                attribs.append(("proxy_chain", ', '.join(results['proxy_chain'])))
            log_cas_event("Validated ticket.", attribs)
            iou = results.get('iou', None)
            proxy_chain = results.get('proxy_chain', None)
            doc_begin = dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                    <cas:authenticationSuccess>
                        <cas:user>%s</cas:user>
                """) % xml_escape(avatar.username)
            doc_attributes = make_cas_attributes(avatar.attribs)
            doc_proxy = ""
            if iou is not None:
                doc_proxy = "    <cas:proxyGrantingTicket>%s</cas:proxyGrantingTicket>" % (
                    xml_escape(iou))
            doc_proxy_chain = ""
            if proxy_chain is not None:
                parts = ['''        <cas:proxies>''']
                for pgturl in proxy_chain:
                    parts.append("""            <cas:proxy>%s</cas:proxy>""" % xml_escape(pgturl))
                parts.append('''        </cas:proxies>''')
                doc_proxy_chain = '\n'.join(parts)
                del parts
            doc_end = dedent("""\
                    </cas:authenticationSuccess>
                </cas:serviceResponse>
                """)
            doc_parts = [doc_begin]
            for part in (doc_attributes, doc_proxy, doc_proxy_chain):
                if len(part) > 0:
                    doc_parts.append(part)
            doc_parts.append(doc_end)
            return '\n'.join(doc_parts)

        def renderFailure(err, ticket, request):
            log_cas_event("Failed to validate ticket.", [
                ('client_ip', request.getClientIP()),
                ('ticket', ticket)])
            err.trap(InvalidTicket, InvalidProxyCallback, InvalidService)
            request.setResponseCode(403)
            code = "INVALID_TICKET"
            msg = "Validation failed for ticket '%s'." % ticket
            if err.check(InvalidTicketSpec):
                code = "INVALID_TICKET_SPEC"
            elif err.check(InvalidProxyCallback):
                code = "INVALID_PROXY_CALLBACK"
                msg = "Invalid proxy callback."
            elif err.check(InvalidService):
                code = "INVALID_SERVICE"
                msg = "Invalid service."
            doc_fail = dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                   <cas:authenticationFailure code="%(code)s">
                      %(msg)s.    
                   </cas:authenticationFailure>
                </cas:serviceResponse>
                """) % {
                    'code': xml_escape(code),
                    'msg': xml_escape(msg),}
            return doc_fail

        d.addCallback(self._validateProxyUrl, pgturl, service, ticket, request)
        d.addCallback(getAvatar, service)
        d.addCallback(renderSuccess, ticket, request)
        d.addErrback(renderFailure, ticket, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d        

    def _validateProxyUrl(self, data, pgturl, service, ticket, request):
        """
        Validate service callback.
        Generate PGT + IOU.
        POST both to pgturl.
        Return avatar.
        Optionally return IOU, proxy_chain.
        """
        # NOTE: `data` is the validated ST or PT data.
        # `ticket` is the ST or PT *identifier*, but the ticket has already
        # been consumed at this point.  The ID is needed just to in case a 
        # PGT is created and we want to record its origin ST/PT.
        avatar_id = data['avatar_id']
        tgt = data['tgt']
        # If the validated ticket was a PT, extract the proxy_chain that was used
        # to create *its* parent PGT so it can be added to the proxy chain for
        # the requested PGT.
        #
        # E.g. Service A obtains a PGT.  It uses PGT-A to get PT-A and request
        # a service from B.  Service B uses PT-A to get PGT-B.  It requests
        # PT-B from CAS, and uses PT-B to request service from C.  C validates
        # PT-B.  The response to C would include the pgturls for A (first) and
        # B (second).
        if 'proxy_chain' in data:
            proxy_chain = data['proxy_chain']
        else:
            proxy_chain = None
        if pgturl == "":
            return data
        
        def _mkPGT(_):
            return self.ticket_store.mkProxyGrantingTicket(
                service, ticket, tgt, pgturl, proxy_chain=proxy_chain)
        
        def _sendTicketAndIou(pgt_info, pgturl, httpClientFactory):
            pgt = pgt_info['pgt']
            iou = pgt_info['iou']

            def iou_cb(resp_text, pgtiou):
                """
                Return the iou parameter.
                """
                return pgtiou
                
            q = {'pgtId': pgt, 'pgtIou': iou}
            log_cas_event("Sending pgtId and pgtIou to client.", [
                ('pgturl', pgturl),
                ('pgtIou', iou),
                ('pgtId', pgt),])
            httpClient = httpClientFactory(self.reactor)
            d = httpClient.get(pgturl, params=q, timeout=30)
            d.addCallback(http_status_filter, [(200, 200)], InvalidProxyCallback)
            d.addCallback(treq.content)
            d.addCallback(iou_cb, iou)
            return d
            
        def _package_result(iou, data):
            data['iou'] = iou
            return data
        
        if self.validate_pgturl:
            httpClientFactory = createVerifyingHTTPClient
            p = urlparse.urlparse(pgturl)
            if p.scheme.lower() != "https":
                raise NotHTTPSError("The pgtUrl '%s' is not HTTPS.")
        else:
            httpClientFactory = createNonVerifyingHTTPClient
        httpClient = httpClientFactory(self.reactor)
        d = httpClient.get(pgturl)
        d.addCallback(http_status_filter, [(200, 200)], InvalidProxyCallback)
        d.addCallback(treq.content)
        d.addCallback(_mkPGT)
        d.addCallback(_sendTicketAndIou, pgturl, httpClientFactory)
        d.addCallback(_package_result, data)
        return d

    @app.route('/proxy', methods=['GET'])
    def proxy_GET(self, request):
        log_http_event(request)
        try:
            pgt = get_single_param(request, 'pgt')
            targetService = get_single_param(request, 'targetService')
        except BadRequestError as ex:
            log.err(ex)
            request.setResponseCode(400)
            return dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                    <cas:proxyFailure code="INVALID_REQUEST">
                        Error requesting proxy ticket.       
                        &apos;pgt&apos; and &apos;targetService&apos; parameters are both required exactly once.
                    </cas:proxyFailure>
                </cas:serviceResponse>
                """)
        # Validate the PGT and get the PT

        def successResult(ticket, targetService, pgt, request):
            log_cas_event("Issued proxy ticket", [
                ('client_ip', request.getClientIP()),
                ('ticket', ticket),
                ('targetService', targetService),
                ('PGT', pgt),])
            return dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                    <cas:proxySuccess>
                        <cas:proxyTicket>%(ticket)s</cas:proxyTicket>
                    </cas:proxySuccess>
                </cas:serviceResponse>
                """) % {'ticket': xml_escape(ticket)}

        def failureResult(err, targetService, pgt, request):
            log_cas_event("Failed to issue proxy ticket", [
                ('client_ip', request.getClientIP()),
                ('targetService', targetService),
                ('PGT', pgt),])
            if not err.check(InvalidTicket, InvalidService):
                log.err(err)
            code = "INTERNAL_ERROR"
            msg = "An internal error occured."
            if err.check(InvalidTicket):
                code = "BAD_PGT"
                msg = "PGT '%s' is invalid." % pgt
            elif err.check(InvalidService):
                code = "INVALID_SERVICE"
                msg = "Target service is not authorized."
            return dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                    <cas:proxyFailure code="%(code)s">
                        %(msg)s
                    </cas:proxyFailure>
                </cas:serviceResponse>
                """) % {
                    'code': xml_escape(code),
                    'msg': xml_escape(msg),
                }

        d = defer.maybeDeferred(self.ticket_store.mkProxyTicket, targetService, pgt)
        d.addCallback(successResult, targetService, pgt, request)
        d.addErrback(failureResult, targetService, pgt, request)
        return d

    @app.route('/static/', methods=['GET'], branch=True)
    def static_GET(self, request):
        static = self._static
        if static is None:
            log.msg('[ERROR] type="not_found" client_ip="%s" uri="%s"' % (
                        request.getClientIP(), request.uri))
            return self._get_page_view(VIEW_NOT_FOUND, request)
        else:
            return File(static)

    @app.handle_errors(werkzeug.exceptions.NotFound)
    def error_handler(self, request, failure):
        log.msg('[ERROR] type="not_found" client_ip="%s" uri="%s"' % (
                    request.getClientIP(), request.uri))
        return self._get_page_view(VIEW_NOT_FOUND, request)

    @app.handle_errors(BadRequestError)
    def handle_bad_request(self, request, failure):
        log.msg('[ERROR] type="bad_request" client_ip="%s" uri="%s"' % (
                    request.getClientIP(), request.uri))
        return self._get_page_view(VIEW_ERROR_5XX, failure, request)
Beispiel #8
0
class ServerApp(object):

    app = Klein()
    cookie_name = 'tgc'

    def __init__(self,
                 ticket_store,
                 realm,
                 checkers,
                 validService=None,
                 requireSSL=True):
        self.cookies = {}
        self.ticket_store = ticket_store
        self.portal = Portal(realm)
        self.requireSSL = requireSSL
        map(self.portal.registerChecker, checkers)
        self.validService = validService or (lambda x: True)

    @app.route('/login', methods=['GET'])
    def login_GET(self, request):
        """
        Present a username/password login page to the browser.
        """
        d = self._authenticateByCookie(request)
        d.addErrback(lambda _: self._presentLogin(request))

        def eb(r, request):
            request.setResponseCode(400)

        return d.addErrback(eb, request)

    def _authenticateByCookie(self, request):
        tgc = request.getCookie(self.cookie_name)
        if not tgc:
            return defer.fail(CookieAuthFailed("No cookie"))

        service = request.args['service'][0]
        d = self.ticket_store.useTicketGrantingCookie(tgc)

        # XXX untested
        def eb(err, request):
            # delete the cookie
            request.addCookie(self.cookie_name,
                              '',
                              expires='Thu, 01 Jan 1970 00:00:00 GMT')
            return err

        d.addErrback(eb, request)
        return d.addCallback(self._authenticated, service, request)

    def _presentLogin(self, request):
        service = request.args['service'][0]
        d = self.ticket_store.mkLoginTicket(service)

        def render(ticket, service):
            return '''
            <html>
                <body>
                    <form method="post" action="">
                        Username: <input type="text" name="username" />
                        <br />Password: <input type="password" name="password" />
                        <input type="hidden" name="lt" value="%(lt)s" />
                        <input type="hidden" name="service" value="%(service)s" />
                        <input type="submit" value="Sign in" />
                    </form>
                </body>
            </html>
            ''' % {
                'lt': cgi.escape(ticket),
                'service': cgi.escape(service),
            }

        return d.addCallback(render, service)

    def _authenticated(self, username, service, request):
        """
        Call this after authentication has succeeded to finish the request.
        """
        @defer.inlineCallbacks
        def maybeAddCookie(username, request):
            if not request.getCookie(self.cookie_name):
                path = request.URLPath().sibling('').path
                ticket = yield self.ticket_store.mkTicketGrantingCookie(
                    username)
                request.addCookie(self.cookie_name,
                                  ticket,
                                  path=path,
                                  secure=self.requireSSL)
                request.cookies[-1] += '; HttpOnly'
            defer.returnValue(username)

        def mkServiceTicket(username, service):

            return self.ticket_store.mkServiceTicket(username, service)

        def redirect(ticket, service, request):
            query = urlencode({
                'ticket': ticket,
            })
            request.redirect(service + '?' + query)

        d = maybeAddCookie(username, request)
        d.addCallback(mkServiceTicket, service)
        return d.addCallback(redirect, service, request)

    @app.route('/login', methods=['POST'])
    def login_POST(self, request):
        """
        Accept a username/password, verify the credentials and redirect them
        appropriately.
        """
        service = request.args['service'][0]
        username = request.args['username'][0]
        password = request.args['password'][0]
        ticket = request.args['lt'][0]

        def checkPassword(_, username, password):
            credentials = UsernamePassword(username, password)
            return self.portal.login(credentials, None, IUser)

        def extractUsername(user):
            return user.username

        def eb(err, service, request):
            query = urlencode({
                'service': service,
            })
            request.redirect('/login?' + query)
            request.setResponseCode(403)

        # check credentials
        d = self.ticket_store.useLoginTicket(ticket, service)
        d.addCallback(checkPassword, username, password)
        d.addCallback(extractUsername)
        d.addCallback(self._authenticated, service, request)
        d.addErrback(eb, service, request)
        return d

    @app.route('/logout', methods=['GET'])
    def logout_GET(self, request):
        tgc = request.getCookie(self.cookie_name)
        self.ticket_store.expireTicket(tgc)
        request.addCookie(self.cookie_name,
                          '',
                          expires='Thu, 01 Jan 1970 00:00:00 GMT')
        return 'You have been logged out'

    @app.route('/validate', methods=['GET'])
    def validate_GET(self, request):
        """
        Validate a service ticket, consuming the ticket in the process.
        """
        ticket = request.args['ticket'][0]
        service = request.args['service'][0]
        d = self.ticket_store.useServiceTicket(ticket, service)

        def renderUsername(username):
            return 'yes\n' + username + '\n'

        def renderFailure(err, request):
            request.setResponseCode(403)
            return 'no\n\n'

        d.addCallback(renderUsername)
        d.addErrback(renderFailure, request)
        return d