Connexion: Support for multiple security requirements (x-tokenInfoFunc)

Created on 24 May 2018  Â·  8Comments  Â·  Source: zalando/connexion

Description

In security_decorator(), in case multiple security-requirements are defined, all of them are being ignored.

Expected behaviour

  1. Logical OR operation: Authenticate if at least one function succeeds
  2. Logical AND operation: Authenticate if all functions succeeded

In any case the behavior should not be to ignore all security requirements

Actual behaviour

All security requirements are ignored and security_passthrough() is applied

Additional info:

security_decorator() on operation.py line 74

Most helpful comment

Valid remarks, but I just stumbled on the same issue. We also have a use case for it: every app needs an API key to be able to talk to our API and on top of this, we have a bearer token in the authorization header if a user is logged in into the application. We could work around it by encoding our API key in the claims of the JWT token authentication if we would go for JWT tokens, but this makes the separate security mechanisms closely intertwined and will complicate the documentation of our API imho (which is kind of ironic since we started to use the OpenAPI spec to simplify it).

According to this, it should be possible to do so:

Some REST APIs support several authentication types. The security section lets you combine the security requirements using logical OR and AND to achieve the desired result. security uses the following logic

...

That is, security is an array of hashmaps, where each hashmap contains one or more named security schemes. Items in a hashmap are combined using logical AND, and array items are combined using logical OR. Security schemes combined via OR are alternatives – any one can be used in the given context. Security schemes combined via AND must be used simultaneously in the same request.

The case @fgsalomon mentions is explicitly given as an example (see the section "Multiple API keys").

About the second remark: I think a collection containing all of the returned information would make the most sense. (e.g. an array with the elements in the same order as the security definitions in the spec or a dict mapping from the keys in the security definition). In case of the example of @fgsalomon, it could be

[return_value_of_key_infofunc, return_value_of_secretkey_infofunc]

or

{
    'key': return_value_of_key_infofunc,
    'secretkey': return_value_of_secretkey_infofunc
}

I do understand that there is not enough animo do implement the requested changes though, but can this at least be clearly documented that this is a deviation from the specification? I took me a while to figure out what the problem was and it would be nice if we could save other people from doing the same. (And yes, I do know there is a log message and I agree I should've seen it, but I thought I was following documentation / specification and didn't bother to go completely through the logs, my bad.)

All 8 comments

We are facing the same problem with version 2.0.0. We'd like to use multiple security requirements (in our case x-apikeyInfoFunc) but when multiple requirements are present all requests are denied.

secure.py:

@property
def security_decorator(self):
    ....
    for security_req in self.security:
               if not security_req:
                  continue
              elif len(security_req) > 1:
                  logger.warning("... More than one security scheme in security requirement defined. "
                               "**DENYING ALL REQUESTS**", extra=vars(self))
    return security_deny

Can you post your security definition and use-case? I assume you want to have the OR case (which works in 2.0.0). Only the AND case is not supported.

Actually, we want to have the AND case. We didn't have this issue with connexion 1.1.15 and OAS2, however we'd like to move up to OAS3. Are there any plans to support the AND case?

A sample definition api.yaml:

openapi: 3.0.0
info:
  version: '1'
  title: Sample HTTP GET endpoint

components:
  securitySchemes:
    key:
      type: apiKey
      in: header
      name: Key
    secret_key:
      type: apiKey
      in: header
      name: SecretKey
paths:
  /:
    get:
      # Both 'Key' and 'SecretKey' must be used together
      operationId: app.sample_get
      security:
        - key: []
          secret_key: []
      responses:
        200:
          description: OK

app.py:

def sample_get(**kwargs):
    return kwargs

requirements.txt:

flask==1.0.2
connexion==2.0.0

Run application and send request:

connexion run api.yaml
 python -c 'import requests; print(requests.get("http://localhost:5000/", headers={"Key": "my_key", "SecretKey": "secret_key"}).text)'

Returns:

{
  "detail": "The server encountered an internal error and was unable to complete your request.  Either the server is overloaded or there is an error in the application.",
  "status": 500,
  "title": "Internal Server Error",
  "type": "about:blank"
}

Application log:

WARNING:connexion.options:The swagger_ui directory could not be found.
    Please install connexion with extra install: pip install connexion[swagger-ui]
    or provide the path to your local installation by passing swagger_path=<your path>

