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

Add support for JWT authentication #732

Merged
merged 7 commits into from
Nov 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 54 additions & 16 deletions connexion/decorators/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ def get_apikeyinfo_func(security_definition):
return None


def get_bearerinfo_func(security_definition):
"""
:type security_definition: dict
:rtype: function

>>> get_bearerinfo_func({'x-bearerInfoFunc': 'foo.bar'})
'<function foo.bar>'
"""
func = (security_definition.get("x-bearerInfoFunc") or
os.environ.get('BEARERINFO_FUNC'))
if func:
return get_function_from_name(func)
return None


def security_passthrough(function):
"""
:type function: types.FunctionType
Expand Down Expand Up @@ -137,26 +152,39 @@ def validate_scope(required_scopes, token_scopes):
return True


def verify_oauth(token_info_func, scope_validate_func):
def wrapper(request, required_scopes):
authorization = request.headers.get('Authorization')
if not authorization:
return None
def verify_authorization_token(request, token_info_func):
"""
:param request: ConnexionRequest
:param token_info_func: types.FunctionType
:rtype: dict
"""
authorization = request.headers.get('Authorization')
if not authorization:
return None

try:
auth_type, token = authorization.split(None, 1)
except ValueError:
raise OAuthProblem(description='Invalid authorization header')
try:
auth_type, token = authorization.split(None, 1)
except ValueError:
raise OAuthProblem(description='Invalid authorization header')

if auth_type.lower() != 'bearer':
return None
if auth_type.lower() != 'bearer':
return None

token_info = token_info_func(token)
if token_info is None:
raise OAuthResponseProblem(
description='Provided token is not valid',
token_response=None
)

token_info = token_info_func(token)
return token_info


def verify_oauth(token_info_func, scope_validate_func):
def wrapper(request, required_scopes):
token_info = verify_authorization_token(request, token_info_func)
if token_info is None:
raise OAuthResponseProblem(
description='Provided oauth token is not valid',
token_response=None
)
return None

# Fallback to 'scopes' for backward compability
token_scopes = token_info.get('scope', token_info.get('scopes', ''))
Expand Down Expand Up @@ -222,6 +250,16 @@ def wrapper(request, required_scopes):
return wrapper


def verify_bearer(bearer_info_func):
"""
:param bearer_info_func: types.FunctionType
:rtype: types.FunctionType
"""
def wrapper(request, required_scopes):
return verify_authorization_token(request, bearer_info_func)
return wrapper


def verify_security(auth_funcs, required_scopes, function):
@functools.wraps(function)
def wrapper(request):
Expand Down
29 changes: 22 additions & 7 deletions connexion/operations/secure.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
from ..decorators.decorator import (BeginOfRequestLifecycleDecorator,
EndOfRequestLifecycleDecorator)
from ..decorators.security import (get_apikeyinfo_func, get_basicinfo_func,
get_bearerinfo_func,
get_scope_validate_func, get_tokeninfo_func,
security_deny, security_passthrough,
verify_apikey, verify_basic, verify_oauth,
verify_security)
verify_apikey, verify_basic, verify_bearer,
verify_oauth, verify_security)

logger = logging.getLogger("connexion.operations.secure")

Expand Down Expand Up @@ -118,16 +119,30 @@ def security_decorator(self):
continue

auth_funcs.append(verify_basic(basic_info_func))
elif scheme == 'bearer':
bearer_info_func = get_bearerinfo_func(security_scheme)
if not bearer_info_func:
logger.warning("... x-bearerInfoFunc missing", extra=vars(self))
continue
auth_funcs.append(verify_bearer(bearer_info_func))
else:
logger.warning("... Unsupported http authorization scheme %s" % scheme, extra=vars(self))

elif security_scheme['type'] == 'apiKey':
apikey_info_func = get_apikeyinfo_func(security_scheme)
if not apikey_info_func:
logger.warning("... x-apikeyInfoFunc missing", extra=vars(self))
continue
scheme = security_scheme.get('x-authentication-scheme', '').lower()
if scheme == 'bearer':
bearer_info_func = get_bearerinfo_func(security_scheme)
if not bearer_info_func:
logger.warning("... x-bearerInfoFunc missing", extra=vars(self))
continue
auth_funcs.append(verify_bearer(bearer_info_func))
else:
apikey_info_func = get_apikeyinfo_func(security_scheme)
if not apikey_info_func:
logger.warning("... x-apikeyInfoFunc missing", extra=vars(self))
continue

auth_funcs.append(verify_apikey(apikey_info_func, security_scheme['in'], security_scheme['name']))
auth_funcs.append(verify_apikey(apikey_info_func, security_scheme['in'], security_scheme['name']))

else:
logger.warning("... Unsupported security scheme type %s" % security_scheme['type'], extra=vars(self))
Expand Down
8 changes: 8 additions & 0 deletions docs/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ parameters: apikey and required_scopes.

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

Bearer Authentication (JWT)
---------------------------

With Connexion, the API security definition **must** include a
``x-bearerInfoFunc`` or set ``BEARERINFO_FUNC`` env var. It uses the same
semantics as for ``x-tokenInfoFunc``, but the function accepts one parameter: token.

You can find a `minimal JWT example application`_ in Connexion's "examples/openapi3" folder.

HTTPS Support
-------------
Expand Down
14 changes: 14 additions & 0 deletions examples/openapi3/jwt/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
=======================
JWT Auth Example
=======================

Running:

.. code-block:: bash

$ sudo pip3 install -r requirements.txt
$ ./app.py

Now open your browser and go to http://localhost:8080/ui/ to see the Swagger UI.
Use endpoint **/auth** to generate JWT token, copy it, then click **Authorize** button and paste the token.
Now you can use endpoint **/secret** to check autentication.
53 changes: 53 additions & 0 deletions examples/openapi3/jwt/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
'''
Basic example of a resource server
'''

import time

import connexion
import six
from werkzeug.exceptions import Unauthorized

from jose import JWTError, jwt

JWT_ISSUER = 'com.zalando.connexion'
JWT_SECRET = 'change_this'
JWT_LIFETIME_SECONDS = 600
JWT_ALGORITHM = 'HS256'


def generate_token(user_id):
timestamp = _current_timestamp()
payload = {
"iss": JWT_ISSUER,
"iat": int(timestamp),
"exp": int(timestamp + JWT_LIFETIME_SECONDS),
"sub": str(user_id),
}

return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)


def decode_token(token):
try:
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
except JWTError as e:
six.raise_from(Unauthorized, e)


def get_secret(user, token_info) -> str:
return '''
You are user_id {user} and the secret is 'wbevuec'.
Decoded token claims: {token_info}.
'''.format(user=user, token_info=token_info)


def _current_timestamp() -> int:
return int(time.time())


if __name__ == '__main__':
app = connexion.FlaskApp(__name__)
app.add_api('openapi.yaml')
app.run(port=8080)
45 changes: 45 additions & 0 deletions examples/openapi3/jwt/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
openapi: 3.0.0
info:
title: JWT Example
version: '1.0'
paths:
/auth/{user_id}:
get:
summary: Return JWT token
operationId: app.generate_token
parameters:
- name: user_id
description: User unique identifier
in: path
required: true
example: 12
schema:
type: integer
responses:
'200':
description: JWT token
content:
'text/plain':
schema:
type: string
/secret:
get:
summary: Return secret string
operationId: app.get_secret
responses:
'200':
description: secret response
content:
'text/plain':
schema:
type: string
security:
- jwt: ['secret']

components:
securitySchemes:
jwt:
type: http
scheme: bearer
bearerFormat: JWT
x-bearerInfoFunc: app.decode_token
4 changes: 4 additions & 0 deletions examples/openapi3/jwt/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
connexion>=2.0.0rc3
python-jose[cryptography]
six>=1.9
Flask>=0.10.1
5 changes: 4 additions & 1 deletion tests/api/test_secure_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def test_security(oauth_requests, secure_endpoint_app):
assert get_bye_bad_token.content_type == 'application/problem+json'
get_bye_bad_token_reponse = json.loads(get_bye_bad_token.data.decode('utf-8', 'replace')) # type: dict
assert get_bye_bad_token_reponse['title'] == 'Unauthorized'
assert get_bye_bad_token_reponse['detail'] == "Provided oauth token is not valid"
assert get_bye_bad_token_reponse['detail'] == "Provided token is not valid"

response = app_client.get('/v1.0/more-than-one-security-definition') # type: flask.Response
assert response.status_code == 401
Expand All @@ -84,6 +84,9 @@ def test_security(oauth_requests, secure_endpoint_app):
get_bye_from_connexion = app_client.get('/v1.0/byesecure-from-connexion', headers=headers) # type: flask.Response
assert get_bye_from_connexion.data == b'Goodbye test-user (Secure!)'

headers = {"Authorization": "Bearer 100"}
get_bye_from_connexion = app_client.get('/v1.0/byesecure-jwt/test-user', headers=headers) # type: flask.Response
assert get_bye_from_connexion.data == b'Goodbye test-user (Secure: 100)'

def test_checking_that_client_token_has_all_necessary_scopes(
oauth_requests, secure_endpoint_app):
Expand Down
7 changes: 7 additions & 0 deletions tests/fakeapi/hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ def get_bye_secure_from_connexion(req_context):
def get_bye_secure_ignoring_context(name):
return 'Goodbye {name} (Secure!)'.format(name=name)

def get_bye_secure_jwt(name, user, token_info):
return 'Goodbye {name} (Secure: {user})'.format(name=name, user=user)

def with_problem():
return problem(type='http://www.example.com/error',
Expand Down Expand Up @@ -499,3 +501,8 @@ def apikey_info(apikey, required_scopes=None):
if apikey == 'mykey':
return {'sub': 'admin'}
return None

def jwt_info(token):
if token == '100':
return {'sub': '100'}
return None
26 changes: 26 additions & 0 deletions tests/fixtures/secure_endpoint/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,27 @@ paths:
required: true
schema:
type: string
'/byesecure-jwt/{name}':
get:
summary: Generate goodbye
description: Generates a goodbye message.
operationId: fakeapi.hello.get_bye_secure_jwt
security:
- jwt: []
responses:
'200':
description: goodbye response
content:
text/plain:
schema:
type: string
parameters:
- name: name
in: path
description: Name of the person to say bye.
required: true
schema:
type: string
/more-than-one-security-definition:
get:
summary: Some external call to API
Expand Down Expand Up @@ -121,3 +142,8 @@ components:
name: X-Auth
in: header
x-apikeyInfoFunc: fakeapi.hello.apikey_info
jwt:
type: http
scheme: bearer
bearerFormat: JWT
x-bearerInfoFunc: fakeapi.hello.jwt_info
28 changes: 28 additions & 0 deletions tests/fixtures/secure_endpoint/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ securityDefinitions:
in: header
x-apikeyInfoFunc: fakeapi.hello.apikey_info

jwt:
type: apiKey
name: Authorization
in: header
x-authentication-scheme: Bearer
x-bearerInfoFunc: fakeapi.hello.jwt_info

paths:
/byesecure/{name}:
get:
Expand Down Expand Up @@ -99,6 +106,27 @@ paths:
required: true
type: string

/byesecure-jwt/<name>:
get:
summary: Generate goodbye
description: ""
operationId: fakeapi.hello.get_bye_secure_jwt
security:
- jwt: []
produces:
- text/plain
responses:
200:
description: goodbye response
schema:
type: string
parameters:
- name: name
in: path
description: Name of the person to say bye.
required: true
type: string

/more-than-one-security-definition:
get:
summary: Some external call to API
Expand Down