storyboard/storyboard/api/auth/openid_client.py

189 lines
7.2 KiB
Python

# Copyright (c) 2014 Mirantis Inc.
#
# 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 re
from oslo_config import cfg
from oslo_log import log
import requests
import six
from six.moves.urllib.parse import urlparse
from storyboard.api.auth import ErrorMessages as e_msg
from storyboard.api.auth import utils
from storyboard.common.exception import AccessDenied
from storyboard.common.exception import InvalidClient
from storyboard.common.exception import InvalidRequest
from storyboard.common.exception import InvalidScope
from storyboard.common.exception import UnauthorizedClient
from storyboard.common.exception import UnsupportedResponseType
LOG = log.getLogger(__name__)
CONF = cfg.CONF
class OpenIdClient(object):
def send_openid_redirect(self, request, response):
# Extract needed parameters
redirect_uri = request.params.get("redirect_uri")
response_type = request.params.get("response_type")
client_id = request.params.get("client_id")
scope = request.params.get("scope")
# Sanity Check: Redirect URI
if not redirect_uri:
raise InvalidRequest(message=e_msg.NO_REDIRECT_URI)
parts = urlparse(redirect_uri)
if not parts.scheme or not parts.path:
raise InvalidRequest(message=e_msg.INVALID_REDIRECT_URI)
# Sanity Check: response_type
if not response_type:
raise UnsupportedResponseType(redirect_uri=redirect_uri,
message=e_msg.NO_RESPONSE_TYPE)
if response_type != 'code':
raise UnsupportedResponseType(redirect_uri=redirect_uri,
message=e_msg.INVALID_RESPONSE_TYPE)
# Sanity Check: client_id
if not client_id:
raise InvalidClient(redirect_uri=redirect_uri,
message=e_msg.NO_CLIENT_ID)
oauth_client_is_invalid = True
for valid_oauth_client in CONF.oauth.valid_oauth_clients:
if ((valid_oauth_client == client_id)
or (valid_oauth_client.startswith('^')
and re.match(valid_oauth_client, client_id))):
oauth_client_is_invalid = False
break
if oauth_client_is_invalid:
raise UnauthorizedClient(redirect_uri=redirect_uri,
message=e_msg.INVALID_CLIENT_ID)
# Sanity Check: scope
if not scope:
raise InvalidScope(redirect_uri=redirect_uri,
message=e_msg.NO_SCOPE)
# TODO(krotscheck): Defer scope check to ACL once available.
if scope != 'user':
raise InvalidScope(redirect_uri=redirect_uri,
message=e_msg.INVALID_SCOPE)
redirect_location = CONF.oauth.openid_url
response.status_code = 303
return_params = {
"scope": six.text_type(scope),
"state": six.text_type(request.params.get("state")),
"client_id": six.text_type(client_id),
"response_type": six.text_type(response_type),
"sb_redirect_uri": six.text_type(redirect_uri)
}
# TODO(krotscheck): URI base should be fully inferred from the request.
# assuming that the API is hosted at /api isn't good.
return_to_url = "%s/api/v1/openid/authorize_return?%s" % (
request.host_url,
utils.join_params(return_params, encode=True)
)
response.status_code = 303
openid_params = {
"openid.ns": "http://specs.openid.net/auth/2.0",
"openid.mode": "checkid_setup",
"openid.claimed_id": "http://specs.openid.net/auth/2.0/"
"identifier_select",
"openid.identity": "http://specs.openid.net/auth/2.0/"
"identifier_select",
"openid.realm": request.host_url,
"openid.return_to": return_to_url,
"openid.ns.sreg": "http://openid.net/sreg/1.0",
"openid.sreg.required": "fullname,email",
"openid.ns.ext2": "http://openid.net/srv/ax/1.0",
"openid.ext2.mode": "fetch_request",
"openid.ext2.type.FirstName": "http://schema.openid.net/"
"namePerson/first",
"openid.ext2.type.LastName": "http://schema.openid.net/"
"namePerson/last",
"openid.ext2.type.Email": "http://schema.openid.net/contact/email",
"openid.ext2.required": "FirstName,LastName,Email"
}
joined_params = utils.join_params(openid_params)
redirect_location = redirect_location + '?' + joined_params
response.headers["Location"] = redirect_location
return response
def verify_openid(self, request):
verify_params = dict(request.params.copy())
verify_params["openid.mode"] = "check_authentication"
redirect_uri = request.params['sb_redirect_uri'] or None
verify_response = requests.post(CONF.oauth.openid_url,
data=verify_params)
content = verify_response.content
if isinstance(content, bytes):
content = content.decode('utf-8')
verify_data_tokens = content.split()
verify_dict = dict((token.split(":")[0], token.split(":")[1])
for token in verify_data_tokens)
if (verify_response.status_code / 100 != 2
or verify_dict['is_valid'] != 'true'):
raise AccessDenied(redirect_uri=redirect_uri,
message=e_msg.OPEN_ID_TOKEN_INVALID)
# Is the data we've received within our required parameters?
required_parameters = {
'openid.sreg.email': e_msg.INVALID_NO_EMAIL,
'openid.sreg.fullname': e_msg.INVALID_NO_NAME,
}
for name, error in six.iteritems(required_parameters):
if name not in verify_params or not verify_params[name]:
raise InvalidRequest(redirect_uri=redirect_uri,
message=error)
return True
def create_association(self, op_location):
# Let's skip it for MVP at least
query_dict = {
"openid.ns": "http://specs.openid.net/auth/2.0",
"openid.mode": "associate",
"openid.assoc_type": "HMAC-SHA256",
"openid.session_type": "no-encryption"
}
assoc_data = requests.post(op_location, data=query_dict).content
data_tokens = assoc_data.split()
data_dict = dict((token.split(":")[0], token.split(":")[1])
for token in data_tokens)
return data_dict["assoc_handle"]
client = OpenIdClient()