summaryrefslogtreecommitdiffstats
path: root/ipsilon
diff options
context:
space:
mode:
authorPatrick Uiterwijk <puiterwijk@redhat.com>2014-11-13 10:18:05 +0100
committerSimo Sorce <simo@redhat.com>2014-11-14 13:06:27 -0500
commit943158d19f879eb6ad515edeb59017671e4252c5 (patch)
tree17afd88f27a156b95224b666d4270be2ae66890d /ipsilon
parentd4f4bc7b1ed80887534698825fc93ff3cf68dfe7 (diff)
downloadipsilon-943158d19f879eb6ad515edeb59017671e4252c5.tar.gz
ipsilon-943158d19f879eb6ad515edeb59017671e4252c5.tar.xz
ipsilon-943158d19f879eb6ad515edeb59017671e4252c5.zip
Add support for Persona Identity Provider
Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com> Reviewed-by: Simo Sorce <simo@redhat.com>
Diffstat (limited to 'ipsilon')
-rwxr-xr-xipsilon/install/ipsilon-server-install9
-rwxr-xr-xipsilon/login/common.py6
-rw-r--r--ipsilon/providers/persona/__init__.py0
-rwxr-xr-xipsilon/providers/persona/auth.py152
-rwxr-xr-xipsilon/providers/personaidp.py130
5 files changed, 295 insertions, 2 deletions
diff --git a/ipsilon/install/ipsilon-server-install b/ipsilon/install/ipsilon-server-install
index df2a965..1b9e58f 100755
--- a/ipsilon/install/ipsilon-server-install
+++ b/ipsilon/install/ipsilon-server-install
@@ -93,6 +93,9 @@ def install(plugins, args):
args['httpd_conf'] = os.path.join(HTTPDCONFD,
'ipsilon-%s.conf' % args['instance'])
args['data_dir'] = os.path.join(DATADIR, args['instance'])
+ args['public_data_dir'] = os.path.join(args['data_dir'], 'public')
+ args['wellknown_dir'] = os.path.join(args['public_data_dir'],
+ 'well-known')
if os.path.exists(ipsilon_conf):
shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
if os.path.exists(idp_conf):
@@ -101,6 +104,8 @@ def install(plugins, args):
os.makedirs(instance_conf, 0700)
confopts = {'instance': args['instance'],
'datadir': args['data_dir'],
+ 'publicdatadir': args['public_data_dir'],
+ 'wellknowndir': args['wellknown_dir'],
'sysuser': args['system_user'],
'ipsilondir': BINDIR,
'staticdir': STATICDIR,
@@ -142,6 +147,10 @@ def install(plugins, args):
confopts)
if not os.path.exists(args['httpd_conf']):
os.symlink(idp_conf, args['httpd_conf'])
+ if not os.path.exists(args['public_data_dir']):
+ os.makedirs(args['public_data_dir'], 0755)
+ if not os.path.exists(args['wellknown_dir']):
+ os.makedirs(args['wellknown_dir'], 0755)
sessdir = os.path.join(args['data_dir'], 'sessions')
if not os.path.exists(sessdir):
os.makedirs(sessdir, 0700)
diff --git a/ipsilon/login/common.py b/ipsilon/login/common.py
index b394fa0..ce921c5 100755
--- a/ipsilon/login/common.py
+++ b/ipsilon/login/common.py
@@ -179,16 +179,18 @@ class LoginFormBase(LoginPageBase):
cookie = SecureCookie(USERNAME_COOKIE)
cookie.receive()
username = cookie.value
- if username is None:
- username = ''
target = None
if self.trans is not None:
tid = self.trans.transaction_id
target = self.trans.retrieve().get('login_target')
+ username = self.trans.retrieve().get('login_username')
if tid is None:
tid = ''
+ if username is None:
+ username = ''
+
context = {
"title": 'Login',
"action": '%s/%s' % (self.basepath, self.formpage),
diff --git a/ipsilon/providers/persona/__init__.py b/ipsilon/providers/persona/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ipsilon/providers/persona/__init__.py
diff --git a/ipsilon/providers/persona/auth.py b/ipsilon/providers/persona/auth.py
new file mode 100755
index 0000000..a8e771b
--- /dev/null
+++ b/ipsilon/providers/persona/auth.py
@@ -0,0 +1,152 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 Ipsilon project Contributors, for licensee see COPYING
+
+from ipsilon.providers.common import ProviderPageBase
+from ipsilon.util.trans import Transaction
+from ipsilon.util.user import UserSession
+
+import base64
+import cherrypy
+import time
+import json
+import M2Crypto
+
+
+class AuthenticateRequest(ProviderPageBase):
+
+ def __init__(self, *args, **kwargs):
+ super(AuthenticateRequest, self).__init__(*args, **kwargs)
+ self.trans = None
+
+ def _preop(self, *args, **kwargs):
+ try:
+ # generate a new id or get current one
+ self.trans = Transaction('persona', **kwargs)
+ if self.trans.cookie.value != self.trans.provider:
+ self.debug('Invalid transaction, %s != %s' % (
+ self.trans.cookie.value, self.trans.provider))
+ except Exception, e: # pylint: disable=broad-except
+ self.debug('Transaction initialization failed: %s' % repr(e))
+ raise cherrypy.HTTPError(400, 'Invalid transaction id')
+
+ def pre_GET(self, *args, **kwargs):
+ self._preop(*args, **kwargs)
+
+ def pre_POST(self, *args, **kwargs):
+ self._preop(*args, **kwargs)
+
+
+class Sign(AuthenticateRequest):
+
+ def _base64_url_decode(self, inp):
+ inp += '=' * (4 - (len(inp) % 4))
+ return base64.urlsafe_b64decode(inp)
+
+ def _base64_url_encode(self, inp):
+ return base64.urlsafe_b64encode(inp).replace('=', '')
+
+ def _persona_sign(self, email, publicKey, certDuration):
+ self.debug('Signing for %s with duration of %s' % (email,
+ certDuration))
+ header = {'alg': 'RS256'}
+ header = json.dumps(header)
+ header = self._base64_url_encode(header)
+
+ claim = {}
+ # Valid from 10 seconds before now to account for clock skew
+ claim['iat'] = 1000 * int(time.time() - 10)
+ # Validity of at most 24 hours
+ claim['exp'] = 1000 * int(time.time() +
+ min(certDuration, 24 * 60 * 60))
+
+ claim['iss'] = self.cfg.issuer_domain
+ claim['public-key'] = json.loads(publicKey)
+ claim['principal'] = {'email': email}
+
+ claim = json.dumps(claim)
+ claim = self._base64_url_encode(claim)
+
+ certificate = '%s.%s' % (header, claim)
+ digest = M2Crypto.EVP.MessageDigest('sha256')
+ digest.update(certificate)
+ signature = self.cfg.key.sign(digest.digest(), 'sha256')
+ signature = self._base64_url_encode(signature)
+ signed_certificate = '%s.%s' % (certificate, signature)
+
+ return signed_certificate
+
+ def _willing_to_sign(self, email, username):
+ for domain in self.cfg.allowed_domains:
+ if email == ('%s@%s' % (username, domain)):
+ return True
+ return False
+
+ def POST(self, *args, **kwargs):
+ if 'email' not in kwargs or 'publicKey' not in kwargs \
+ or 'certDuration' not in kwargs or '@' not in kwargs['email']:
+ cherrypy.response.status = 400
+ raise Exception('Invalid request: %s' % kwargs)
+
+ us = UserSession()
+ user = us.get_user()
+
+ if user.is_anonymous:
+ raise cherrypy.HTTPError(401, 'Not signed in')
+
+ if not self._willing_to_sign(kwargs['email'], user.name):
+ self.log('Not willing to sign for %s, logged in as %s' % (
+ kwargs['email'], user.name))
+ raise cherrypy.HTTPError(403, 'Incorrect user')
+
+ return self._persona_sign(kwargs['email'], kwargs['publicKey'],
+ kwargs['certDuration'])
+
+
+class SignInResult(AuthenticateRequest):
+ def GET(self, *args, **kwargs):
+ user = UserSession().get_user()
+
+ return self._template('persona/signin_result.html',
+ loggedin=not user.is_anonymous)
+
+
+class SignIn(AuthenticateRequest):
+ def __init__(self, *args, **kwargs):
+ super(SignIn, self).__init__(*args, **kwargs)
+ self.result = SignInResult(*args, **kwargs)
+ self.trans = None
+
+ def GET(self, *args, **kwargs):
+ username = None
+ domain = None
+ if 'email' in kwargs:
+ if '@' in kwargs['email']:
+ username, domain = kwargs['email'].split('@', 2)
+ self.debug('Persona SignIn requested for: %s@%s' % (username,
+ domain))
+
+ returl = '%s/persona/SignIn/result?%s' % (
+ self.basepath, self.trans.get_GET_arg())
+ data = {'login_return': returl,
+ 'login_target': 'Persona',
+ 'login_username': username}
+ self.trans.store(data)
+ redirect = '%s/login?%s' % (self.basepath,
+ self.trans.get_GET_arg())
+ self.debug('Redirecting: %s' % redirect)
+ raise cherrypy.HTTPRedirect(redirect)
+
+
+class Persona(AuthenticateRequest):
+
+ def __init__(self, *args, **kwargs):
+ super(Persona, self).__init__(*args, **kwargs)
+ self.Sign = Sign(*args, **kwargs)
+ self.SignIn = SignIn(*args, **kwargs)
+ self.trans = None
+
+ def GET(self, *args, **kwargs):
+ user = UserSession().get_user()
+ return self._template('persona/provisioning.html',
+ loggedin=not user.is_anonymous)
diff --git a/ipsilon/providers/personaidp.py b/ipsilon/providers/personaidp.py
new file mode 100755
index 0000000..355726d
--- /dev/null
+++ b/ipsilon/providers/personaidp.py
@@ -0,0 +1,130 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 Ipsilon project Contributors, for licensee see COPYING
+
+from __future__ import absolute_import
+
+from ipsilon.providers.common import ProviderBase
+from ipsilon.util.plugin import PluginObject
+from ipsilon.util import config as pconfig
+from ipsilon.info.common import InfoMapping
+from ipsilon.providers.persona.auth import Persona
+from ipsilon.tools import files
+
+import json
+import M2Crypto
+import os
+
+
+class IdpProvider(ProviderBase):
+
+ def __init__(self, *pargs):
+ super(IdpProvider, self).__init__('persona', 'persona', *pargs)
+ self.mapping = InfoMapping()
+ self.page = None
+ self.basepath = None
+ self.key = None
+ self.key_info = None
+ self.description = """
+Provides Persona authentication infrastructure. """
+
+ self.new_config(
+ self.name,
+ pconfig.String(
+ 'issuer domain',
+ 'The issuer domain of the Persona provider',
+ 'localhost'),
+ pconfig.String(
+ 'idp key file',
+ 'The key where the Persona key is stored.',
+ 'persona.key'),
+ pconfig.List(
+ 'allowed domains',
+ 'List of domains this IdP is willing to issue claims for.'),
+ )
+
+ @property
+ def issuer_domain(self):
+ return self.get_config_value('issuer domain')
+
+ @property
+ def idp_key_file(self):
+ return self.get_config_value('idp key file')
+
+ @property
+ def allowed_domains(self):
+ return self.get_config_value('allowed domains')
+
+ def get_tree(self, site):
+ self.init_idp()
+ self.page = Persona(site, self)
+ # self.admin = AdminPage(site, self)
+
+ return self.page
+
+ def init_idp(self):
+ # Init IDP data
+ try:
+ self.key = M2Crypto.RSA.load_key(self.idp_key_file,
+ lambda *args: None)
+ except Exception, e: # pylint: disable=broad-except
+ self._debug('Failed to init Persona provider: %r' % e)
+ return None
+
+ def on_enable(self):
+ super(IdpProvider, self).on_enable()
+ self.init_idp()
+
+
+class Installer(object):
+
+ def __init__(self, *pargs):
+ self.name = 'persona'
+ self.ptype = 'provider'
+ self.pargs = pargs
+
+ def install_args(self, group):
+ group.add_argument('--persona', choices=['yes', 'no'], default='yes',
+ help='Configure Persona Provider')
+
+ def configure(self, opts):
+ if opts['persona'] != 'yes':
+ return
+
+ # Check storage path is present or create it
+ path = os.path.join(opts['data_dir'], 'persona')
+ if not os.path.exists(path):
+ os.makedirs(path, 0700)
+
+ keyfile = os.path.join(path, 'persona.key')
+ exponent = 0x10001
+ key = M2Crypto.RSA.gen_key(2048, exponent)
+ key.save_key(keyfile, cipher=None)
+ key_n = 0
+ for c in key.n[4:]:
+ key_n = (key_n*256) + ord(c)
+ wellknown = dict()
+ wellknown['authentication'] = '/%s/persona/SignIn/' % opts['instance']
+ wellknown['provisioning'] = '/%s/persona/' % opts['instance']
+ wellknown['public-key'] = {'algorithm': 'RS',
+ 'e': str(exponent),
+ 'n': str(key_n)}
+ with open(os.path.join(opts['wellknown_dir'], 'browserid'), 'w') as f:
+ f.write(json.dumps(wellknown))
+
+ # Add configuration data to database
+ po = PluginObject(*self.pargs)
+ po.name = 'persona'
+ po.wipe_data()
+ po.wipe_config_values()
+ config = {'issuer domain': opts['hostname'],
+ 'idp key file': keyfile,
+ 'allowed domains': opts['hostname']}
+ po.save_plugin_config(config)
+
+ # Update global config to add login plugin
+ po.is_enabled = True
+ po.save_enabled_state()
+
+ # Fixup permissions so only the ipsilon user can read these files
+ files.fix_user_dirs(path, opts['system_user'])