Support TOTP auth plugin

Add support for time-based one-time password (TOTP) authentication.

Change-Id: I004677ac7f0e2fb8c059ad14868e661e8ee4c1f9
This commit is contained in:
guang-yee 2016-02-16 22:17:16 -08:00
parent fc95d25544
commit 9e29e6e9c5
8 changed files with 210 additions and 16 deletions

View File

@ -55,6 +55,8 @@ this V3 defines a number of different
against a V3 identity service using a username and password.
- :py:class:`~keystoneauth1.identity.v3.TokenMethod`: Authenticate against
a V3 identity service using an existing token.
- :py:class:`~keystoneauth1.identity.v3.TOTPMethod`: Authenticate against
a V3 identity service using Time-Based One-Time Password (TOTP).
- :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`: Authenticate
against a V3 identity service using Kerberos.
@ -81,6 +83,8 @@ like the V2 plugins:
only a :py:class:`~keystoneauth1.identity.v3.PasswordMethod`.
- :py:class:`~keystoneauth1.identity.v3.Token`: Authenticate using only a
:py:class:`~keystoneauth1.identity.v3.TokenMethod`.
- :py:class:`~keystoneauth1.identity.v3.TOTP`: Authenticate using
only a :py:class:`~keystoneauth1.identity.v3.TOTPMethod`.
- :py:class:`~keystoneauth1.extras.kerberos.Kerberos`: Authenticate using
only a :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`.
@ -175,6 +179,7 @@ authentication plugins that are available in `keystoneauth` are:
- v2token: :py:class:`keystoneauth1.identity.v2.Token`
- v3password: :py:class:`keystoneauth1.identity.v3.Password`
- v3token: :py:class:`keystoneauth1.identity.v3.Token`
- v3totp: :py:class:`keystoneauth1.identity.v3.TOTP`
Creating Authentication Plugins

View File

@ -46,6 +46,9 @@ V3OidcAuthorizationCode = oidc.OidcAuthorizationCode
V3OidcAccessToken = oidc.OidcAccessToken
"""See :class:`keystoneauth1.identity.v3.oidc.OidcAccessToken`"""
V3TOTP = v3.TOTP
"""See :class:`keystoneauth1.identity.v3.TOTP`"""
__all__ = ('BaseIdentityPlugin',
'Password',
'Token',
@ -55,4 +58,5 @@ __all__ = ('BaseIdentityPlugin',
'V3Token',
'V3OidcPassword',
'V3OidcAuthorizationCode',
'V3OidcAccessToken')
'V3OidcAccessToken',
'V3TOTP')

View File

@ -13,9 +13,10 @@
from keystoneauth1.identity.v3.base import * # noqa
from keystoneauth1.identity.v3.federation import * # noqa
from keystoneauth1.identity.v3.k2k import * # noqa
from keystoneauth1.identity.v3.oidc import * # noqa
from keystoneauth1.identity.v3.password import * # noqa
from keystoneauth1.identity.v3.token import * # noqa
from keystoneauth1.identity.v3.oidc import * # noqa
from keystoneauth1.identity.v3.totp import * # noqa
__all__ = ('Auth',
@ -34,4 +35,7 @@ __all__ = ('Auth',
'TokenMethod',
'OidcAuthorizationCode',
'OidcPassword',)
'OidcPassword',
'TOTPMethod',
'TOTP')

View File

@ -0,0 +1,81 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
from keystoneauth1.identity.v3 import base
__all__ = ('TOTPMethod', 'TOTP')
class TOTPMethod(base.AuthMethod):
"""Construct a User/Passcode based authentication method.
:param string passcode: TOTP passcode for authentication.
:param string username: Username for authentication.
:param string user_id: User ID for authentication.
:param string user_domain_id: User's domain ID for authentication.
:param string user_domain_name: User's domain name for authentication.
"""
_method_parameters = ['user_id',
'username',
'user_domain_id',
'user_domain_name',
'passcode']
def get_auth_data(self, session, auth, headers, **kwargs):
user = {'passcode': self.passcode}
if self.user_id:
user['id'] = self.user_id
elif self.username:
user['name'] = self.username
if self.user_domain_id:
user['domain'] = {'id': self.user_domain_id}
elif self.user_domain_name:
user['domain'] = {'name': self.user_domain_name}
return 'totp', {'user': user}
def get_cache_id_elements(self):
# NOTE(gyee): passcode is not static so we cannot use it as part of
# the key in caching.
params = copy.copy(self._method_parameters)
params.remove('passcode')
return dict(('totp_%s' % p, getattr(self, p))
for p in self._method_parameters)
class TOTP(base.AuthConstructor):
"""A plugin for authenticating with a username and TOTP passcode.
:param string auth_url: Identity service endpoint for authentication.
:param string passcode: TOTP passcode for authentication.
:param string username: Username for authentication.
:param string user_id: User ID for authentication.
:param string user_domain_id: User's domain ID for authentication.
:param string user_domain_name: User's domain name for authentication.
:param string trust_id: Trust ID for trust scoping.
:param string domain_id: Domain ID for domain scoping.
:param string domain_name: Domain name for domain scoping.
:param string project_id: Project ID for project scoping.
:param string project_name: Project name for project scoping.
:param string project_domain_id: Project's domain ID for project.
:param string project_domain_name: Project's domain name for project.
:param bool reauthenticate: Allow fetching a new token if the current one
is going to expire. (optional) default True
"""
_auth_method_class = TOTPMethod

