first commit
This commit is contained in:
commit
73c33d8c60
|
@ -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.
|
|
@ -0,0 +1,7 @@
|
|||
class Plugin(object):
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
def get_apps(self):
|
||||
return [__name__]
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
# nothing
|
|
@ -0,0 +1,5 @@
|
|||
from django.conf.urls import patterns, url
|
||||
|
||||
urlpatterns = patterns('authentic2_idp_ltpa.views',
|
||||
url('^idp/ltpa/$', 'ltpa'),
|
||||
)
|
|
@ -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)
|
|
@ -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
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
)
|
Reference in New Issue