How to use cookies and basic HTTP authentication in CherryPy?

I have a CherryPy web application that requires authentication. I have HTTP Basic Basic Authentication with a configuration that looks like this:

app_config = { '/' : { 'tools.sessions.on': True, 'tools.sessions.name': 'zknsrv', 'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'zknsrv', 'tools.auth_basic.checkpassword': checkpassword, } } 

HTTP auth works fine at this point. For example, this will give me a successful login message, which I defined inside AuthTest :

 curl http://realuser: realpass@localhost /AuthTest/ 

Since sessions are enabled, I can save cookies and examine the one that CherryPy sets:

 curl --cookie-jar cookie.jar http://realuser: realpass@localhost /AuthTest/ 

The cookie.jar will look like this:

 # Netscape HTTP Cookie File # http://curl.haxx.se/rfc/cookie_spec.html # This file was generated by libcurl! Edit at your own risk. localhost FALSE / FALSE 1348640978 zknsrv 821aaad0ba34fd51f77b2452c7ae3c182237deb3 

However, I will receive an HTTP 401 Not Authorized failure if I provide this session identifier without a username and password, for example:

 curl --cookie 'zknsrv=821aaad0ba34fd51f77b2452c7ae3c182237deb3' http://localhost/AuthTest 

What am I missing?

Thanks so much for any help.

+4
source share
1 answer

So, the short answer is: you can do this, but you need to write your own CherryPy tool (a before_handler ), and you should not include basic authentication in the CherryPy configuration (that is, you should not do anything like tools.auth.on or tools.auth.basic... etc.) - you need to handle HTTP authentication yourself. The reason for this is that the built-in Basic Authentication stuff seems to be pretty primitive. If you protect something by enabling Basic Authentication, as I did above, it will verify the authentication before verifying the session and your cookies will do nothing.

My decision, in prose

Fortunately, even though CherryPy doesn't have the ability to do both inline and you can use your inline session code. You still have to write your own code to handle the Basic Authentication element, but overall it's not so bad, and using session code is a big win, because creating a custom session manager is a good way to introduce security errors into your webapp.

In the end, I was able to take a lot of things from a page on the CherryPy wiki called Simple Authentication and Access Restrictions . This code uses CP sessions, but instead of Basic Auth, it uses a special page with a login form that sends ?username=USERNAME&password=PASSWORD . What I did is basically nothing more than changing the provided check_auth function to use a special login page using HTTP headers.

In general, you need a function that you can add as a CherryPy tool, namely before_handler . (In the source code, this function was called check_auth() , but I renamed it to protect() .) This function first tries to see if the cookies contain a (valid) session ID, and if that fails, it tries to see if there are any in the headers HTTP authentication.

Then you need an authentication method for this page; I do this with require() plus some conditions that are only callers that return True . In my case, these conditions are zkn_admin() and user_is() functions; if you have more complex needs, you can also look at member_of() , any_of() and all_of() for the source code.

If you do this, you already have a way to log in - you just send valid session cookies or HTTPBA credentials to any URL that you protect with the @require() decorator. Now you need to log out.

(The source code instead has an AuthController class that contains login() and logout() , and you can use the entire AuthController object in the HTTP document tree by simply placing auth = AuthController() inside your CherryPy root class and navigate to it with the URL an address like http://example.com/auth/login and http://example.com/auth/logout . My code doesn't use an authcontroller object, just a few functions.)

Some notes about my code

  • Warning. Since I wrote my own parser for the headers of the HTTP headers, it only analyzes what I talked about, which means just HTTP Basic Auth - not, for example, Digest Auth or something else. For my application, this is fine; for yours, this may not be.
  • It accepts several functions defined elsewhere in my code: user_verify() and user_is_admin()
  • I also use the debugprint() function, which only outputs the output when the DEBUG variable is set, and I left these calls for clarity.
  • You can call it cherrypy.tools.WHATEVER (see last line); I named it zkauth based on the name of my application. Be careful not to call it auth or the name of any other built-in tool.
  • Then you need to enable cherrypy.tools.WHATEVER in your CherryPy setup.
  • As you can see from all TODO messages: this code is still in a stream state, and not 100% checked for cases of edges - sorry for that! Hope this still gives you enough ideas.

My solution is in code

 import base64 import re import cherrypy SESSION_KEY = '_zkn_username' def protect(*args, **kwargs): debugprint("Inside protect()...") authenticated = False conditions = cherrypy.request.config.get('auth.require', None) debugprint("conditions: {}".format(conditions)) if conditions is not None: # A condition is just a callable that returns true or false try: # TODO: I'm not sure if this is actually checking for a valid session? # or if just any data here would work? this_session = cherrypy.session[SESSION_KEY] # check if there is an active session # sessions are turned on so we just have to know if there is # something inside of cherrypy.session[SESSION_KEY]: cherrypy.session.regenerate() # I can't actually tell if I need to do this myself or what email = cherrypy.request.login = cherrypy.session[SESSION_KEY] authenticated = True debugprint("Authenticated with session: {}, for user: {}".format( this_session, email)) except KeyError: # If the session isn't set, it either wasn't present or wasn't valid. # Now check if the request includes HTTPBA? # FFR The auth header looks like: "AUTHORIZATION: Basic <base64shit>" # TODO: cherrypy has got to handle this for me, right? authheader = cherrypy.request.headers.get('AUTHORIZATION') debugprint("Authheader: {}".format(authheader)) if authheader: #b64data = re.sub("Basic ", "", cherrypy.request.headers.get('AUTHORIZATION')) # TODO: what happens if you get an auth header that doesn't use basic auth? b64data = re.sub("Basic ", "", authheader) decodeddata = base64.b64decode(b64data.encode("ASCII")) # TODO: test how this handles ':' characters in username/passphrase. email,passphrase = decodeddata.decode().split(":", 1) if user_verify(email, passphrase): cherrypy.session.regenerate() # This line of code is discussed in doc/sessions-and-auth.markdown cherrypy.session[SESSION_KEY] = cherrypy.request.login = email authenticated = True else: debugprint ("Attempted to log in with HTTBA username {} but failed.".format( email)) else: debugprint ("Auth header was not present.") except: debugprint ("Client has no valid session and did not provide HTTPBA credentials.") debugprint ("TODO: ensure that if I have a failure inside the 'except KeyError'" + " section above, it doesn't get to this section... I'd want to" + " show a different error message if that happened.") if authenticated: for condition in conditions: if not condition(): debugprint ("Authentication succeeded but authorization failed.") raise cherrypy.HTTPError("403 Forbidden") else: raise cherrypy.HTTPError("401 Unauthorized") cherrypy.tools.zkauth = cherrypy.Tool('before_handler', protect) def require(*conditions): """A decorator that appends conditions to the auth.require config variable.""" def decorate(f): if not hasattr(f, '_cp_config'): f._cp_config = dict() if 'auth.require' not in f._cp_config: f._cp_config['auth.require'] = [] f._cp_config['auth.require'].extend(conditions) return f return decorate #### CONDITIONS # # Conditions are callables that return True # if the user fulfills the conditions they define, False otherwise # # They can access the current user as cherrypy.request.login # TODO: test this function with cookies, I want to make sure that cherrypy.request.login is # set properly so that this function can use it. def zkn_admin(): return lambda: user_is_admin(cherrypy.request.login) def user_is(reqd_email): return lambda: reqd_email == cherrypy.request.login #### END CONDITIONS def logout(): email = cherrypy.session.get(SESSION_KEY, None) cherrypy.session[SESSION_KEY] = cherrypy.request.login = None return "Logout successful" 

Now all you have to do is enable embedded sessions and your own cherrypy.tools.WHATEVER in your CherryPy setup. Again, do not enable cherrypy.tools.auth . My configuration looked like this:

 config_root = { '/' : { 'tools.zkauth.on': True, 'tools.sessions.on': True, 'tools.sessions.name': 'zknsrv', } } 
+6
source

Source: https://habr.com/ru/post/1436290/


All Articles