Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework required_scopes checking #1474

Merged
merged 14 commits into from
Mar 21, 2022
8 changes: 3 additions & 5 deletions connexion/operations/secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,31 +75,29 @@ def security_decorator(self):
return self._api.security_handler_factory.security_passthrough

auth_funcs = []
required_scopes = None
for security_req in self.security:
if not security_req:
auth_funcs.append(self._api.security_handler_factory.verify_none())
continue

sec_req_funcs = {}
oauth = False
for scheme_name, scopes in security_req.items():
for scheme_name, required_scopes in security_req.items():
security_scheme = self.security_schemes[scheme_name]

if security_scheme['type'] == 'oauth2':
if oauth:
logger.warning("... multiple OAuth2 security schemes in AND fashion not supported", extra=vars(self))
break
oauth = True
required_scopes = scopes
token_info_func = self._api.security_handler_factory.get_tokeninfo_func(security_scheme)
scope_validate_func = self._api.security_handler_factory.get_scope_validate_func(security_scheme)
if not token_info_func:
logger.warning("... x-tokenInfoFunc missing", extra=vars(self))
break

sec_req_funcs[scheme_name] = self._api.security_handler_factory.verify_oauth(
token_info_func, scope_validate_func)
token_info_func, scope_validate_func, required_scopes)

# Swagger 2.0
elif security_scheme['type'] == 'basic':
Expand Down Expand Up @@ -159,7 +157,7 @@ def security_decorator(self):
else:
auth_funcs.append(self._api.security_handler_factory.verify_multiple_schemes(sec_req_funcs))

return functools.partial(self._api.security_handler_factory.verify_security, auth_funcs, required_scopes)
return functools.partial(self._api.security_handler_factory.verify_security, auth_funcs)

def get_mimetype(self):
return DEFAULT_MIMETYPE
Expand Down
4 changes: 2 additions & 2 deletions connexion/security/async_security_handler_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ async def wrapper(request, token, required_scopes):
return wrapper

@classmethod
def verify_security(cls, auth_funcs, required_scopes, function):
def verify_security(cls, auth_funcs, function):
Ruwann marked this conversation as resolved.
Show resolved Hide resolved
@functools.wraps(function)
async def wrapper(request):
token_info = cls.no_value
for func in auth_funcs:
token_info = func(request, required_scopes)
token_info = func(request)
while asyncio.iscoroutine(token_info):
token_info = await token_info
if token_info is not cls.no_value:
Expand Down
41 changes: 24 additions & 17 deletions connexion/security/security_handler_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,10 @@ def get_auth_header_value(request):
raise OAuthProblem(description='Invalid authorization header')
return auth_type.lower(), value

def verify_oauth(self, token_info_func, scope_validate_func):
def verify_oauth(self, token_info_func, scope_validate_func, required_scopes):
check_oauth_func = self.check_oauth_func(token_info_func, scope_validate_func)

def wrapper(request, required_scopes):
def wrapper(request):
auth_type, token = self.get_auth_header_value(request)
if auth_type != 'bearer':
return self.no_value
Expand All @@ -188,7 +188,7 @@ def wrapper(request, required_scopes):
def verify_basic(self, basic_info_func):
check_basic_info_func = self.check_basic_auth(basic_info_func)

def wrapper(request, required_scopes):
def wrapper(request):
auth_type, user_pass = self.get_auth_header_value(request)
if auth_type != 'basic':
return self.no_value
Expand All @@ -198,7 +198,7 @@ def wrapper(request, required_scopes):
except Exception:
raise OAuthProblem(description='Invalid authorization header')

return check_basic_info_func(request, username, password, required_scopes=required_scopes)
return check_basic_info_func(request, username, password)

return wrapper

Expand All @@ -221,7 +221,7 @@ def get_cookie_value(cookies, name):
def verify_api_key(self, api_key_info_func, loc, name):
check_api_key_func = self.check_api_key(api_key_info_func)

def wrapper(request, required_scopes):
def wrapper(request):

def _immutable_pop(_dict, key):
"""
Expand Down Expand Up @@ -252,7 +252,7 @@ def _immutable_pop(_dict, key):
if api_key is None:
return self.no_value

return check_api_key_func(request, api_key, required_scopes=required_scopes)
return check_api_key_func(request, api_key)

return wrapper

Expand All @@ -263,11 +263,11 @@ def verify_bearer(self, token_info_func):
"""
check_bearer_func = self.check_bearer_token(token_info_func)

def wrapper(request, required_scopes):
def wrapper(request):
auth_type, token = self.get_auth_header_value(request)
if auth_type != 'bearer':
return self.no_value
return check_bearer_func(request, token, required_scopes=required_scopes)
return check_bearer_func(request, token)

