Add OTP to v3OIDCpassword plugin

Problem description
===================
When using OpenStack in federated environments, to enhance user's
authentication, some Identity Providers allow them (users) to use an OTP
(One Time Password) method. This requires users to send a generated code
together with his/her username and password any time he/she is
authenticating in the Identity Provider. It can be applied to both Id
and Access tokens authentication flows.

If the user is using the OTP, he/she will not be able to use the
OpenStack CLI, because the user needs to send an OTP value to the
Identity Provider together with the username and password.

Proposal
========
To allow users that have OTP enabled in their Identity Provider to use
the OpenStack CLI with their credentials, we propose to add a new
optional configuration in the v3OIDCpassword plugin to allow users to
enter their OTP Code.

The new configuration will contain the key for the OTP code that the
user's Identity Provider expects in its Rest API. As the OTP is not
defined by the OpenID Connect protocol (it defines just "credentials"
that is a set of attributes that authorize the user, like username,
password, OTP, etc); we defined the property as the expected key to the
OTP in the IdP's Rest API. For example, in KeyCloak, the OTP key in its
Access Token API is called "totp". Therefore, if the user configures
the new property, then the CLI will request the OTP code when necessary.

We do not create a property to set the OTP code specifically,
because the OTP changes every time, so is not so practical to users to
reconfiguring the property every time the OTP code expires.

Change-Id: Ibd27470c9250000d24cf085ccf6b0c31c782c21e
This commit is contained in:
pedro 2019-11-26 17:24:14 -03:00 committed by Pedro Henrique
parent 8d69194876
commit d552a9a1b7
5 changed files with 102 additions and 3 deletions

View File

@ -300,6 +300,33 @@ session on the keystone acting as a Service Provider, for example:
k2ksession = session.Session(auth=k2kauth)
The `OpenIDConnectPassword` plugin also supports OTP. This option is required
in cases when the Identity Provider requires more than a password to
authenticate the user. As the OTP usually is a short-lived code that
continually changes, then, when this option is active, the user will be
requested to input the OTP code when executing the authentication process.
To enable this option, the user will need to export the environment variable
"OS_IDP_OTP_KEY" with the OTP key used by the Identity Provider's
authentication API.
E.g.: If the Identity Provider's authentication API requires some JSON like:
.. code-block:: json
{
"username": "user1",
"password": "passwd",
"totp": "763907"
}
Then, you will use the "totp" value in your "OS_IDP_OTP_KEY", something like
"export OS_IDP_OTP_KEY=totp".
After the configuration of the "OS_IDP_OTP_KEY" environment variable,
every time that you will log in through the python openstack-client, a prompt
will be displayed requesting to you to input your OTP code.
Version Independent Identity Plugins
------------------------------------

View File

@ -222,8 +222,7 @@ class _OidcBase(federation.FederationBaseAuth, metaclass=abc.ABCMeta):
"OpenID-Connect authentication response from %s is %s",
access_token_endpoint, sanitized_response
)
access_token = response[self.access_token_type]
return access_token
return response[self.access_token_type]
def _get_keystone_token(self, session, access_token):
r"""Exchange an access token for a keystone token.
@ -319,7 +318,7 @@ class OidcPassword(_OidcBase):
access_token_endpoint=None,
discovery_endpoint=None,
access_token_type='access_token',
username=None, password=None,
username=None, password=None, idp_otp_key=None,
**kwargs):
"""The OpenID Password plugin expects the following.
@ -341,6 +340,7 @@ class OidcPassword(_OidcBase):
**kwargs)
self.username = username
self.password = password
self.idp_otp_key = idp_otp_key
def get_payload(self, session):
"""Get an authorization grant for the "password" grant type.
@ -355,8 +355,41 @@ class OidcPassword(_OidcBase):
'password': self.password,
'scope': self.scope,
'client_id': self.client_id}
self.manage_otp_from_session_or_request_to_the_user(payload, session)
return payload
def manage_otp_from_session_or_request_to_the_user(self, payload, session):
"""Get the OTP code from the session or else request to the user.
When the OS_IDP_OTP_KEY environment variable is set, this method will
verify if there is an OTP value in the current session, if it exists,
we use it (the OTP from session) to send to the Identity Provider when
retrieving the access token. If there is no OTP in the current session,
we ask the user to enter it (the OTP), and we add it to the session to
execute the authentication flow.
The OTP is being stored in the session because in some flows, the CLI
is doing the authentication process two times, so saving the OTP
in the session, allow us to use the same OTP in a short time interval,
avoiding to request it to the user twice in a row.
:param payload:
:param session:
:return:
"""
if not self.idp_otp_key:
return
otp_from_session = getattr(session, 'otp', None)
if otp_from_session:
payload[self.idp_otp_key] = otp_from_session
else:
payload[self.idp_otp_key] = input(
"Please, enter the generated OTP code: ")
setattr(session, 'otp', payload[self.idp_otp_key])
class OidcClientCredentials(_OidcBase):
"""Implementation for OpenID Connect Client Credentials."""

View File

@ -151,6 +151,10 @@ class OpenIDConnectPassword(_OpenIDConnectBase):
loading.Opt('username', help='Username', required=True),
loading.Opt('password', secret=True,
help='Password', required=True),
loading.Opt('idp_otp_key',
help='A key to be used in the Identity Provider access'
' token endpoint to pass the OTP value. '
'E.g. totp'),
])
return options

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
import urllib
import uuid
import warnings
@ -22,6 +23,7 @@ from keystoneauth1.tests.unit import utils
KEYSTONE_TOKEN_VALUE = uuid.uuid4().hex
BUILTIN_INPUT = "builtins.input"
class BaseOIDCTests(object):
@ -322,6 +324,32 @@ class OIDCPasswordTests(BaseOIDCTests, utils.TestCase):
response = self.plugin.get_unscoped_auth_ref(self.session)
self.assertEqual(KEYSTONE_TOKEN_VALUE, response.auth_token)
@mock.patch(BUILTIN_INPUT)
def test_otp_while_generating_the_access_token(self, mock_input):
"""Test the configured otp in the access token flow."""
self.plugin.idp_otp_key = "totp"
otp = 123
new_otp = 111
access_token = \
oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP["access_token"]
mock_input.return_value = otp
self.requests_mock.post(
self.ACCESS_TOKEN_ENDPOINT,
json=oidc_fixtures.ACCESS_TOKEN_VIA_PASSWORD_RESP)
# Verify if the OTP is set in the session and if the request is OK
payload = self.plugin.get_payload(self.session)
resp = self.plugin._get_access_token(self.session, payload)
self.assertEqual(resp, access_token)
self.assertEqual(self.session.otp, otp)
# Verify if the OTP is obtained from the session.
mock_input.return_value = new_otp
payload = self.plugin.get_payload(self.session)
self.assertEqual(payload[self.plugin.idp_otp_key], otp)
class OIDCAuthorizationGrantTests(BaseOIDCTests, utils.TestCase):
def setUp(self):

View File

@ -0,0 +1,7 @@
---
features:
- |
Add support to OTP in the v3OIDCpassword plugin, to enable this feature,
the user must configure the environment variable OS_IDP_OTP_KEY with the name
of the OTP key that the Identity Provider expects while generating the
access token. E.g. OS_IDP_OTP_KEY=totp