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)
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)
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)
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)
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. 'pgt' and 'targetService' 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)
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