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:
parent
8d69194876
commit
d552a9a1b7
@ -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
|
||||
------------------------------------
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user