WARNING:connexion.options:The 'swagger_json' option is deprecated. Please use 'serve_spec' instead
WARNING:connexion.options:The swagger_ui directory could not be found.
    Please install connexion with extra install: pip install connexion[swagger-ui]
    or provide the path to your local installation by passing swagger_path=<your path>

WARNING:connexion.operations.secure:... More than one security scheme in security requirement defined. **DENYING ALL REQUESTS**
 * Serving Flask app "connexion.cli" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
ERROR:flask.app:Exception on / [GET]
Traceback (most recent call last):
  File "/home/fgsalomon/miniconda3/lib/python3.6/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/fgsalomon/miniconda3/lib/python3.6/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/fgsalomon/miniconda3/lib/python3.6/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/fgsalomon/miniconda3/lib/python3.6/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/home/fgsalomon/miniconda3/lib/python3.6/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/fgsalomon/miniconda3/lib/python3.6/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/fgsalomon/miniconda3/lib/python3.6/site-packages/connexion/decorators/decorator.py", line 73, in wrapper
    response = function(request)
  File "/home/fgsalomon/miniconda3/lib/python3.6/site-packages/connexion/decorators/security.py", line 104, in deny
    raise ConnexionException("Error in security definitions")
connexion.exceptions.ConnexionException: Error in security definitions

I can only speak for myself (not for zalando folks): I started using connexion some month ago and didn't like the way how basic authentication was supposed to be handled. So I changed the whole security flow, which has now ended up in connexion 2.0. The change was even bigger then needed for myself, but I tried to find a generic solution which also works for other users.
I thought about supporting the AND case, but decided against it:

  • I didn't find a valid use-case for it
  • A successful authentication returns a token_info which is stored in the request context. So one can e.g. also have user context for basic auth or apikey requests like for oauth2. If multiple security mechanism succeed, it's unclear which information to use in the context.

Valid remarks, but I just stumbled on the same issue. We also have a use case for it: every app needs an API key to be able to talk to our API and on top of this, we have a bearer token in the authorization header if a user is logged in into the application. We could work around it by encoding our API key in the claims of the JWT token authentication if we would go for JWT tokens, but this makes the separate security mechanisms closely intertwined and will complicate the documentation of our API imho (which is kind of ironic since we started to use the OpenAPI spec to simplify it).

According to this, it should be possible to do so:

Some REST APIs support several authentication types. The security section lets you combine the security requirements using logical OR and AND to achieve the desired result. security uses the following logic

...

That is, security is an array of hashmaps, where each hashmap contains one or more named security schemes. Items in a hashmap are combined using logical AND, and array items are combined using logical OR. Security schemes combined via OR are alternatives – any one can be used in the given context. Security schemes combined via AND must be used simultaneously in the same request.

The case @fgsalomon mentions is explicitly given as an example (see the section "Multiple API keys").

About the second remark: I think a collection containing all of the returned information would make the most sense. (e.g. an array with the elements in the same order as the security definitions in the spec or a dict mapping from the keys in the security definition). In case of the example of @fgsalomon, it could be

[return_value_of_key_infofunc, return_value_of_secretkey_infofunc]

or

{
    'key': return_value_of_key_infofunc,
    'secretkey': return_value_of_secretkey_infofunc
}

I do understand that there is not enough animo do implement the requested changes though, but can this at least be clearly documented that this is a deviation from the specification? I took me a while to figure out what the problem was and it would be nice if we could save other people from doing the same. (And yes, I do know there is a log message and I agree I should've seen it, but I thought I was following documentation / specification and didn't bother to go completely through the logs, my bad.)

Note: to disable connexion security checking, comment out this block of code from connexion/operation.py and connexion/operations/abstract.py:

# NOTE: the security decorator should be applied last to check auth before anything
security_decorator = self.security_decorator
logger.debug('... Adding security decorator (%r)', security_decorator)
function = security_decorator(function)

Note: to disable connexion security checking, comment out this block of code from connexion/operation.py and connexion/operations/abstract.py:

# NOTE: the security decorator should be applied last to check auth before anything
security_decorator = self.security_decorator
logger.debug('... Adding security decorator (%r)', security_decorator)
function = security_decorator(function)

Then I suppose you don't have any security validation anymore at all?
Or will it just use the "_first available_" security function?

I am having the same issue where I need to setup 2 api_keys i.e implement AND operation. This thread concludes that we cannot do that .. but does anyone know if this has changed or still the same. Thank you

Was this page helpful?
0 / 5 - 0 ratings