{"id":2344,"date":"2016-03-04T21:29:34","date_gmt":"2016-03-04T21:29:34","guid":{"rendered":"http:\/\/bogott.net\/unspecified\/?p=2344"},"modified":"2016-03-11T18:03:26","modified_gmt":"2016-03-11T18:03:26","slug":"keystone-horizon-and-multi-factor-auth-part-12-keystone","status":"publish","type":"post","link":"https:\/\/bogott.net\/unspecified\/?p=2344","title":{"rendered":"Keystone, Horizon, and multi-factor auth (part 1\/2:  Keystone)"},"content":{"rendered":"<p>Fair warning:  This post documents a recent work project &#8212; it contains neither lush landscape photos nor close-ups of underwater creatures.<\/p>\n<p>&#8212;<\/p>\n<p>For a couple of years now the <a href=\"https:\/\/wikitech.wikimedia.org\/wiki\/Portal:Wikimedia_Labs\">Wikimedia Labs<\/a> team has had ambitions to deprecate our homemade OpenStack web interface in favor of the official OpenStack user interface, <a href=\"http:\/\/docs.openstack.org\/developer\/horizon\/\">Horizon<\/a>.  There are dozens of issues to overcome in this transition, but one of the biggest is security:  We require two-factor authentication to access all potentially-destructive web interfaces in Labs; Horizon (and, until recently, Keystone) had no support for anything beyond simple username\/password logins.<\/p>\n<p>A stock install of Horizon has been publicly available and attached to the backend Labs services for quite a while, but most features were intentionally disabled.  Many, many volunteers have rights to manipulate VMs in labs, and I can&#8217;t run the risk of of a random stranger logging in with the password &#8216;password&#8217; and deleting a dozen instances.<\/p>\n<p>Our second factor is standard totp token, enforced on our current UI by the <a href=\"https:\/\/www.mediawiki.org\/wiki\/Extension:OATHAuth\">OATHAuth mediawiki extension<\/a>.  Since the migration to Horizon could take a year or more, I want users to be able to use the same credentials on both systems.  That means we needed a Keystone plugin that used the same keys that are currently used on our existing system.  At my request, security engineer <a href=\"https:\/\/www.mediawiki.org\/wiki\/User:CSteipp_%28WMF%29\">Chris Steipp<\/a> set up a devstack instance and quickly rattled off a Keystone plugin that works in OpenStack Kilo and Liberty and can authenticate against our existing keys.  Here&#8217;s the good bit:<\/p>\n<p><code><br \/>\n@dependency.requires('identity_api')<br \/>\nclass Wmtotp(auth.AuthMethodHandler):<\/p>\n<p>    method = METHOD_NAME<\/p>\n<p>    def authenticate(self, context, auth_payload, auth_context):<br \/>\n        \"\"\"Try to authenticate against the identity backend.\"\"\"<br \/>\n        user_info = auth_plugins.UserAuthInfo.create(auth_payload, self.method)<\/p>\n<p>        try:<br \/>\n            self.identity_api.authenticate(<br \/>\n                context,<br \/>\n                user_id=user_info.user_id,<br \/>\n                password=user_info.password)<br \/>\n        except AssertionError:<br \/>\n            # authentication failed because of invalid username or password<br \/>\n            msg = _('Invalid username or password')<br \/>\n            raise exception.Unauthorized(msg)<\/p>\n<p>        # Password auth succeeded, check two-factor<br \/>\n        # LOG.debug(\"OATH: Doing 2FA for user_info \" +<br \/>\n        #     ( \"%s(%r)\" % (user_info.__class__, user_info.__dict__) ) )<br \/>\n        # LOG.debug(\"OATH: Doing 2FA for auth_payload \" +<br \/>\n        #     ( \"%s(%r)\" % (auth_payload.__class__, auth_payload) ) )<br \/>\n        cnx = mysql.connector.connect(<br \/>\n            user=CONF.oath.dbuser,<br \/>\n            password=CONF.oath.dbpass,<br \/>\n            database=CONF.oath.dbname,<br \/>\n            host=CONF.oath.dbhost)<br \/>\n        cur = cnx.cursor(buffered=True)<br \/>\n        sql = ('SELECT oath.secret as secret from user '<br \/>\n               'left join oathauth_users as oath on oath.id = user.user_id '<br \/>\n               'where user.user_name = %s LIMIT 1')<br \/>\n        cur.execute(sql, (user_info.user_ref['name'], ))<br \/>\n        secret = cur.fetchone()[0]<\/p>\n<p>        if secret:<br \/>\n            if 'totp' in auth_payload['user']:<br \/>\n                (p, d) = oath.accept_totp(<br \/>\n                    base64.b16encode(base64.b32decode(secret)),<br \/>\n                    auth_payload['user']['totp'])<br \/>\n                if p:<br \/>\n                    LOG.debug(\"OATH: 2FA passed\")<br \/>\n                else:<br \/>\n                    LOG.debug(\"OATH: 2FA failed\")<br \/>\n                    msg = _('Invalid two-factor token')<br \/>\n                    raise exception.Unauthorized(msg)<br \/>\n            else:<br \/>\n                LOG.debug(\"OATH: 2FA failed, missing totp param\")<br \/>\n                msg = _('Missing two-factor token')<br \/>\n                raise exception.Unauthorized(msg)<br \/>\n        else:<br \/>\n            LOG.debug(\"OATH: user '%s' does not have 2FA enabled.\",<br \/>\n                      user_info.user_ref['name'])<\/p>\n<p>        auth_context['user_id'] = user_info.user_id<br \/>\n<\/code><\/p>\n<p>Rather than build a new Keystone package with the plugin included, I just wrote a <a href=\"https:\/\/gerrit.wikimedia.org\/r\/#\/c\/274167\/\">puppet patch<\/a> to drop it into a strategic location.  That turns out to be easy because Keystone is designed with this kind of extensibility in mind.  (Extensibility patterns tend to change with every release, but that&#8217;s a rant for another day.)<\/p>\n<p>The current trunk of Keystone includes a plugin called &#8216;totp.py&#8217; which I haven&#8217;t read but which surely duplicates some or all of Chris&#8217;s plugin.  As we draw nearer to running Newton we&#8217;ll investigate whether or not our custom code can be reduced.<\/p>\n<p>Next post:  Getting that OTP from the user and handling it in Horizon.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Fair warning: This post documents a recent work project &#8212; it contains neither lush landscape photos nor close-ups of underwater creatures. &#8212; For a couple of years now the Wikimedia Labs team has had ambitions to deprecate our homemade OpenStack &hellip; <a href=\"https:\/\/bogott.net\/unspecified\/?p=2344\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9],"tags":[],"class_list":["post-2344","post","type-post","status-publish","format-standard","hentry","category-operations"],"_links":{"self":[{"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=\/wp\/v2\/posts\/2344","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2344"}],"version-history":[{"count":9,"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=\/wp\/v2\/posts\/2344\/revisions"}],"predecessor-version":[{"id":2369,"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=\/wp\/v2\/posts\/2344\/revisions\/2369"}],"wp:attachment":[{"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2344"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2344"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bogott.net\/unspecified\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2344"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}