commit 73c33d8c60a9df56ef5db26a94016ea41688db78 Author: Benjamin Dauvergne Date: Mon Mar 10 12:46:29 2014 +0100 first commit diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..e614c37 --- /dev/null +++ b/README.txt @@ -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. diff --git a/authentic2_idp_ltpa/__init__.py b/authentic2_idp_ltpa/__init__.py new file mode 100644 index 0000000..3b6ffc0 --- /dev/null +++ b/authentic2_idp_ltpa/__init__.py @@ -0,0 +1,7 @@ +class Plugin(object): + def get_before_urls(self): + from . import urls + return urls.urlpatterns + + def get_apps(self): + return [__name__] diff --git a/authentic2_idp_ltpa/app_settings.py b/authentic2_idp_ltpa/app_settings.py new file mode 100644 index 0000000..5f859e5 --- /dev/null +++ b/authentic2_idp_ltpa/app_settings.py @@ -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 diff --git a/authentic2_idp_ltpa/models.py b/authentic2_idp_ltpa/models.py new file mode 100644 index 0000000..556df42 --- /dev/null +++ b/authentic2_idp_ltpa/models.py @@ -0,0 +1 @@ +# nothing diff --git a/authentic2_idp_ltpa/urls.py b/authentic2_idp_ltpa/urls.py new file mode 100644 index 0000000..4225ea8 --- /dev/null +++ b/authentic2_idp_ltpa/urls.py @@ -0,0 +1,5 @@ +from django.conf.urls import patterns, url + +urlpatterns = patterns('authentic2_idp_ltpa.views', + url('^idp/ltpa/$', 'ltpa'), +) diff --git a/authentic2_idp_ltpa/utils.py b/authentic2_idp_ltpa/utils.py new file mode 100644 index 0000000..ab51957 --- /dev/null +++ b/authentic2_idp_ltpa/utils.py @@ -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) diff --git a/authentic2_idp_ltpa/views.py b/authentic2_idp_ltpa/views.py new file mode 100644 index 0000000..e3f80bf --- /dev/null +++ b/authentic2_idp_ltpa/views.py @@ -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 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..22122c4 --- /dev/null +++ b/setup.py @@ -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', + ], + }, +)