Skip to content

Commit

Permalink
[FDS-2373] Update jwt verification + synpy update v4.4.0->v4.4.1 (#1493)
Browse files Browse the repository at this point in the history
* Update jwt verification + synpy update v4.4.0->v4.4.1
  • Loading branch information
BryanFauble authored Sep 6, 2024
1 parent a7ece1d commit c6db5bb
Show file tree
Hide file tree
Showing 9 changed files with 1,070 additions and 888 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ dmypy.json
# End of https://www.toptal.com/developers/gitignore/api/python

# Synapse configuration file
.synapseConfig
.synapseConfig*

# Google services authorization credentials file
credentials.json
Expand Down
1,801 changes: 945 additions & 856 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pygsheets = "^2.0.4"
PyYAML = "^6.0.0"
rdflib = "^6.0.0"
setuptools = "^66.0.0"
synapseclient = "4.4.0"
synapseclient = "4.4.1"
tenacity = "^8.0.1"
toml = "^0.10.2"
great-expectations = "^0.15.0"
Expand All @@ -78,6 +78,7 @@ asyncio = "^3.4.3"
pytest-asyncio = "^0.23.7"
jaeger-client = {version = "^4.8.0", optional = true}
flask-opentracing = {version="^2.0.0", optional = true}
PyJWT = "^2.9.0"

[tool.poetry.extras]
api = ["connexion", "Flask", "Flask-Cors", "Jinja2", "pyopenssl", "jaeger-client", "flask-opentracing"]
Expand Down
2 changes: 1 addition & 1 deletion schematic_api/api/openapi/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ components:
type: http
scheme: bearer
bearerFormat: JWT
x-bearerInfoFunc: schematic_api.api.security_controller_.info_from_bearerAuth
x-bearerInfoFunc: schematic_api.api.security_controller.info_from_bearer_auth

# TO DO: refactor query parameters and remove access_token
paths:
Expand Down
47 changes: 47 additions & 0 deletions schematic_api/api/security_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import logging
from typing import Dict, Union

from jwt import PyJWKClient, decode
from jwt.exceptions import PyJWTError
from synapseclient import Synapse

from schematic.configuration.configuration import CONFIG

logger = logging.getLogger(__name__)

syn = Synapse(
configPath=CONFIG.synapse_configuration_path,
cache_client=False,
)
jwks_client = PyJWKClient(
uri=syn.authEndpoint + "/oauth2/jwks", headers=syn._generate_headers()
)


def info_from_bearer_auth(token: str) -> Dict[str, Union[str, int]]:
"""
Authenticate user using bearer token. The token claims are decoded and returned.
Example from:
<https://pyjwt.readthedocs.io/en/stable/usage.html#retrieve-rsa-signing-keys-from-a-jwks-endpoint>
Args:
token (str): Bearer token.
Returns:
dict: Decoded token information.
"""
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
data = decode(
jwt=token,
key=signing_key.key,
algorithms=[signing_key.algorithm_name],
options={"verify_aud": False},
)

return data
except PyJWTError:
logger.exception("Error decoding authentication token")
# When the return type is None the web framework will return a 401 OAuthResponseProblem exception
return None
14 changes: 0 additions & 14 deletions schematic_api/api/security_controller_.py

This file was deleted.

16 changes: 15 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Fixtures and helpers for use across all tests"""
import configparser
import logging
import os
import shutil
Expand All @@ -8,7 +9,7 @@
import pytest
from dotenv import load_dotenv

from schematic.configuration.configuration import CONFIG
from schematic.configuration.configuration import CONFIG, Configuration
from schematic.models.metadata import MetadataModel
from schematic.schemas.data_model_graph import DataModelGraph, DataModelGraphExplorer
from schematic.schemas.data_model_parser import DataModelParser
Expand Down Expand Up @@ -152,6 +153,19 @@ def DMGE(helpers: Helpers) -> DataModelGraphExplorer:
return dmge


@pytest.fixture(scope="class")
def syn_token(config: Configuration):
synapse_config_path = config.synapse_configuration_path
config_parser = configparser.ConfigParser()
config_parser.read(synapse_config_path)
# try using synapse access token
if "SYNAPSE_ACCESS_TOKEN" in os.environ:
token = os.environ["SYNAPSE_ACCESS_TOKEN"]
else:
token = config_parser["authentication"]["authtoken"]
return token


def metadata_model(helpers, data_model_labels):
metadata_model = MetadataModel(
inputMModelLocation=helpers.get_data_path("example.model.jsonld"),
Expand Down
59 changes: 59 additions & 0 deletions tests/integration/test_security_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from pytest import LogCaptureFixture

from schematic_api.api.security_controller import info_from_bearer_auth


class TestSecurityController:
def test_valid_synapse_token(self, syn_token: str) -> None:
# GIVEN a valid synapse token
assert syn_token is not None

# WHEN the token is decoded
decoded_token = info_from_bearer_auth(syn_token)

# THEN the decoded claims are a dictionary
assert isinstance(decoded_token, dict)
assert "sub" in decoded_token
assert decoded_token["sub"] is not None
assert "token_type" in decoded_token
assert decoded_token["token_type"] is not None

def test_invalid_synapse_signing_key(self, caplog: LogCaptureFixture) -> None:
# GIVEN an invalid synapse token
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)

random_token = jwt.encode(
payload={"sub": "random"}, key=private_key, algorithm="RS256"
)

# WHEN the token is decoded
decoded_token = info_from_bearer_auth(random_token)

# THEN nothing is returned
assert decoded_token is None

# AND an error is logged
assert (
"jwt.exceptions.PyJWKClientError: Unable to find a signing key that matches:"
in caplog.text
)

def test_invalid_synapse_token_not_enough_parts(
self, caplog: LogCaptureFixture
) -> None:
# GIVEN an invalid synapse token
random_token = "invalid token"

# WHEN the token is decoded
decoded_token = info_from_bearer_auth(random_token)

# THEN nothing is returned
assert decoded_token is None

# AND an error is logged
assert "jwt.exceptions.DecodeError: Not enough segments" in caplog.text
14 changes: 0 additions & 14 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import configparser
import json
import logging
import os
Expand Down Expand Up @@ -100,19 +99,6 @@ def get_MockComponent_attribute() -> Generator[str, None, None]:
yield MockComponent_attribute


@pytest.fixture(scope="class")
def syn_token(config: Configuration):
synapse_config_path = config.synapse_configuration_path
config_parser = configparser.ConfigParser()
config_parser.read(synapse_config_path)
# try using synapse access token
if "SYNAPSE_ACCESS_TOKEN" in os.environ:
token = os.environ["SYNAPSE_ACCESS_TOKEN"]
else:
token = config_parser["authentication"]["authtoken"]
return token


@pytest.fixture
def request_headers(syn_token: str) -> Dict[str, str]:
headers = {"Authorization": "Bearer " + syn_token}
Expand Down

0 comments on commit c6db5bb

Please sign in to comment.