Support TOTP auth plugin
Add support for time-based one-time password (TOTP) authentication. Change-Id: I004677ac7f0e2fb8c059ad14868e661e8ee4c1f9
This commit is contained in:
parent
fc95d25544
commit
9e29e6e9c5
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
81
keystoneauth1/identity/v3/totp.py
Normal file
81
keystoneauth1/identity/v3/totp.py
Normal 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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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``.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user