#295 Kill the Persona IdP plugin
Opened 6 years ago by puiterwijk. Modified 6 years ago
puiterwijk/ipsilon personaisdead  into  master

@@ -153,19 +153,6 @@ 

  identity Provider

  

  

- %package persona

- Summary:        Persona provider plugin

- Group:          System Environment/Base

- License:        GPLv3+

- Provides:       ipsilon-provider = %{version}-%{release}

- Requires:       %{name} = %{version}-%{release}

- Requires:       m2crypto

- BuildArch:      noarch

- 

- %description persona

- Provides a Persona provider plugin for the Ipsilon identity Provider

- 

- 

  %package authfas

  Summary:        Fedora Authentication System login plugin

  Group:          System Environment/Base
@@ -394,10 +381,6 @@ 

  %{python2_sitelib}/ipsilon/providers/openidc/

  %{_datadir}/ipsilon/templates/openidc/

  

- %files persona

- %{python2_sitelib}/ipsilon/providers/persona*

- %{_datadir}/ipsilon/templates/persona

- 

  %files authfas

  %{python2_sitelib}/ipsilon/login/authfas*

  

@@ -1,146 +0,0 @@ 

- # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING

- 

- from ipsilon.providers.common import ProviderPageBase

- from ipsilon.util.user import UserSession

- from ipsilon.util.endpoint import allow_iframe

- 

- import base64

- import cherrypy

- import time

- import json

- import M2Crypto

- 

- 

- class AuthenticateRequest(ProviderPageBase):

- 

-     def __init__(self, site, provider, *args, **kwargs):

-         super(AuthenticateRequest, self).__init__(site, provider)

-         self.trans = None

- 

-     def _preop(self, *args, **kwargs):

-         self.trans = self.get_valid_transaction('persona', **kwargs)

- 

-     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

- 

-     @allow_iframe

-     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):

-     @allow_iframe

-     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

- 

-     @allow_iframe

-     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

- 

-     @allow_iframe

-     def GET(self, *args, **kwargs):

-         user = UserSession().get_user()

-         return self._template('persona/provisioning.html',

-                               loggedin=not user.is_anonymous)

@@ -1,135 +0,0 @@ 

- # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING

- 

- from __future__ import absolute_import

- 

- from ipsilon.providers.common import ProviderBase, ProviderInstaller

- 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', '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.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 as 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()

- 

-     def get_client_display_name(self, clientid):

-         return clientid

- 

-     def consent_to_display(self, consentdata):

-         return []

- 

- 

- class Installer(ProviderInstaller):

- 

-     def __init__(self, *pargs):

-         super(Installer, self).__init__()

-         self.name = 'persona'

-         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, changes):

-         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, 0o700)

- 

-         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['instanceurl'])

-         wellknown['provisioning'] = '%s/persona/' % opts['instanceurl']

-         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'])

file modified
+6 -3
@@ -5,7 +5,7 @@ 

  from ipsilon.util import errors

  from ipsilon.login.common import Login

  from ipsilon.login.common import Logout

- from ipsilon.admin.common import Admin

+ from ipsilon.admin.common import Admin, AdminError

  from ipsilon.providers.common import LoadProviders

  from ipsilon.admin.loginstack import LoginStack

  from ipsilon.admin.info import InfoPlugins
@@ -65,8 +65,11 @@ 

          providers = []

          for plugin in self._site['provider_config'].enabled:

              # pylint: disable=no-member,protected-access

-             obj = self.admin.providers._get_plugin_obj(plugin)

-             providers.extend(obj.get_providers())

+             try:

+                 obj = self.admin.providers._get_plugin_obj(plugin)

+                 providers.extend(obj.get_providers())

+             except AdminError:

+                 self.error('Failed to load plugin %s' % plugin)

          providers = sorted(providers, key=lambda provider: provider.name)

          return self._template('index.html', title='Ipsilon',

                                providers=providers, heads=self.html_heads)

@@ -6,6 +6,7 @@ 

  import os

  from jinja2 import Environment, FileSystemLoader

  import ipsilon.util.sessions

+ from ipsilon.admin.common import AdminError

  from ipsilon.util.data import AdminStore, Store, UserStore, TranStore

  from ipsilon.util.sessions import SqlSession, EtcdSession

  from ipsilon.root import Root
@@ -115,6 +116,9 @@ 

                       'authz_config']:

          for plugin in root._site[facility].enabled:

              logger.info('Handling plugin %s', plugin)

+             if not plugin in root._site[facility].available:

+                 logger.error('Plugin was unavailable')

+                 continue

              plugin = root._site[facility].available[plugin]

              logger.info('Creating plugin AdminStore table')

              adminstore.create_plugin_data_table(plugin.name)

@@ -87,9 +87,6 @@ 

  \fB\-\-openid\-dburi\fR \fIOPENID_DBURI\fR

  OpenID database URI (override template)

  .TP

- \fB\-\-persona\fR

- Configure Persona Provider

- .TP

  \fB\-\-saml2\fR

  Configure SAML2 Provider

  .TP

file modified
-2
@@ -35,7 +35,6 @@ 

                'ipsilon.providers.openid.extensions',

                'ipsilon.providers.openidc',

                'ipsilon.providers.openidc.plugins',

-               'ipsilon.providers.persona',

                'ipsilon.authz', 'ipsilon.user',

                'ipsilon.tools', 'ipsilon.helpers',

                'tests', 'tests.helpers'],
@@ -59,7 +58,6 @@ 

                  (DATA+'templates/saml2', glob('templates/saml2/*.html')),

                  (DATA+'templates/openid', glob('templates/openid/*')),

                  (DATA+'templates/openidc', glob('templates/openidc/*')),

-                 (DATA+'templates/persona', glob('templates/persona/*.html')),

                  (DATA+'templates/install', glob('templates/install/*.conf')),

                  (DATA+'templates/install/openidc',

                   glob('templates/install/openidc/*.conf')),

@@ -1,62 +0,0 @@ 

- {% 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 %}

@@ -1,22 +0,0 @@ 

- {% 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 %}

I have explicitly not removed the entries from the dbupgrades script, to make sure that we verify we work correctly in upgrade cases.

Sad to see it go, but LGTM

Only one comment though.
The failure on plugin loading was intentional, as otherwise some missing plugins that may restrict access would cause auth to fail open.
I would rather just explicitly handle retired plugins in a white list to allow upgrades to not fail but not allow all plugins to fail without consequences.

Just my 2c

Probably worth removing the reference to Persona in man/ipsilon-server-install.1 too. Do we want to add an obsolete to the spec file too?

Simo raises a good point too, but I'm happy with going with this fix for now, and filing an issue to do it "properly" later on.

Beyond that, looks good.