diff --git a/README.rst b/README.rst index e07cbb15e..a723f9f60 100644 --- a/README.rst +++ b/README.rst @@ -92,6 +92,9 @@ New in Connexion 2.0: All spec validation errors should be wrapped with `InvalidSpecification`. - Support for nullable/x-nullable, readOnly and writeOnly/x-writeOnly has been added to the standard json schema validator. - Custom validators can now be specified on api level (instead of app level). +- Added support for basic authentication and apikey authentication +- If unsupported security requirements are defined or ``x-tokenInfoFunc``/``x-tokenInfoUrl`` is missing, connexion now denies requests instead of allowing access without security-check. +- Accessing ``connexion.request.user`` / ``flask.request.user`` is no longer supported, use ``connexion.context['user']`` instead How to Use ========== diff --git a/docs/security.rst b/docs/security.rst index ada79ff2b..5045f8f32 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -4,35 +4,71 @@ Security OAuth 2 Authentication and Authorization ---------------------------------------- -Connexion supports one of the three OAuth 2 handling methods. (See -"TODO" below.) With Connexion, the API security definition **must** -include a 'x-tokenInfoUrl' or 'x-tokenInfoFunc (or set ``TOKENINFO_URL`` -or ``TOKENINFO_FUNC`` env var respectively). +Connexion supports one of the three OAuth 2 handling methods. +With Connexion, the API security definition **must** include a +``x-tokenInfoFunc`` or set ``TOKENINFO_FUNC`` env var. -If 'x-tokenInfoFunc' is used, it must contain a reference to a function +``x-tokenInfoFunc`` must contain a reference to a function used to obtain the token info. This reference should be a string using the same syntax that is used to connect an ``operationId`` to a Python function when routing. For example, an ``x-tokenInfoFunc`` of ``auth.verifyToken`` would pass the user's token string to the function -``verifyToken`` in the module ``auth.py``. The referenced function should -return a dict containing a ``scope`` or ``scopes`` field that is either -a space-separated list or an array of scopes belonging to the supplied -token. This list of scopes will be validated against the scopes required -by the API security definition to determine if the user is authorized. +``verifyToken`` in the module ``auth.py``. The referenced function accepts +a token string as argument and should return a dict containing a ``scope`` +field that is either a space-separated list or an array of scopes belonging to +the supplied token. This list of scopes will be validated against the scopes +required by the API security definition to determine if the user is authorized. +You can supply a custom scope validation func with ``x-scopeValidateFunc`` +or set ``SCOPEVALIDATE_FUNC`` env var, otherwise +``connexion.decorators.security.validate_scope`` will be used as default. -If 'x-tokenInfoUrl' is used, it must contain a URL to validate and get -the token information which complies with `RFC 6749 `_. -When both 'x-tokenInfoUrl' and 'x-tokenInfoFunc' are used, Connexion -will prioritize the function method. Connexion expects to receive the -OAuth token in the ``Authorization`` header field in the format -described in `RFC 6750 `_ section 2.1. This aspect represents -a significant difference from the usual OAuth flow. +The recommended approach is to return a dict which complies with +`RFC 7662 `_. Note that you have to validate the ``active`` +or ``exp`` fields etc. yourself. -The ``uid`` property (username) of the Token Info response will be passed in the ``user`` argument to the handler function. +The ``sub`` property of the Token Info response will be passed in the ``user`` +argument to the handler function. + +Deprecated features, retained for backward compability: + +- As alternative to ``x-tokenInfoFunc``, you can set ``x-tokenInfoUrl`` or + ``TOKENINFO_URL`` env var. It must contain a URL to validate and get the token + information which complies with `RFC 6749 `_. + When both ``x-tokenInfoUrl`` and ``x-tokenInfoFunc`` are used, Connexion + will prioritize the function method. Connexion expects the authorization + server to receive the OAuth token in the ``Authorization`` header field in the + format described in `RFC 6750 `_ section 2.1. This aspect represents + a significant difference from the usual OAuth flow. +- ``scope`` field can also be named ``scopes``. +- ``sub`` field can also be named ``uid``. You can find a `minimal OAuth example application`_ in Connexion's "examples" folder. + +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. + +ApiKey Authentication +--------------------- + +With Connexion, the API security definition **must** include a +``x-apikeyInfoFunc`` or set ``APIKEYINFO_FUNC`` env var. It uses the same +semantics as for ``x-basicInfoFunc``, but the function accepts two +parameters: apikey and required_scopes. + +You can find a `minimal Basic Auth example application`_ in Connexion's "examples" folder. + + HTTPS Support ------------- @@ -43,7 +79,7 @@ Swagger UI cannot be used to play with the API. What is the correct way to start a HTTPS server when using Connexion? .. _rfc6750: https://tools.ietf.org/html/rfc6750 -.. _swager.spec.security_definition: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-definitions-object -.. _swager.spec.security_requirement: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-requirement-object .. _rfc6749: https://tools.ietf.org/html/rfc6749 -.. _minimal OAuth example application: https://github.com/zalando/connexion/tree/master/examples/oauth2 +.. _rfc7662: https://tools.ietf.org/html/rfc7662 +.. _minimal OAuth example application: https://github.com/zalando/connexion/tree/master/examples/swagger2/oauth2 +.. _minimal Basic Auth example application: https://github.com/zalando/connexion/tree/master/examples/swagger2/basicauth diff --git a/examples/openapi3/basicauth/README.rst b/examples/openapi3/basicauth/README.rst index c6de48b32..e1c265858 100644 --- a/examples/openapi3/basicauth/README.rst +++ b/examples/openapi3/basicauth/README.rst @@ -11,4 +11,10 @@ Running: Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI. -The hardcoded credentials are ``admin`` and ``secret``. +The hardcoded credentials are ``admin`` and ``secret``. For an example with +correct authentication but missing access rights, use ``foo`` and ``bar``. + +For a more simple example which doesn't use oauth scope for authorization see +the `Swagger2 Basic Auth example`_. + +.. _Swagger2 Basic Auth example: https://github.com/zalando/connexion/tree/master/examples/swagger2/basicauth diff --git a/examples/openapi3/basicauth/app.py b/examples/openapi3/basicauth/app.py index 9ea5fdc1f..74bfcaf23 100755 --- a/examples/openapi3/basicauth/app.py +++ b/examples/openapi3/basicauth/app.py @@ -1,49 +1,39 @@ #!/usr/bin/env python3 ''' -Connexion HTTP Basic Auth example - -Most of the code stolen from http://flask.pocoo.org/snippets/8/ - -Warning: It is recommended to use 'decorator' package to create decorators for - your view functions to keep Connexion working as expected. For more - details please check: https://github.com/zalando/connexion/issues/142 +Basic example of a resource server ''' import connexion -import flask - -try: - from decorator import decorator -except ImportError: - import sys - import logging - logging.error('Missing dependency. Please run `pip install decorator`') - sys.exit(1) +from connexion.decorators.security import validate_scope +from connexion.exceptions import OAuthScopeProblem -def check_auth(username: str, password: str): - '''This function is called to check if a username / - password combination is valid.''' - return username == 'admin' and password == 'secret' +def basic_auth(username, password, required_scopes=None): + if username == 'admin' and password == 'secret': + info = {'sub': 'admin', 'scope': 'secret'} + elif username == 'foo' and password == 'bar': + info = {'sub': 'user1', 'scope': ''} + else: + # optional: raise exception for custom error response + return None + # optional + if required_scopes is not None and not validate_scope(required_scopes, info['scope']): + raise OAuthScopeProblem( + description='Provided user doesn\'t have the required access rights', + required_scopes=required_scopes, + token_scopes=info['scope'] + ) -def authenticate(): - '''Sends a 401 response that enables basic auth''' - return flask.Response('You have to login with proper credentials', 401, - {'WWW-Authenticate': 'Basic realm="Login Required"'}) + return info -@decorator -def requires_auth(f: callable, *args, **kwargs): - auth = flask.request.authorization - if not auth or not check_auth(auth.username, auth.password): - return authenticate() - return f(*args, **kwargs) +def dummy_func(token): + return None -@requires_auth -def get_secret() -> str: - return 'This is a very secret string requiring authentication!' +def get_secret(user) -> str: + return "You are {user} and the secret is 'wbevuec'".format(user=user) if __name__ == '__main__': diff --git a/examples/openapi3/basicauth/openapi.yaml b/examples/openapi3/basicauth/openapi.yaml index a44694c8e..4ab8325f8 100644 --- a/examples/openapi3/basicauth/openapi.yaml +++ b/examples/openapi3/basicauth/openapi.yaml @@ -15,9 +15,19 @@ paths: schema: type: string security: + - oauth2: ['secret'] - basic: [] components: securitySchemes: + oauth2: + type: oauth2 + x-tokenInfoFunc: app.dummy_func + flows: + implicit: + authorizationUrl: https://example.com/oauth2/dialog + scopes: + secret: Allow accessing secret basic: type: http scheme: basic + x-basicInfoFunc: app.basic_auth diff --git a/examples/swagger2/basicauth/README.rst b/examples/swagger2/basicauth/README.rst index e2d5039a2..b265e8e82 100644 --- a/examples/swagger2/basicauth/README.rst +++ b/examples/swagger2/basicauth/README.rst @@ -6,9 +6,14 @@ Running: .. code-block:: bash - $ sudo pip3 install --upgrade connexion # install Connexion from PyPI + $ sudo pip3 install --upgrade connexion[swagger-ui] # install Connexion from PyPI $ ./app.py Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI. The hardcoded credentials are ``admin`` and ``secret``. + +For a more advanced example which reuses oauth scope for authorization see +the `OpenAPI3 Basic Auth example`_. + +.. _OpenAPI3 Basic Auth example: https://github.com/zalando/connexion/tree/master/examples/openapi3/basicauth diff --git a/examples/swagger2/basicauth/app.py b/examples/swagger2/basicauth/app.py index 34ffb8ac0..4246507b3 100755 --- a/examples/swagger2/basicauth/app.py +++ b/examples/swagger2/basicauth/app.py @@ -1,49 +1,22 @@ #!/usr/bin/env python3 ''' -Connexion HTTP Basic Auth example - -Most of the code stolen from http://flask.pocoo.org/snippets/8/ - -Warning: It is recommended to use 'decorator' package to create decorators for - your view functions to keep Connexion working as expected. For more - details please check: https://github.com/zalando/connexion/issues/142 +Basic example of a resource server ''' import connexion -import flask - -try: - from decorator import decorator -except ImportError: - import sys - import logging - logging.error('Missing dependency. Please run `pip install decorator`') - sys.exit(1) - - -def check_auth(username: str, password: str): - '''This function is called to check if a username / - password combination is valid.''' - return username == 'admin' and password == 'secret' -def authenticate(): - '''Sends a 401 response that enables basic auth''' - return flask.Response('You have to login with proper credentials', 401, - {'WWW-Authenticate': 'Basic realm="Login Required"'}) +def basic_auth(username, password, required_scopes=None): + if username == 'admin' and password == 'secret': + return {'sub': 'admin'} + # optional: raise exception for custom error response + return None -@decorator -def requires_auth(f: callable, *args, **kwargs): - auth = flask.request.authorization - if not auth or not check_auth(auth.username, auth.password): - return authenticate() - return f(*args, **kwargs) +def get_secret(user) -> str: + return "You are {user} and the secret is 'wbevuec'".format(user=user) -@requires_auth -def get_secret() -> str: - return 'This is a very secret string requiring authentication!' if __name__ == '__main__': app = connexion.FlaskApp(__name__) diff --git a/examples/swagger2/basicauth/swagger.yaml b/examples/swagger2/basicauth/swagger.yaml index 6a70eefc9..fe0d0fd6e 100644 --- a/examples/swagger2/basicauth/swagger.yaml +++ b/examples/swagger2/basicauth/swagger.yaml @@ -19,3 +19,4 @@ paths: securityDefinitions: basic: type: basic + x-basicInfoFunc: app.basic_auth