diff options
Diffstat (limited to 'keystone/api/os_oauth2.py')
-rw-r--r-- | keystone/api/os_oauth2.py | 279 |
1 files changed, 241 insertions, 38 deletions
diff --git a/keystone/api/os_oauth2.py b/keystone/api/os_oauth2.py index 43e3dfcbe..81f3dbd3d 100644 --- a/keystone/api/os_oauth2.py +++ b/keystone/api/os_oauth2.py @@ -16,16 +16,22 @@ import flask from flask import make_response import http.client from oslo_log import log +from oslo_serialization import jsonutils from keystone.api._shared import authentication from keystone.api._shared import json_home_relations +from keystone.common import provider_api +from keystone.common import utils from keystone.conf import CONF from keystone import exception +from keystone.federation import utils as federation_utils from keystone.i18n import _ from keystone.server import flask as ks_flask LOG = log.getLogger(__name__) +PROVIDERS = provider_api.ProviderAPIs + _build_resource_relation = json_home_relations.os_oauth2_resource_rel_func @@ -69,44 +75,6 @@ class AccessTokenResource(ks_flask.ResourceBase): POST /v3/OS-OAUTH2/token """ - client_auth = flask.request.authorization - if not client_auth: - error = exception.OAuth2InvalidClient( - int(http.client.UNAUTHORIZED), - http.client.responses[http.client.UNAUTHORIZED], - _('OAuth2.0 client authorization is required.')) - LOG.info('Get OAuth2.0 Access Token API: ' - 'field \'authorization\' is not found in HTTP Headers.') - raise error - if client_auth.type != 'basic': - error = exception.OAuth2InvalidClient( - int(http.client.UNAUTHORIZED), - http.client.responses[http.client.UNAUTHORIZED], - _('OAuth2.0 client authorization type %s is not supported.') - % client_auth.type) - LOG.info('Get OAuth2.0 Access Token API: ' - f'{error.message_format}') - raise error - client_id = client_auth.username - client_secret = client_auth.password - - if not client_id: - error = exception.OAuth2InvalidClient( - int(http.client.UNAUTHORIZED), - http.client.responses[http.client.UNAUTHORIZED], - _('OAuth2.0 client authorization is invalid.')) - LOG.info('Get OAuth2.0 Access Token API: ' - 'client_id is not found in authorization.') - raise error - if not client_secret: - error = exception.OAuth2InvalidClient( - int(http.client.UNAUTHORIZED), - http.client.responses[http.client.UNAUTHORIZED], - _('OAuth2.0 client authorization is invalid.')) - LOG.info('Get OAuth2.0 Access Token API: ' - 'client_secret is not found in authorization.') - raise error - grant_type = flask.request.form.get('grant_type') if grant_type is None: error = exception.OAuth2InvalidRequest( @@ -125,6 +93,45 @@ class AccessTokenResource(ks_flask.ResourceBase): LOG.info('Get OAuth2.0 Access Token API: ' f'{error.message_format}') raise error + + auth_method = '' + client_id = flask.request.form.get('client_id') + client_secret = flask.request.form.get('client_secret') + client_cert = flask.request.environ.get("SSL_CLIENT_CERT") + client_auth = flask.request.authorization + if not client_cert and client_auth and client_auth.type == 'basic': + client_id = client_auth.username + client_secret = client_auth.password + + if not client_id: + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: ' + 'failed to get a client_id from the request.') + raise error + if client_cert: + auth_method = 'tls_client_auth' + elif client_secret: + auth_method = 'client_secret_basic' + + if auth_method in CONF.oauth2.oauth2_authn_methods: + if auth_method == 'tls_client_auth': + return self._tls_client_auth(client_id, client_cert) + if auth_method == 'client_secret_basic': + return self._client_secret_basic(client_id, client_secret) + + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: ' + 'failed to get client credentials from the request.') + raise error + + def _client_secret_basic(self, client_id, client_secret): + """Get an OAuth2.0 basic Access Token.""" auth_data = { 'identity': { 'methods': ['application_credential'], @@ -168,6 +175,202 @@ class AccessTokenResource(ks_flask.ResourceBase): resp.status = '200 OK' return resp + def _check_mapped_properties(self, cert_dn, user, user_domain): + mapping_id = CONF.oauth2.get('oauth2_cert_dn_mapping_id') + try: + mapping = PROVIDERS.federation_api.get_mapping(mapping_id) + except exception.MappingNotFound: + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: ' + 'mapping id %s is not found. ', + mapping_id) + raise error + + rule_processor = federation_utils.RuleProcessor( + mapping.get('id'), mapping.get('rules')) + try: + mapped_properties = rule_processor.process(cert_dn) + except exception.Error as error: + LOG.exception(error) + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: ' + 'mapping rule process failed. ' + 'mapping_id: %s, rules: %s, data: %s.', + mapping_id, mapping.get('rules'), + jsonutils.dumps(cert_dn)) + raise error + except Exception as error: + LOG.exception(error) + error = exception.OAuth2OtherError( + int(http.client.INTERNAL_SERVER_ERROR), + http.client.responses[http.client.INTERNAL_SERVER_ERROR], + str(error)) + LOG.info('Get OAuth2.0 Access Token API: ' + 'mapping rule process failed. ' + 'mapping_id: %s, rules: %s, data: %s.', + mapping_id, mapping.get('rules'), + jsonutils.dumps(cert_dn)) + raise error + + mapping_user = mapped_properties.get('user', {}) + mapping_user_name = mapping_user.get('name') + mapping_user_id = mapping_user.get('id') + mapping_user_email = mapping_user.get('email') + mapping_domain = mapping_user.get('domain', {}) + mapping_user_domain_id = mapping_domain.get('id') + mapping_user_domain_name = mapping_domain.get('name') + if mapping_user_name and mapping_user_name != user.get('name'): + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: %s check failed. ' + 'DN value: %s, DB value: %s.', + 'user name', mapping_user_name, user.get('name')) + raise error + if mapping_user_id and mapping_user_id != user.get('id'): + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: %s check failed. ' + 'DN value: %s, DB value: %s.', + 'user id', mapping_user_id, user.get('id')) + raise error + if mapping_user_email and mapping_user_email != user.get('email'): + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: %s check failed. ' + 'DN value: %s, DB value: %s.', + 'user email', mapping_user_email, user.get('email')) + raise error + if (mapping_user_domain_id and + mapping_user_domain_id != user_domain.get('id')): + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: %s check failed. ' + 'DN value: %s, DB value: %s.', + 'user domain id', mapping_user_domain_id, + user_domain.get('id')) + raise error + if (mapping_user_domain_name and + mapping_user_domain_name != user_domain.get('name')): + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: %s check failed. ' + 'DN value: %s, DB value: %s.', + 'user domain name', mapping_user_domain_name, + user_domain.get('name')) + raise error + + def _tls_client_auth(self, client_id, client_cert): + """Get an OAuth2.0 certificate-bound Access Token.""" + try: + cert_subject_dn = utils.get_certificate_subject_dn(client_cert) + except exception.ValidationError: + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: ' + 'failed to get the subject DN from the certificate.') + raise error + try: + cert_issuer_dn = utils.get_certificate_issuer_dn(client_cert) + except exception.ValidationError: + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: ' + 'failed to get the issuer DN from the certificate.') + raise error + client_cert_dn = {} + for key in cert_subject_dn: + client_cert_dn['SSL_CLIENT_SUBJECT_DN_%s' % + key.upper()] = cert_subject_dn.get(key) + for key in cert_issuer_dn: + client_cert_dn['SSL_CLIENT_ISSUER_DN_%s' % + key.upper()] = cert_issuer_dn.get(key) + + try: + user = PROVIDERS.identity_api.get_user(client_id) + except exception.UserNotFound: + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: ' + 'the user does not exist. user id: %s.', + client_id) + raise error + project_id = user.get('default_project_id') + if not project_id: + error = exception.OAuth2InvalidClient( + int(http.client.UNAUTHORIZED), + http.client.responses[http.client.UNAUTHORIZED], + _('Client authentication failed.')) + LOG.info('Get OAuth2.0 Access Token API: ' + 'the user does not have default project. user id: %s.', + client_id) + raise error + + user_domain = PROVIDERS.resource_api.get_domain( + user.get('domain_id')) + self._check_mapped_properties(client_cert_dn, user, user_domain) + thumbprint = utils.get_certificate_thumbprint(client_cert) + LOG.debug(f'The mTLS certificate thumbprint: {thumbprint}') + try: + token = PROVIDERS.token_provider_api.issue_token( + user_id=client_id, + method_names=['oauth2_credential'], + project_id=project_id, + thumbprint=thumbprint + ) + except exception.Error as error: + if error.code == 401: + error = exception.OAuth2InvalidClient( + error.code, error.title, + str(error)) + elif error.code == 400: + error = exception.OAuth2InvalidRequest( + error.code, error.title, + str(error)) + else: + error = exception.OAuth2OtherError( + error.code, error.title, + 'An unknown error occurred and failed to get an OAuth2.0 ' + 'access token.') + LOG.exception(error) + raise error + except Exception as error: + error = exception.OAuth2OtherError( + int(http.client.INTERNAL_SERVER_ERROR), + http.client.responses[http.client.INTERNAL_SERVER_ERROR], + str(error)) + LOG.exception(error) + raise error + + resp = make_response({ + 'access_token': token.id, + 'token_type': 'Bearer', + 'expires_in': CONF.token.expiration + }) + resp.status = '200 OK' + return resp + class OSAuth2API(ks_flask.APIBase): _name = 'OS-OAUTH2' |