55d55bcdf1
Abstract authentication function so plugins for other authentication backends can be implemented in cases where keystone is not used. Currently, mistral is hard coded to support keystone and keycloak. Change-Id: If6ff35e91c3d35c2741332c7e739bb92b1234c54 Implements: blueprint mistral-abstract-auth
170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
# Copyright 2016 - Nokia Networks
|
|
#
|
|
# 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 logging
|
|
import pprint
|
|
import requests
|
|
|
|
from mistralclient import auth
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class KeycloakAuthHandler(auth.AuthHandler):
|
|
|
|
def authenticate(self, req):
|
|
"""Performs authentication using Keycloak OpenID Protocol.
|
|
|
|
:param req: Request dict containing list of parameters required
|
|
for Keycloak authentication.
|
|
|
|
auth_url: Base authentication url of KeyCloak server (e.g.
|
|
"https://my.keycloak:8443/auth"
|
|
client_id: Client ID (according to OpenID Connect protocol).
|
|
client_secret: Client secret (according to OpenID Connect
|
|
protocol).
|
|
realm_name: KeyCloak realm name.
|
|
username: User name (Optional, if None then access_token must be
|
|
provided).
|
|
password: Password (Optional).
|
|
access_token: Access token. If passed, username and password are
|
|
not used and this method just validates the token and refreshes
|
|
it if needed (Optional, if None then username must be
|
|
provided).
|
|
cacert: SSL certificate file (Optional).
|
|
insecure: If True, SSL certificate is not verified (Optional).
|
|
|
|
"""
|
|
if not isinstance(req, dict):
|
|
raise TypeError('The input "req" is not typeof dict.')
|
|
|
|
auth_url = req.get('auth_url')
|
|
client_id = req.get('client_id')
|
|
client_secret = req.get('client_secret')
|
|
realm_name = req.get('realm_name')
|
|
username = req.get('username')
|
|
password = req.get('password')
|
|
access_token = req.get('access_token')
|
|
cacert = req.get('cacert')
|
|
insecure = req.get('insecure', False)
|
|
|
|
if not auth_url:
|
|
raise ValueError('Base authentication url is not provided.')
|
|
|
|
if not client_id:
|
|
raise ValueError('Client ID is not provided.')
|
|
|
|
if not client_secret:
|
|
raise ValueError('Client secret is not provided.')
|
|
|
|
if not realm_name:
|
|
raise ValueError('Project(realm) name is not provided.')
|
|
|
|
if username and access_token:
|
|
raise ValueError(
|
|
"User name and access token can't be "
|
|
"provided at the same time."
|
|
)
|
|
|
|
if not username and not access_token:
|
|
raise ValueError(
|
|
'Either user name or access token must be provided.'
|
|
)
|
|
|
|
if access_token:
|
|
response = self._authenticate_with_token(
|
|
auth_url,
|
|
client_id,
|
|
client_secret,
|
|
access_token,
|
|
cacert,
|
|
insecure
|
|
)
|
|
else:
|
|
response = self._authenticate_with_password(
|
|
auth_url,
|
|
client_id,
|
|
client_secret,
|
|
realm_name,
|
|
username,
|
|
password,
|
|
cacert,
|
|
insecure
|
|
)
|
|
|
|
response['project_id'] = realm_name
|
|
|
|
return response
|
|
|
|
def _authenticate_with_token(auth_url, client_id, client_secret,
|
|
auth_token, cacert=None, insecure=None):
|
|
# TODO(rakhmerov): Implement.
|
|
raise NotImplementedError
|
|
|
|
def _authenticate_with_password(auth_url, client_id, client_secret,
|
|
realm_name, username, password,
|
|
cacert=None, insecure=None):
|
|
access_token_endpoint = (
|
|
"%s/realms/%s/protocol/openid-connect/token" %
|
|
(auth_url, realm_name)
|
|
)
|
|
|
|
client_auth = (client_id, client_secret)
|
|
|
|
body = {
|
|
'grant_type': 'password',
|
|
'username': username,
|
|
'password': password,
|
|
'scope': 'profile'
|
|
}
|
|
|
|
resp = requests.post(
|
|
access_token_endpoint,
|
|
auth=client_auth,
|
|
data=body,
|
|
verify=not insecure
|
|
)
|
|
|
|
try:
|
|
resp.raise_for_status()
|
|
except Exception as e:
|
|
raise Exception("Failed to get access token:\n %s" % str(e))
|
|
|
|
LOG.debug(
|
|
"HTTP response from OIDC provider: %s" %
|
|
pprint.pformat(resp.json())
|
|
)
|
|
|
|
return resp.json()['access_token']
|
|
|
|
|
|
# An example of using KeyCloak OpenID authentication.
|
|
if __name__ == '__main__':
|
|
print("Using username/password to get access token from KeyCloak...")
|
|
|
|
auth_handler = KeycloakAuthHandler()
|
|
|
|
a_token = auth_handler.authenticate(
|
|
"https://my.keycloak:8443/auth",
|
|
client_id="mistral_client",
|
|
client_secret="4a080907-921b-409a-b793-c431609c3a47",
|
|
realm_name="mistral",
|
|
username="user",
|
|
password="secret",
|
|
insecure=True
|
|
)
|
|
|
|
print("Access token: %s" % a_token)
|