first commit

This commit is contained in:
Benjamin Dauvergne 2014-03-10 12:46:29 +01:00
commit 73c33d8c60
8 changed files with 211 additions and 0 deletions

31
README.txt Normal file
View File

@ -0,0 +1,31 @@
IDP for IBM Domino. It can generate LTPA tokens.
Install
=======
You just have to install the package in your virtualenv and relaunch, it will
be automatically loaded by the plugin framework.
Settings
========
A2_LTPA_TOKEN_SECRET
Secret to sign tokens, can be plain string, base64 encode with the 'b64:'
prefix or hex encoded with the 'hex:' prefix. It's mandatory.
A2_LTPA_TOKEN_DURATION
Lifetime of a token as seconds, default is 3600 (1 hour).
A2_LTPA_COOKIE_NAME
Name of the cookie to set.
A2_LTPA_COOKIE_DOMAIN
Domain to set the cookie for cross-domain usage.
A2_LTPA_COOKIE_HTTP_ONLY
Should the cookie be only sent with HTTP request, default is true.

View File

@ -0,0 +1,7 @@
class Plugin(object):
def get_before_urls(self):
from . import urls
return urls.urlpatterns
def get_apps(self):
return [__name__]

View File

@ -0,0 +1,28 @@
class AppSettings(object):
__DEFAULTS = {
'TOKEN_DURATION': 8*3600,
'TOKEN_SECRET': None,
'COOKIE_NAME': 'domino',
'COOKIE_DOMAIN': None,
'COOKIE_HTTP_ONLY': True,
}
def __init__(self, prefix):
self.prefix = prefix
def _setting(self, name, dflt):
from django.conf import settings
return getattr(settings, self.prefix + name, dflt)
def __getattr__(self, name):
if name not in self.__DEFAULTS:
raise AttributeError(name)
return self._setting(name, self.__DEFAULTS[name])
# Ugly? Guido recommends this himself ...
# http://mail.python.org/pipermail/python-ideas/2012-May/014969.html
import sys
app_settings = AppSettings('A2_LTPA_')
app_settings.__name__ = __name__
sys.modules[__name__] = app_settings

View File

@ -0,0 +1 @@
# nothing

View File

@ -0,0 +1,5 @@
from django.conf.urls import patterns, url
urlpatterns = patterns('authentic2_idp_ltpa.views',
url('^idp/ltpa/$', 'ltpa'),
)

View File

@ -0,0 +1,98 @@
import hashlib
import time
import base64
def to_hex(l):
h = hex(long(l))[2:-1]
h = '0' * (8 - len(h)) + h
return h
def decode_secret(secret):
if secret.startswith('b64:'):
secret = secret[4:].decode('base64')
elif secret.startswith('hex:'):
secret = secret[4:].decode('hex')
assert len(secret) == 20, 'secret must be 20 bytes long'
return secret
def generate_domino_ltpa_token(user, secret, creation=None, expire=None,
user_charset='utf8', duration=3600):
'''Generate a Domino LTPA Token for a given user'''
if creation is None:
creation = time.time()
if expire is None:
expire = creation + duration
token = ''
# header
token += '\x00\x01\x02\x03'
#
token += to_hex(creation)
token += to_hex(expire)
token += user.encode(user_charset)
h = hashlib.sha1(token)
h.update(secret)
token += h.digest()
return base64.b64encode(token)
def parse_token(token, secret=None, user_charset='utf8'):
'''Parse a Domino LTPA token'''
token = token.strip()
try:
token = base64.b64decode(token)
except TypeError:
raise AssertionError('%r is not base64' % token)
assert len(token) > 40, 'token too short: %d < 41 bytes' % len(token)
header = token[:4]
assert header == '\x00\x01\x02\x03', 'wrong token header: %r' % header
creation = int(token[4:12], 16)
expire = int(token[12:20], 16)
digest = token[-20:]
user = token[20:-20].decode(user_charset)
if secret is not None:
computed_digest = hashlib.sha1(token[:-20]+secret).digest()
assert digest == computed_digest, 'invalid digest: %r != %r' % (
digest, computed_digest)
return user, creation, expire
if __name__ == '__main__':
import argparse
import datetime
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('--secret',
required=True,
help='secret as hex or b64 string, must be 20 bytes long, prefix '
'with hex: or b64:')
subparsers = parser.add_subparsers(help='sub-command help')
# create the parser for the "a" command
parser_generate = subparsers.add_parser('generate', help='a help')
parser_generate.set_defaults(command='generate')
parser_generate.add_argument('user', help='user\'s username')
# create the parser for the "b" command
parser_parse = subparsers.add_parser('parse', help='b help')
parser_parse.set_defaults(command='parse')
parser_parse.add_argument('token', help='the LTPA cookie content')
args = parser.parse_args()
if args.secret.startswith('hex:'):
secret = args.secret[4:].decode('hex')
elif args.secret.startswith('b64:'):
secret = args.secret[4:].decode('base64')
else:
secret = args.secret
assert len(secret) == 20, 'an LTPA secret must be 20 bytes long'
if args.command == 'generate':
print generate_domino_ltpa_token(user=args.user,
secret=secret)
elif args.command == 'parse':
user, creation, expire = parse_token(args.token, secret=secret)
def from_timestamp(t):
return datetime.datetime.utcfromtimestamp(t).isoformat() + 'Z'
print 'User:', user
print 'Creation timestamp:', from_timestamp(creation)
print 'Expire timestamp:', from_timestamp(expire)

View File

@ -0,0 +1,21 @@
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth import REDIRECT_FIELD_NAME
from . import app_settings, utils
@login_required
def ltpa(request):
'''Ask for authentication then generate a cookie'''
next_url = request.REQUEST[REDIRECT_FIELD_NAME]
response = HttpResponseRedirect(next_url)
if app_settings.TOKEN_SECRET is None:
raise ImproperlyConfigured('missing TOKEN_SECRET')
secret = utils.decode_secret(app_settings.TOKEN_SECRET)
token = utils.generate_domino_ltpa_token(request.user.username, secret)
domain = app_settings.COOKIE_DOMAIN or request.META['HTTP_HOST']
response.set_cookie(app_settings.COOKIE_NAME, token, domain=domain,
httponly=app_settings.COOKIE_HTTP_ONLY)
return response

20
setup.py Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/python
from setuptools import setup, find_packages
import os
setup(name='authentic2-idp-ltpa',
version='1.0',
license='AGPLv3',
description='Authentic2 IdP LTPA',
author="Entr'ouvert",
author_email="info@entrouvert.com",
packages=find_packages(os.path.dirname(__file__) or '.'),
install_requires=[
'djangorestframework',
],
entry_points={
'authentic2.plugin': [
'authentic-idp-ltpa = authentic2_idp_ltpa:Plugin',
],
},
)