summaryrefslogtreecommitdiffstats
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
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>
-rw-r--r--contrib/fedora/ipsilon.spec16
-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
-rwxr-xr-xsetup.py2
-rw-r--r--templates/install/idp.conf8
-rw-r--r--templates/persona/provisioning.html62
-rw-r--r--templates/persona/signin_result.html22
10 files changed, 405 insertions, 2 deletions
diff --git a/contrib/fedora/ipsilon.spec b/contrib/fedora/ipsilon.spec
index a28cbf3..0546296 100644
--- a/contrib/fedora/ipsilon.spec
+++ b/contrib/fedora/ipsilon.spec
@@ -12,6 +12,7 @@ BuildRequires: python2-devel
BuildRequires: python-setuptools
BuildRequires: lasso-python
BuildRequires: python-openid, python-openid-cla, python-openid-teams
+BuildRequires: m2crypto
Requires: ipsilon-tools = %{version}-%{release}
Requires: ipsilon-provider = %{version}-%{release}
Requires: mod_wsgi
@@ -67,6 +68,17 @@ Requires: python-openid-teams
Provides an OpenId provider plugin for the Ipsilon identity Provider
+%package persona
+Summary: Persona provider plugin
+Group: System Environment/Base
+License: GPLv3+
+Provides: ipsilon-provider = %{version}-%{release}
+Requires: m2crypto
+
+%description persona
+Provides a Persona provider plugin for the Ipsilon identity Provider
+
+
%package authfas
Summary: Fedora Authentication System login plugin
Group: System Environment/Base
@@ -192,6 +204,10 @@ fi
%{python2_sitelib}/ipsilon/providers/openid*
%{_datadir}/ipsilon/templates/openid/*
+%files persona
+%{python2_sitelib}/ipsilon/providers/persona*
+%{_datadir}/ipsilon/templates/persona/*
+
%files authfas
%{python2_sitelib}/ipsilon/login/authfas*
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'])
diff --git a/setup.py b/setup.py
index b3d5e96..27fd395 100755
--- a/setup.py
+++ b/setup.py
@@ -34,6 +34,7 @@ setup(
'ipsilon.providers', 'ipsilon.providers.saml2',
'ipsilon.providers.openid',
'ipsilon.providers.openid.extensions',
+ 'ipsilon.providers.persona',
'ipsilon.tools', 'ipsilon.helpers',
'tests', 'tests.helpers'],
data_files = [('share/man/man7', ["man/ipsilon.7"]),
@@ -50,6 +51,7 @@ setup(
(DATA+'templates/login', glob('templates/login/*.html')),
(DATA+'templates/saml2', glob('templates/saml2/*.html')),
(DATA+'templates/openid', glob('templates/openid/*.html')),
+ (DATA+'templates/persona', glob('templates/persona/*.html')),
(DATA+'templates/install', glob('templates/install/*.conf')),
(DATA+'templates/install/saml2',
glob('templates/install/saml2/*.conf')),
diff --git a/templates/install/idp.conf b/templates/install/idp.conf
index 19af096..9cf2595 100644
--- a/templates/install/idp.conf
+++ b/templates/install/idp.conf
@@ -1,4 +1,5 @@
Alias /${instance}/ui ${staticdir}/ui
+Alias /.well-known %{wellknowndir}
WSGIScriptAlias /${instance} ${ipsilondir}/ipsilon
WSGIDaemonProcess ${instance} user=${sysuser} group=${sysuser} home=${datadir}
${wsgi_socket}
@@ -15,3 +16,10 @@ ${sslrequiressl}
<Directory ${staticdir}>
Require all granted
</Directory>
+
+<Directory ${wellknowndir}>
+ Require all granted
+</Directory>
+<Location /.well-known/browserid>
+ ForceType application/json
+</Location>
diff --git a/templates/persona/provisioning.html b/templates/persona/provisioning.html
new file mode 100644
index 0000000..a693cac
--- /dev/null
+++ b/templates/persona/provisioning.html
@@ -0,0 +1,62 @@
+{% extends "master.html" %}
+{% block main %}
+<div class="col-sm-12">
+ <div id="welcome">
+ <p>This page is used internally</p>
+ </div>
+</div>
+
+<script type="text/javascript" src="https://login.persona.org/provisioning_api.js"></script>
+<script type="text/javascript">
+ var xmlhttp = new XMLHttpRequest()
+
+ var loggedin = {{ loggedin|lower }};
+
+ xmlhttp.onreadystatechange = function()
+ {
+ if(xmlhttp.readyState == 4)
+ {
+ if(xmlhttp.status == 200)
+ {
+ navigator.id.registerCertificate(xmlhttp.responseText);
+ }
+ else if((xmlhttp.status == 401) || (xmlhttp.status == 403))
+ {
+ navigator.id.raiseProvisioningFailure('Error in provisioning!');
+ }
+ else
+ {
+ alert("Response code: " + xmlhttp.status);
+ alert("Response text: " + xmlhttp.responseText);
+ }
+ }
+ }
+
+ function generateServerSide(email, publicKey, certDuration, callback)
+ {
+ xmlhttp.open("POST", "Sign/", true);
+ xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ xmlhttp.send("email=" + encodeURIComponent(email)
+ + "&publicKey=" + encodeURIComponent(publicKey)
+ + "&certDuration=" + encodeURIComponent(certDuration));
+ }
+
+ function startProvisioning()
+ {
+ navigator.id.beginProvisioning(function(email, certDuration)
+ {
+ if(loggedin)
+ {
+ navigator.id.genKeyPair(function(publicKey)
+ {
+ generateServerSide(email, publicKey, certDuration);
+ });
+ } else {
+ navigator.id.raiseProvisioningFailure('user is not authenticated');
+ }
+ });
+ }
+
+ startProvisioning();
+</script>
+{% endblock %}
diff --git a/templates/persona/signin_result.html b/templates/persona/signin_result.html
new file mode 100644
index 0000000..cda130d
--- /dev/null
+++ b/templates/persona/signin_result.html
@@ -0,0 +1,22 @@
+{% extends "master.html" %}
+{% block main %}
+<div class="col-sm-12">
+ <div id="welcome">
+ <p>This page is used internally</p>
+ </div>
+</div>
+
+<script type="text/javascript" src="https://login.persona.org/authentication_api.js"></script>
+<script type="text/javascript">
+ var loggedin = {{ loggedin|lower }};
+
+ if(loggedin)
+ {
+ navigator.id.beginAuthentication(function(email) {
+ navigator.id.completeAuthentication();
+ });
+ } else {
+ navigator.id.raiseAuthenticationFailure('User cancelled signon');
+ }
+</script>
+{% endblock %}