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.
|
against a V3 identity service using a username and password.
|
||||||
- :py:class:`~keystoneauth1.identity.v3.TokenMethod`: Authenticate against
|
- :py:class:`~keystoneauth1.identity.v3.TokenMethod`: Authenticate against
|
||||||
a V3 identity service using an existing token.
|
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
|
- :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`: Authenticate
|
||||||
against a V3 identity service using Kerberos.
|
against a V3 identity service using Kerberos.
|
||||||
|
|
||||||
@ -81,6 +83,8 @@ like the V2 plugins:
|
|||||||
only a :py:class:`~keystoneauth1.identity.v3.PasswordMethod`.
|
only a :py:class:`~keystoneauth1.identity.v3.PasswordMethod`.
|
||||||
- :py:class:`~keystoneauth1.identity.v3.Token`: Authenticate using only a
|
- :py:class:`~keystoneauth1.identity.v3.Token`: Authenticate using only a
|
||||||
:py:class:`~keystoneauth1.identity.v3.TokenMethod`.
|
: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
|
- :py:class:`~keystoneauth1.extras.kerberos.Kerberos`: Authenticate using
|
||||||
only a :py:class:`~keystoneauth1.extras.kerberos.KerberosMethod`.
|
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`
|
- v2token: :py:class:`keystoneauth1.identity.v2.Token`
|
||||||
- v3password: :py:class:`keystoneauth1.identity.v3.Password`
|
- v3password: :py:class:`keystoneauth1.identity.v3.Password`
|
||||||
- v3token: :py:class:`keystoneauth1.identity.v3.Token`
|
- v3token: :py:class:`keystoneauth1.identity.v3.Token`
|
||||||
|
- v3totp: :py:class:`keystoneauth1.identity.v3.TOTP`
|
||||||
|
|
||||||
|
|
||||||
Creating Authentication Plugins
|
Creating Authentication Plugins
|
||||||
|
@ -46,6 +46,9 @@ V3OidcAuthorizationCode = oidc.OidcAuthorizationCode
|
|||||||
V3OidcAccessToken = oidc.OidcAccessToken
|
V3OidcAccessToken = oidc.OidcAccessToken
|
||||||
"""See :class:`keystoneauth1.identity.v3.oidc.OidcAccessToken`"""
|
"""See :class:`keystoneauth1.identity.v3.oidc.OidcAccessToken`"""
|
||||||
|
|
||||||
|
V3TOTP = v3.TOTP
|
||||||
|
"""See :class:`keystoneauth1.identity.v3.TOTP`"""
|
||||||
|
|
||||||
__all__ = ('BaseIdentityPlugin',
|
__all__ = ('BaseIdentityPlugin',
|
||||||
'Password',
|
'Password',
|
||||||
'Token',
|
'Token',
|
||||||
@ -55,4 +58,5 @@ __all__ = ('BaseIdentityPlugin',
|
|||||||
'V3Token',
|
'V3Token',
|
||||||
'V3OidcPassword',
|
'V3OidcPassword',
|
||||||
'V3OidcAuthorizationCode',
|
'V3OidcAuthorizationCode',
|
||||||
'V3OidcAccessToken')
|
'V3OidcAccessToken',
|
||||||
|
'V3TOTP')
|
||||||
|
@ -13,9 +13,10 @@
|
|||||||
from keystoneauth1.identity.v3.base import * # noqa
|
from keystoneauth1.identity.v3.base import * # noqa
|
||||||
from keystoneauth1.identity.v3.federation import * # noqa
|
from keystoneauth1.identity.v3.federation import * # noqa
|
||||||
from keystoneauth1.identity.v3.k2k 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.password import * # noqa
|
||||||
from keystoneauth1.identity.v3.token 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',
|
__all__ = ('Auth',
|
||||||
@ -34,4 +35,7 @@ __all__ = ('Auth',
|
|||||||
'TokenMethod',
|
'TokenMethod',
|
||||||
|
|
||||||
'OidcAuthorizationCode',
|
'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
|
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):
|
class Password(loading.BaseV3Loader):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -23,27 +44,16 @@ class Password(loading.BaseV3Loader):
|
|||||||
|
|
||||||
def get_options(self):
|
def get_options(self):
|
||||||
options = super(Password, self).get_options()
|
options = super(Password, self).get_options()
|
||||||
|
_add_common_identity_options(options)
|
||||||
|
|
||||||
options.extend([
|
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"),
|
loading.Opt('password', secret=True, help="User's password"),
|
||||||
])
|
])
|
||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
def load_from_options(self, **kwargs):
|
def load_from_options(self, **kwargs):
|
||||||
if (kwargs.get('username') and
|
_assert_identity_options(kwargs)
|
||||||
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)
|
|
||||||
|
|
||||||
return super(Password, self).load_from_options(**kwargs)
|
return super(Password, self).load_from_options(**kwargs)
|
||||||
|
|
||||||
@ -139,3 +149,25 @@ class OpenIDConnectAccessToken(loading.BaseFederationLoader):
|
|||||||
help='OAuth 2.0 Access Token'),
|
help='OAuth 2.0 Access Token'),
|
||||||
])
|
])
|
||||||
return options
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import random
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from keystoneauth1 import exceptions
|
from keystoneauth1 import exceptions
|
||||||
@ -64,3 +65,53 @@ class V3PasswordTests(utils.TestCase):
|
|||||||
password=uuid.uuid4().hex,
|
password=uuid.uuid4().hex,
|
||||||
user_domain_id=uuid.uuid4().hex,
|
user_domain_id=uuid.uuid4().hex,
|
||||||
project_name=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
|
v3oidcaccesstoken = keystoneauth1.loading._plugins.identity.v3:OpenIDConnectAccessToken
|
||||||
v3oauth1 = keystoneauth1.extras.oauth1._loading:V3OAuth1
|
v3oauth1 = keystoneauth1.extras.oauth1._loading:V3OAuth1
|
||||||
v3kerberos = keystoneauth1.extras.kerberos._loading:Kerberos
|
v3kerberos = keystoneauth1.extras.kerberos._loading:Kerberos
|
||||||
|
v3totp = keystoneauth1.loading._plugins.identity.v3:TOTP
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
source-dir = doc/source
|
source-dir = doc/source
|
||||||
|
Loading…
Reference in New Issue
Block a user