GrahamDumpleton/mod_wsgi — pull request #41 by mathieubouchard (2014-09-30 03:23:26+00:00)
Title: Implement support for Apache mod_session module

This commit add hooks to mod_wsgi so it can be configured to be a provider to mod_session. This implements all 4 methods listed in Issue #185 of the old bug tracker (https://code.google.com/p/modwsgi/issues/detail?id=185).

If you look at the diff, you will see a lot of modifications on wsgi_hook_check_user_id. In reality, I didn't touch this method. The problem occurred because I reused a lot of the code used to call a python method.

I used the 4 new hooks to secure a web server using one Django app. I wanted the users to use a Django form to log in instead of a browser pop-up. I used mod_auth_form and didn't want to stock the username / password values into an encrypted cookie or database. Since the mod is dependant on a very strict session support, I had to simulate an authentication with a username / password (in reality, I'm using Django session cookie to authenticate the user).

The Apache config I used is :
```
WSGIScriptAlias /app /home/user/myapp/app.wsgi

AuthType Form
AuthFormProvider wsgi
AuthName "RQ"
AuthFormLoginRequiredLocation /app/accounts/login/?next=%{REQUEST_URI}
WSGIAuthUserScript /home/user/myapp/auth.wsgi
Session On
WSGISessionScript /home/user/myapp/auth.wsgi
Require valid-user

Require all granted

```

Here's a description of the four python methods that must be created to support sessions :

1. *load_session* : Receives an AuthObject. Returns a string (the encoded session). A value of None means there is no saved session.
2. *decode_session* : Receives an AuthObject and a string (the encoded session). Returns a dictionary of strings as key / values (no other types are supported). A return value of None means an Error.
3. *encode_session* : Receives an AuthObject and a dictionary of strings as key / values. Returns either a string (the encoded session) or None if the hook must be DECLINED.
4. *save_session* : Receives an AuthObject and a string (the encoded session). Returns either True if the session was saved successfully, False if there was an error or None if the hook must be DECLINED.

Full example (django implementation) :
```
# -*- coding: utf-8 -*-

import sys
import os

APACHE_AUTH_KEY = u"AUTH_NAME"
APACHE_USER_KEY = u"user"
APACHE_PASS_KEY = u"pw"

sys.path.insert(0, os.path.dirname(__file__))
os.environ['DJANGO_SETTINGS_MODULE'] = 'myapp.settings'

def __get_apache_keys_(environ):
user_key = APACHE_USER_KEY
pw_key = APACHE_PASS_KEY
if APACHE_AUTH_KEY in environ:
authname = environ[APACHE_AUTH_KEY]
if authname is not None:
user_key = authname + u"-" + user_key
pw_key = authname + u"-" + pw_key

return (user_key, pw_key)

def __get_session_id_(environ):
from django.conf import settings
from django.http import parse_cookie

if u"HTTP_COOKIE" in environ:
cookies = parse_cookie(environ[u"HTTP_COOKIE"])
if settings.SESSION_COOKIE_NAME in cookies:
return cookies[settings.SESSION_COOKIE_NAME]

def __get_session_(environ):
from django.contrib.sessions.models import Session

s = None

session_id = __get_session_id_(environ)
if session_id is not None:
try:
s = Session.objects.get(pk = session_id)
except Session.DoesNotExists:
pass

return s

def __encode_data_(data):
from importlib import import_module
from django.conf import settings

SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
s = SessionStore()
return s.encode(data)

def __decode_data_(data):
from importlib import import_module
from django.conf import settings

SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
s = SessionStore()
return s.decode(data)

def check_password(environ, username, password):
s = __get_session_(environ)
session_data = s.get_decoded()

from django.contrib.auth import SESSION_KEY
if SESSION_KEY in session_data and session_data[SESSION_KEY] is not None:
from django.contrib.auth import get_user_model
UserModel = get_user_model()

try:
user = UserModel.objects.get(pk = session_data[SESSION_KEY])
return True
except UserModel.DoesNotExist:
pass

return False

def load_session(environ):
s = __get_session_(environ)
if s is not None:
return s.session_data

def decode_session(environ, data):
session_data = __decode_data_(data)

(user_key, pw_key) = __get_apache_keys_(environ)
if user_key not in session_data:
from django.contrib.auth import SESSION_KEY
from django.contrib.auth import get_user_model
UserModel = get_user_model()

user = None
if SESSION_KEY in session_data:
uid = session_data[SESSION_KEY]
if uid is not None:
try:
user = UserModel.objects.get(pk = uid)
except UserModel.DoesNotExist:
pass

if user is not None:
session_data[user_key] = user.get_username()
if pw_key not in session_data:
session_data[pw_key] = u""

return {k.encode(u"utf-8"):v.encode(u"utf-8") for (k,v) in session_data.iteritems() if no$

def encode_session(environ, data):
(user_key, pw_key) = __get_apache_keys_(environ)
if user_key in data:
del data[user_key]
if pw_key in data:
del data[pw_key]

s = __get_session_(environ)
session_data = __decode_data_(s.session_data)
for key in data:
if not key.startswith(u"_"):
session_data[key] = data[key]

return __encode_data_(session_data)

def save_session(environ, data):
s = __get_session_(environ)
if s is not None:
s.session_data = data
s.save()
return True

return False
```

To avoid overriding django values, we can also ignore session saving :
```
def encode_session(environ, data):
pass

def save_session(environ, data):
pass
```