return wrapper

Expand All @@ -281,10 +281,10 @@ def verify_multiple_schemes(self, schemes):
:rtype: types.FunctionType
"""

def wrapper(request, required_scopes):
def wrapper(request):
token_info = {}
for scheme_name, func in schemes.items():
result = func(request, required_scopes)
result = func(request)
if result is self.no_value:
return self.no_value
token_info[scheme_name] = result
Expand All @@ -299,7 +299,7 @@ def verify_none():
:rtype: types.FunctionType
"""

def wrapper(request, required_scopes):
def wrapper(request):
return {}

return wrapper
Expand Down Expand Up @@ -362,18 +362,25 @@ def wrapper(request, token, required_scopes):
return wrapper

@classmethod
def verify_security(cls, auth_funcs, required_scopes, function):
def verify_security(cls, auth_funcs, function):
@functools.wraps(function)
def wrapper(request):
token_info = cls.no_value
problem = None
for func in auth_funcs:
token_info = func(request, required_scopes)
if token_info is not cls.no_value:
break
try:
token_info = func(request)
if token_info is not cls.no_value:
break
except (OAuthProblem, OAuthResponseProblem, OAuthScopeProblem) as err:
problem = err
Ruwann marked this conversation as resolved.
Show resolved Hide resolved

if token_info is cls.no_value:
logger.info("... No auth provided. Aborting with 401.")
raise OAuthProblem(description='No authorization token provided')
if problem is not None:
raise problem
else:
logger.info("... No auth provided. Aborting with 401.")
raise OAuthProblem(description='No authorization token provided')

# Fallback to 'uid' for backward compatibility
request.context['user'] = token_info.get('sub', token_info.get('uid'))
Expand Down
6 changes: 1 addition & 5 deletions docs/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,7 @@ Basic Authentication
With Connexion, the API security definition **must** include a
``x-basicInfoFunc`` or set ``BASICINFO_FUNC`` env var. It uses the same
semantics as for ``x-tokenInfoFunc``, but the function accepts three
parameters: username, password and required_scopes. If the security declaration
of the operation also has an oauth security requirement, required_scopes is
taken from there, otherwise it's None. This allows authorizing individual
operations with `oauth scope`_ while using basic authentication for
authentication.
parameters: username, password and required_scopes.

You can find a `minimal Basic Auth example application`_ in Connexion's "examples" folder.

Expand Down
3 changes: 2 additions & 1 deletion tests/api/test_secure_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ def test_security(oauth_requests, secure_endpoint_app):
assert response.data == b'"Authenticated"\n'
headers = {"X-AUTH": "wrong-key"}
response = app_client.get('/v1.0/optional-auth', headers=headers) # type: flask.Response
assert response.status_code == 401
assert response.data == b'"Unauthenticated"\n'
assert response.status_code == 200


