mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-09 20:37:46 +00:00
Add support for JWT authentication (#732)
* Add support for JWT * Add example for JWT * Add minimal JWT documentation
This commit is contained in:
@@ -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
|
||||
@@ -137,8 +152,12 @@ def validate_scope(required_scopes, token_scopes):
|
||||
return True
|
||||
|
||||
|
||||
def verify_oauth(token_info_func, scope_validate_func):
|
||||
def wrapper(request, required_scopes):
|
||||
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
|
||||
@@ -154,10 +173,19 @@ def verify_oauth(token_info_func, scope_validate_func):
|
||||
token_info = token_info_func(token)
|
||||
if token_info is None:
|
||||
raise OAuthResponseProblem(
|
||||
description='Provided oauth token is not valid',
|
||||
description='Provided token is not valid',
|
||||
token_response=None
|
||||
)
|
||||
|
||||
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:
|
||||
return None
|
||||
|
||||
# Fallback to 'scopes' for backward compability
|
||||
token_scopes = token_info.get('scope', token_info.get('scopes', ''))
|
||||
if not scope_validate_func(required_scopes, token_scopes):
|
||||
@@ -222,6 +250,16 @@ def verify_apikey(apikey_info_func, loc, name):
|
||||
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):
|
||||
|
||||
@@ -4,10 +4,11 @@ import logging
|
||||
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")
|
||||
|
||||
@@ -118,10 +119,24 @@ class SecureOperation(object):
|
||||
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':
|
||||
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))
|
||||
|
||||
@@ -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
|
||||
-------------
|
||||
|
||||
14
examples/openapi3/jwt/README.rst
Normal file
14
examples/openapi3/jwt/README.rst
Normal file
@@ -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
examples/openapi3/jwt/app.py
Executable file
53
examples/openapi3/jwt/app.py
Executable file
@@ -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
examples/openapi3/jwt/openapi.yaml
Normal file
45
examples/openapi3/jwt/openapi.yaml
Normal file
@@ -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
examples/openapi3/jwt/requirements.txt
Normal file
4
examples/openapi3/jwt/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
connexion>=2.0.0rc3
|
||||
python-jose[cryptography]
|
||||
six>=1.9
|
||||
Flask>=0.10.1
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
@@ -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
tests/fixtures/secure_endpoint/openapi.yaml
vendored
26
tests/fixtures/secure_endpoint/openapi.yaml
vendored
@@ -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
|
||||
@@ -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
tests/fixtures/secure_endpoint/swagger.yaml
vendored
28
tests/fixtures/secure_endpoint/swagger.yaml
vendored
@@ -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:
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user