View File

@ -15,6 +15,27 @@ from keystoneauth1 import identity
from keystoneauth1 import loading
def _add_common_identity_options(options):
options.extend([
loading.Opt('user-id', help='User ID'),
loading.Opt('username',
help='Username',
deprecated=[loading.Opt('user-name')]),
loading.Opt('user-domain-id', help="User's domain id"),
loading.Opt('user-domain-name', help="User's domain name"),
])
def _assert_identity_options(options):
if (options.get('username') and
not (options.get('user_domain_name') or
options.get('user_domain_id'))):
m = "You have provided a username. In the V3 identity API a " \
"username is only unique within a domain so you must " \
"also provide either a user_domain_id or user_domain_name."
raise exceptions.OptionError(m)
class Password(loading.BaseV3Loader):
@property
@ -23,27 +44,16 @@ class Password(loading.BaseV3Loader):
def get_options(self):
options = super(Password, self).get_options()
_add_common_identity_options(options)
options.extend([
loading.Opt('user-id', help='User ID'),
loading.Opt('username',
help='Username',
deprecated=[loading.Opt('user-name')]),
loading.Opt('user-domain-id', help="User's domain id"),
loading.Opt('user-domain-name', help="User's domain name"),
loading.Opt('password', secret=True, help="User's password"),
])
return options
def load_from_options(self, **kwargs):
if (kwargs.get('username') and
not (kwargs.get('user_domain_name') or
kwargs.get('user_domain_id'))):
m = "You have provided a username. In the V3 identity API a " \
"username is only unique within a domain so you must " \
"also provide either a user_domain_id or user_domain_name."
raise exceptions.OptionError(m)
_assert_identity_options(kwargs)
return super(Password, self).load_from_options(**kwargs)
@ -139,3 +149,25 @@ class OpenIDConnectAccessToken(loading.BaseFederationLoader):
help='OAuth 2.0 Access Token'),
])
return options
class TOTP(loading.BaseV3Loader):
@property
def plugin_class(self):
return identity.V3TOTP
def get_options(self):
options = super(TOTP, self).get_options()
_add_common_identity_options(options)
options.extend([
loading.Opt('passcode', secret=True, help="User's TOTP passcode"),
])
return options
def load_from_options(self, **kwargs):
_assert_identity_options(kwargs)
return super(TOTP, self).load_from_options(**kwargs)

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import random
import uuid
from keystoneauth1 import exceptions
@ -64,3 +65,53 @@ class V3PasswordTests(utils.TestCase):
password=uuid.uuid4().hex,
user_domain_id=uuid.uuid4().hex,
project_name=uuid.uuid4().hex)
class TOTPTests(utils.TestCase):
def setUp(self):
super(TOTPTests, self).setUp()
self.auth_url = uuid.uuid4().hex
def create(self, **kwargs):
kwargs.setdefault('auth_url', self.auth_url)
loader = loading.get_plugin_loader('v3totp')
return loader.load_from_options(**kwargs)
def test_basic(self):
username = uuid.uuid4().hex
user_domain_id = uuid.uuid4().hex
# passcode is 6 digits
passcode = ''.join(str(random.randint(0, 9)) for x in range(6))
project_name = uuid.uuid4().hex
project_domain_id = uuid.uuid4().hex
p = self.create(username=username,
user_domain_id=user_domain_id,
project_name=project_name,
project_domain_id=project_domain_id,
passcode=passcode)
totp_method = p.auth_methods[0]
self.assertEqual(username, totp_method.username)
self.assertEqual(user_domain_id, totp_method.user_domain_id)
self.assertEqual(passcode, totp_method.passcode)
self.assertEqual(project_name, p.project_name)
self.assertEqual(project_domain_id, p.project_domain_id)
def test_without_user_domain(self):
self.assertRaises(exceptions.OptionError,
self.create,
username=uuid.uuid4().hex,
passcode=uuid.uuid4().hex)
def test_without_project_domain(self):
self.assertRaises(exceptions.OptionError,
self.create,
username=uuid.uuid4().hex,
passcode=uuid.uuid4().hex,
user_domain_id=uuid.uuid4().hex,
project_name=uuid.uuid4().hex)

View File

@ -0,0 +1,16 @@
---
features:
- >
[`blueprint totp-auth <https://blueprints.launchpad.net/keystone/+spec/totp-auth>`_]
Add an auth plugin to handle Time-Based One-Time Password (TOTP)
authentication via the ``totp`` method. This new plugin will accept the
following identity options:
- ``user-id``: user ID
- ``username``: username
- ``user-domain-id``: user's domain ID
- ``user-domain-name``: user's domain name
- ``passcode``: passcode generated by TOTP app or device
User is uniquely identified by either ``user-id`` or combination of
``username`` and ``user-domain-id`` or ``user-domain-name``.

View File

@ -49,6 +49,7 @@ keystoneauth1.plugin =
v3oidcaccesstoken = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAccessToken
v3oauth1 = keystoneauth1.extras.oauth1._loading:V3OAuth1
v3kerberos = keystoneauth1.extras.kerberos._loading:Kerberos
v3totp = keystoneauth1.loading._plugins.identity.v3:TOTP
[build_sphinx]
source-dir = doc/source