ZC WSGI sessions
Project description
This is an implementation of persistent sessions as a WSGI middleware using zope.session as an underlying mechanism.
To use it:
Add zc.wsgisessions to install_requires list in setup.py of your application (e.g., myapp)
Add the following to myapp.ini:
[filter:sessions] use = egg:zc.wsgisessions
You can add to configuration:
secure = true
or:
http-only = off
Valid words are: true, false, on, off, yes, no, 1, and 0.
Other options include:
domain = .example.com max-age = 10000 path = /foo
You can also specify a database name for session storage:
db-name = appdb
Add sessions to the pipeline after database middleware, but before the application.
Add to a function that is listed as initializer for the database middleware:
zc.wsgisessions.sessions.initialize_database(database)
You can also pass keyword arguments for: db_name, namespace, secret, timeout, and resolution.
Add to a function that is listed as bobo.configure (initializer of your WSGI application):
zope.component.provideAdapter(zc.wsgisessions.sessions.get_session)
You can use some helpers in your authentication code:
PKG_KEY = __name__ # e.g., myapp.auth def get_user(request): return zc.wsgisessions.sessions.get(request, PKG_KEY, 'user') def save_user(request, user): zc.wsgisession.sessions.store(request, PKG_KEY, 'user', user) def forget_user(request): return zc.wsgisessions.sessions.remove(request, PKG_KEY, 'user')
When running Selenium tests, HttpOnly cookies cannot be used. Set the option 'http-only': False in the global_conf dictionary of your testing application.
Detailed Documentation
Sessions
There are two aspects to the sessions support: browser identification, and session storage. Browsers are identified using cookies; if the cookie isn’t set on an incoming request, the response sets it for future requests.
Session data are stored using a persistent session data container, as defined by the zope.session package. An instance is added to the database at startup if not present. We can control certain parameters by passing keyword arguments to the database initializer. One run of this test uses the default settings, while a second run sets custom parameters.
>>> import re >>> import zc.wsgisessions.testing >>> import zc.wsgisessions.sessions >>> db_name = 'sessions' >>> if zc.wsgisessions.testing.TEST_DB_INIT: ... db_name = 'test' ... db = conn.get_connection(db_name).db() ... zc.wsgisessions.sessions.initialize_database( ... db, ... db_name=db_name, ... namespace='browserid_c0defeed', ... secret='0.10612221415937506119', ... timeout=(15 * 60), # 15 minutes ... resolution=60, # 1 minute ... )>>> dbroot = conn.get_connection(db_name).root() >>> dbroot['sessions'] <zope.session.session.PersistentSessionDataContainer object at 0xc0defeed>>>> if zc.wsgisessions.testing.TEST_DB_INIT: ... expected_id = re.compile('browserid_c0defeed') ... expected_secret = re.compile('0.10612221415937506119') ... expected_timeout = 15 * 60 ... expected_resolution = 60 ... else: ... expected_id = re.compile('browserid_[0-9a-f]{8}') ... expected_secret = re.compile('[0-9a-f]{20}') ... expected_timeout = 24 * 60 * 60 ... expected_resolution = 60 * 60 >>> re.match(expected_id, dbroot['browserid_info'][0]) is not None True >>> re.match(expected_secret, dbroot['browserid_info'][1]) is not None True >>> dbroot['sessions'].timeout == expected_timeout True >>> dbroot['sessions'].resolution == expected_resolution True
If the configuration contains secure set to true or if the request is https, secure is added to the Set-Cookie response. Also HttpOnly is added to the Set-Cookie response, unless the configuration sets http-only to false.
>>> global_conf = {} >>> filter_conf = {'db-name': db_name} >>> filter = zc.wsgisessions.sessions.BrowserIdFilter( ... global_conf, **filter_conf)(object()) >>> environ = { ... 'zodb.connection': conn.get_connection('test'), ... 'wsgi.url_scheme': 'https' ... } >>> h = dict(filter.prepare(environ, lambda *args: args)(200, [], None)[1]) >>> cookie_parts = h['Set-Cookie'].split('; ') >>> 'secure' in cookie_parts True >>> 'HttpOnly' in cookie_parts True
When the settings are changed in the filter configuration (in .ini file), the defaults are replaced.
>>> filter_conf.update({'http-only': 'false', 'secure': 'true', ... 'domain': '.example.com', 'max-age': '5000', ... 'path': '/foo'}) >>> filter = zc.wsgisessions.sessions.BrowserIdFilter( ... global_conf, **filter_conf)(object()) >>> environ['wsgi.url_scheme'] = 'http' >>> h = dict(filter.prepare(environ, lambda *args: args)(200, [], None)[1]) >>> cookie_parts = h['Set-Cookie'].split('; ') >>> 'secure' in cookie_parts True >>> 'HttpOnly' in cookie_parts False >>> 'Domain=.example.com' in cookie_parts True >>> 'Max-Age=5000' in cookie_parts True >>> 'Path=/foo' in cookie_parts True
Notice that the URL scheme above was not https, but the secure was set because it was requested in the filter configuration.
For Selenium testing we need to reset HttpOnly and since we are using http URL scheme in development, the default for secure (off) is acceptable. Notice that we are setting http-only in global configuration this time to override the value from the settings in .ini file.
>>> global_conf = {'http-only': 'off'} >>> filter_conf = {'db-name': db_name, 'http-only': 'on'} >>> filter = zc.wsgisessions.sessions.BrowserIdFilter( ... global_conf, **filter_conf)(object()) >>> h = dict(filter.prepare(environ, lambda *args: args)(200, [], None)[1]) >>> cookie_parts = h['Set-Cookie'].split('; ') >>> 'secure' in cookie_parts False >>> 'HttpOnly' in cookie_parts False
The database name for session storage is set in initialize_database to sessions by default or to a supplied db_name (test for the second run of these tests). If we try to pass a wrong database name to the filter from its configuration (in .ini file) we’ll get an error.
>>> if zc.wsgisessions.testing.TEST_DB_INIT: ... filter_conf['db-name'] = 'sessions' ... filter = zc.wsgisessions.sessions.BrowserIdFilter( ... global_conf, **filter_conf)(object()) ... else: ... filter_conf['db-name'] = 'test' ... filter = zc.wsgisessions.sessions.BrowserIdFilter( ... global_conf, **filter_conf)(object()) >>> h = dict(filter.prepare(environ, lambda *args: args)(200, [], None)[1]) Traceback (most recent call last): ... KeyError: 'browserid_info'
Browser identification
Information needed to support the cookies is also stored in the database:
>>> dbroot['browserid_info'] ('browserid_...', '...')>>> cookie_name = dbroot['browserid_info'][0]>>> import webtest >>> app = webtest.TestApp(app) >>> response = app.get('http://localhost/')>>> cookie_value = app.cookies[cookie_name] >>> len(cookie_value) 54
If we change the secret in the database, we can cause the session identifier to be re-set:
>>> import random >>> import transaction>>> secret = '%.20f' % random.random() >>> dbroot['browserid_info'] = cookie_name, secret >>> transaction.commit()>>> response = app.get('http://localhost/')>>> cookie_value == app.cookies[cookie_name] False>>> cookie_value = app.cookies[cookie_name] >>> app.cookies[cookie_name] = 'bad' >>> response = app.get('http://localhost/')>>> cookie_value == app.cookies[cookie_name] False
Session storage
Once the cookie has been loaded from the request, or arranged to be sent with the response, an ISession object is stored on the request. Let’s create one directly so we can see how that works:
>>> sdc = dbroot['sessions'] >>> session = zc.wsgisessions.sessions.Session(cookie_value, sdc)>>> pkgdata = session['myapp.auth'] >>> pkgdata['mydata'] = 42>>> sdc[cookie_value]['myapp.auth']['mydata'] 42>>> list(session) Traceback (most recent call last): ... NotImplementedError
Helpers
>>> import webob >>> import zc.dbconnection >>> import zope.session.interfaces >>> zc.dbconnection.set_local(conn) >>> environ = {'zc.wsgisessions.session': session} >>> request = webob.Request(environ=environ)
get(request, pkg_id, key=None)
Retrieve a value from the session; if no key is specified, retrieves the SessionPkgData container.
>>> pkgdata = zc.wsgisessions.sessions.get(request, 'myapp.auth') >>> zope.session.interfaces.ISessionPkgData.providedBy(pkgdata) True>>> zc.wsgisessions.sessions.get(request, 'myapp.auth', 'blah') is None True>>> pkgdata['blah'] = '!!!' >>> zc.wsgisessions.sessions.get(request, 'myapp.auth', 'blah') '!!!'>>> zc.wsgisessions.sessions.get(request, 'myapp.auth', 'mydata') 42
When specifying a pkg identifier and a key name, the session data object is not created if it doesn’t already exist.
>>> zc.wsgisessions.sessions.get(request, "dontcreateme", "blah") is None True >>> adapter = zope.session.interfaces.ISession(request) >>> adapter.get("dontcreateme") is None True
store(request, pkg_id, key, value)
Store the key/value pair in the session.
>>> obj = object() >>> zc.wsgisessions.sessions.store( ... request, 'myapp.auth', 'someobject', obj) >>> zc.wsgisessions.sessions.get( ... request, 'myapp.auth', 'someobject') is obj True>>> obj = object() >>> zc.wsgisessions.sessions.store( ... request, 'myapp.data', 'someobject', obj) >>> zc.wsgisessions.sessions.get( ... request, 'myapp.auth', 'someobject') is obj False>>> zc.wsgisessions.sessions.get( ... request, 'myapp.data', 'someobject') is obj True
remove(request, pkg_id, key)
Remove a value from the session by key. If pkg_id is not specified, the default pkg_id of zc.wsgisessions.sessions.KEY is used.
>>> _obj = zc.wsgisessions.sessions.remove( ... request, 'myapp.auth', 'someobject') >>> zc.wsgisessions.sessions.get( ... request, 'myapp.auth', 'someobject') is None True>>> zc.wsgisessions.sessions.get( ... request, 'myapp.data', 'someobject') is obj True>>> zc.wsgisessions.sessions.remove( ... request, 'myapp.data', 'someobject') is obj True >>> zc.wsgisessions.sessions.get( ... request, 'myapp.data', 'someobject') is None True
The underlying session data mapping is not created if it does not already exist.
>>> zc.wsgisessions.sessions.remove( ... request, "dontcreateme", "somekey") is None True >>> adapter.get("dontcreateme") is None True
CHANGES
0.6.1 (2013-10-08)
Include CHANGES.txt in release.
0.6.0 (2013-10-08)
Add domain, max-age, and path configuration options.
0.5.1 (2013-06-12)
Open-source release.
0.5 (2013-03-12)
Use a cryptographically secure random number source (os.urandom) for generating browser ids.
Fix a bug in the get/remove helpers that caused SessionData objects to be created unnecessarily.
0.4 (2012-01-03)
Accept a database name parameter for session storage.
0.3 (2011-11-11)
Put arguments to helper functions in a more logical order.
Require pkg_id to discourage bad use pattern.
0.2 (2011-11-10)
Make http-only and secure configurable.
Test configuration options.
Test database initialization and options.
0.1 (2011-11-10)
Initial release