def test_checking_that_client_token_has_all_necessary_scopes(
Expand Down
44 changes: 22 additions & 22 deletions tests/decorators/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ def test_verify_oauth_missing_auth_header(security_handler_factory):
def somefunc(token):
return None

wrapped_func = security_handler_factory.verify_oauth(somefunc, security_handler_factory.validate_scope)
wrapped_func = security_handler_factory.verify_oauth(somefunc, security_handler_factory.validate_scope, ['admin'])

request = MagicMock()
request.headers = {}

assert wrapped_func(request, ['admin']) is security_handler_factory.no_value
assert wrapped_func(request) is security_handler_factory.no_value


def test_verify_oauth_scopes_remote(monkeypatch, security_handler_factory):
Expand All @@ -52,7 +52,7 @@ def get_tokeninfo_response(*args, **kwargs):
return tokeninfo_response

token_info_func = security_handler_factory.get_tokeninfo_func({'x-tokenInfoUrl': 'https://example.org/tokeninfo'})
wrapped_func = security_handler_factory.verify_oauth(token_info_func, security_handler_factory.validate_scope)
wrapped_func = security_handler_factory.verify_oauth(token_info_func, security_handler_factory.validate_scope, ['admin'])

request = MagicMock()
request.headers = {"Authorization": "Bearer 123"}
Expand All @@ -62,30 +62,30 @@ def get_tokeninfo_response(*args, **kwargs):
monkeypatch.setattr('connexion.security.flask_security_handler_factory.session', session)

with pytest.raises(OAuthScopeProblem, match="Provided token doesn't have the required scope"):
wrapped_func(request, ['admin'])
wrapped_func(request)

tokeninfo["scope"] += " admin"
assert wrapped_func(request, ['admin']) is not None
assert wrapped_func(request) is not None

tokeninfo["scope"] = ["foo", "bar"]
with pytest.raises(OAuthScopeProblem, match="Provided token doesn't have the required scope"):
wrapped_func(request, ['admin'])
wrapped_func(request)

tokeninfo["scope"].append("admin")
assert wrapped_func(request, ['admin']) is not None
assert wrapped_func(request) is not None


def test_verify_oauth_invalid_local_token_response_none(security_handler_factory):
def somefunc(token):
return None

wrapped_func = security_handler_factory.verify_oauth(somefunc, security_handler_factory.validate_scope)
wrapped_func = security_handler_factory.verify_oauth(somefunc, security_handler_factory.validate_scope, ['admin'])

request = MagicMock()
request.headers = {"Authorization": "Bearer 123"}

with pytest.raises(OAuthResponseProblem):
wrapped_func(request, ['admin'])
wrapped_func(request)


def test_verify_oauth_scopes_local(security_handler_factory):
Expand All @@ -94,23 +94,23 @@ def test_verify_oauth_scopes_local(security_handler_factory):
def token_info(token):
return tokeninfo

wrapped_func = security_handler_factory.verify_oauth(token_info, security_handler_factory.validate_scope)
wrapped_func = security_handler_factory.verify_oauth(token_info, security_handler_factory.validate_scope, ['admin'])

request = MagicMock()
request.headers = {"Authorization": "Bearer 123"}

with pytest.raises(OAuthScopeProblem, match="Provided token doesn't have the required scope"):
wrapped_func(request, ['admin'])
wrapped_func(request)

tokeninfo["scope"] += " admin"
assert wrapped_func(request, ['admin']) is not None
assert wrapped_func(request) is not None

tokeninfo["scope"] = ["foo", "bar"]
with pytest.raises(OAuthScopeProblem, match="Provided token doesn't have the required scope"):
wrapped_func(request, ['admin'])
wrapped_func(request)

tokeninfo["scope"].append("admin")
assert wrapped_func(request, ['admin']) is not None
assert wrapped_func(request) is not None


def test_verify_basic_missing_auth_header(security_handler_factory):
Expand All @@ -122,7 +122,7 @@ def somefunc(username, password, required_scopes=None):
request = MagicMock()
request.headers = {"Authorization": "Bearer 123"}

assert wrapped_func(request, ['admin']) is security_handler_factory.no_value
assert wrapped_func(request) is security_handler_factory.no_value


def test_verify_basic(security_handler_factory):
Expand All @@ -136,7 +136,7 @@ def basic_info(username, password, required_scopes=None):
request = MagicMock()
request.headers = {"Authorization": 'Basic Zm9vOmJhcg=='}

assert wrapped_func(request, ['admin']) is not None
assert wrapped_func(request) is not None


def test_verify_apikey_query(security_handler_factory):
Expand All @@ -150,7 +150,7 @@ def apikey_info(apikey, required_scopes=None):
request = MagicMock()
request.query = {"auth": 'foobar'}

assert wrapped_func(request, ['admin']) is not None
assert wrapped_func(request) is not None


def test_verify_apikey_header(security_handler_factory):
Expand All @@ -164,7 +164,7 @@ def apikey_info(apikey, required_scopes=None):
request = MagicMock()
request.headers = {"X-Auth": 'foobar'}

assert wrapped_func(request, ['admin']) is not None
assert wrapped_func(request) is not None


def test_multiple_schemes(security_handler_factory):
Expand All @@ -189,12 +189,12 @@ def apikey2_info(apikey, required_scopes=None):
request = MagicMock()
request.headers = {"X-Auth-1": 'foobar'}

assert wrapped_func(request, ['admin']) is security_handler_factory.no_value
assert wrapped_func(request) is security_handler_factory.no_value

request = MagicMock()
request.headers = {"X-Auth-2": 'bar'}

assert wrapped_func(request, ['admin']) is security_handler_factory.no_value
assert wrapped_func(request) is security_handler_factory.no_value

# Supplying both keys does succeed
request = MagicMock()
Expand All @@ -207,13 +207,13 @@ def apikey2_info(apikey, required_scopes=None):
'key1': {'sub': 'foo'},
'key2': {'sub': 'bar'},
}
assert wrapped_func(request, ['admin']) == expected_token_info
assert wrapped_func(request) == expected_token_info


def test_verify_security_oauthproblem(security_handler_factory):
"""Tests whether verify_security raises an OAuthProblem if there are no auth_funcs."""
func_to_secure = MagicMock(return_value='func')
secured_func = security_handler_factory.verify_security([], [], func_to_secure)
secured_func = security_handler_factory.verify_security([], func_to_secure)

request = MagicMock()
with pytest.raises(OAuthProblem) as exc_info:
Expand Down
Loading