Repository navigation
feat: authenticate incoming requests (issue #112) #120
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
Open
jennifer-bowser
wants to merge
24
commits into
issue-111-implement-network-node-discovery
from
issue-112-authenticate-incoming-requests
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
b71ae48
implement auth for incoming requests
jennifer-bowser eab233c
first pass at writing tests
jennifer-bowser 2b7312f
use custom 'AuthError' class to track error reasons & codes instead o…
jennifer-bowser e8d1bc7
modify tests to reflect new AuthError structure
jennifer-bowser 0f4b2e7
fix duplicate imports
jennifer-bowser 2f4092c
fix 'base_vlm_network_url' to include trailing slash
jennifer-bowser d035303
add explanatory comment
jennifer-bowser 006a3df
fix some failing tests
jennifer-bowser 03fd4b6
resolve merge conflicts
jennifer-bowser 81b9bab
fix docstrings for 'JWTValidator' and 'RequestAuthenticator'
jennifer-bowser c66626f
renames 'VlmClient' > 'VLMClient' to align capitalization convention …
jennifer-bowser 9da0a1f
add client-specific cooldowns for refreshing the node registry
jennifer-bowser 84c24e2
adds cooldown for node registry refresh calls in 'RequestAuthenticator'
jennifer-bowser 83a11a5
add tests ensuring cooldown in 'RequestAuthenticator' works properly
jennifer-bowser 44e444a
make a fixture for the app and reset its state after each usage of a …
jennifer-bowser 425f7aa
fix failing tests
jennifer-bowser 6eb25bb
catch missing/malformed token 'exp' values
jennifer-bowser 7d12984
update error handling in JWTValidator
jennifer-bowser 8647619
add audience verification to token checks
jennifer-bowser b851b67
fixes failing test
jennifer-bowser 718489f
adds test for incorrect audience
jennifer-bowser d55c1d0
resolve merge conflicts
jennifer-bowser f655c1b
forward vlm network cred env vars to docker
jennifer-bowser 1e05144
fix trailing slashes in vlm network urls in 'constants.py'
jennifer-bowser File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """Handles validating and decoding JWT tokens""" | ||
|
|
||
| from typing import Any | ||
|
|
||
| import jwt | ||
| from fastapi import Request | ||
|
|
||
| from anyvlm.config import Settings, get_config | ||
| from anyvlm.utils.exceptions import AuthError, AuthErrorReason | ||
|
|
||
|
|
||
| class JWTValidator: | ||
| """Handles validating and decoding JWT tokens""" | ||
|
|
||
| def validate_and_decode_token(self, request: Request) -> dict[str, Any]: | ||
| """Validate and decode tokens from inbound requests. | ||
|
|
||
| :param request: The inbound request containing the token to be validated/decoded | ||
| :return: The decoded token claims | ||
| :raises: `AuthError`, if unable to validate or decode the request's token | ||
| """ | ||
| configs: Settings = get_config() | ||
|
|
||
| auth_header: str | None = request.headers.get("Authorization") | ||
| if not auth_header: | ||
| raise AuthError(error_reason=AuthErrorReason.MISSING_AUTH_HEADER) | ||
| try: | ||
| scheme, token = auth_header.strip().split(sep=" ") | ||
| except ValueError as e: | ||
| raise AuthError(error_reason=AuthErrorReason.MALFORMED_AUTH_HEADER) from e | ||
| if scheme.lower() != "bearer": | ||
| raise AuthError(error_reason=AuthErrorReason.INVALID_SCHEMA) | ||
|
|
||
| try: | ||
| decoded_claims: dict[str, Any] = jwt.decode( | ||
| jwt=token, | ||
| algorithms=["RS256"], | ||
| issuer=configs.vlm_network_base_url, | ||
| audience=configs.vlm_network_audience_url, | ||
| options={ | ||
| "verify_signature": False, # See PR comment for more details: https://github.com/GenomicMedLab/AnyVLM/pull/120 | ||
|
jennifer-bowser marked this conversation as resolved.
Resolved
|
||
| "verify_iss": True, | ||
| "verify_exp": True, | ||
| "verify_aud": True, | ||
| "require": ["iss", "exp", "azp", "aud"], | ||
| }, | ||
| ) | ||
| except jwt.InvalidTokenError as e: | ||
| raise AuthError(error_reason=AuthErrorReason.INVALID_TOKEN) from e | ||
|
jennifer-bowser marked this conversation as resolved.
Resolved
|
||
|
|
||
| try: | ||
| int(decoded_claims["exp"]) # catch malformed/empty 'exp' values | ||
| except TypeError as e: | ||
| raise AuthError(error_reason=AuthErrorReason.INVALID_TOKEN) from e | ||
|
|
||
| return decoded_claims | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| """Handles authenticating inbound requests""" | ||
|
|
||
| from datetime import UTC, datetime, timedelta | ||
| from typing import Any | ||
|
|
||
| from fastapi import Request | ||
|
|
||
| from anyvlm.auth.jwt_validator import JWTValidator | ||
| from anyvlm.outbound.node_registry import NodeRegistry | ||
| from anyvlm.utils.exceptions import AuthError, AuthErrorReason | ||
|
|
||
|
|
||
| class RequestAuthenticator: | ||
| """Handles authenticating inbound requests""" | ||
|
|
||
| NODE_REGISTRY_REFRESH_COOLDOWN_SECONDS = 30 | ||
|
|
||
| def __init__( | ||
| self, | ||
| jwt_validator: JWTValidator, | ||
| node_registry: NodeRegistry, | ||
| ) -> None: | ||
| """Initializes a RequestAuthenticator to authenticate inbound requests from other VLM Network nodes | ||
|
|
||
| :param jwt_validator: A 'JWTValidator' instance, used to validate/decode JWT tokens | ||
| :param node_registry: A 'NodeRegistry' instance, used to retrieve a list of all other nodes on the VLM Network | ||
|
|
||
| :raises: `NodeRetrievalError` if unable to initialize a NodeRegistry | ||
| """ | ||
| self.jwt_validator: JWTValidator = jwt_validator | ||
| self.node_registry: NodeRegistry = node_registry | ||
| self.last_node_refresh: datetime = datetime.now(tz=UTC) | ||
|
|
||
| def authenticate(self, request: Request) -> None: | ||
| """Authenticate a request: validate and decode request token + | ||
| confirm requesting node is known member of the VLM network. | ||
|
|
||
| :param request: The inbound request to authenticate | ||
| :raises: `AuthError`, if unable to authenticate. | ||
| """ | ||
| decoded_token_claims: dict[str, Any] = ( | ||
| self.jwt_validator.validate_and_decode_token(request) | ||
| ) | ||
|
|
||
| client_id: Any = decoded_token_claims.get("azp") | ||
| self.verify_node_membership(client_id=client_id) | ||
|
|
||
| def verify_node_membership(self, client_id: Any) -> None: # noqa: ANN401 | ||
| """verify that the requesting client node is a known member of the VLM Network""" | ||
| # verify that client_id exists and is a string | ||
| if not client_id or not isinstance(client_id, str): | ||
| raise AuthError(error_reason=AuthErrorReason.MISSING_CLIENT_ID) | ||
|
|
||
| # First, check current node registry | ||
| if client_id in self.node_registry.get_node_ids(): | ||
| return | ||
|
|
||
| # If client isn't in current node registry, refresh the registry and try again, | ||
| # UNLESS last refresh was w/in the cool-down period (to protect against DDOS attacks) | ||
| if datetime.now(tz=UTC) <= self.last_node_refresh + timedelta( | ||
| seconds=self.NODE_REGISTRY_REFRESH_COOLDOWN_SECONDS | ||
| ): | ||
| raise AuthError(error_reason=AuthErrorReason.UNRECOGNIZED_NODE) | ||
|
|
||
| self.last_node_refresh = datetime.now(tz=UTC) | ||
| self.node_registry.refresh_nodes() | ||
| if client_id not in self.node_registry.get_node_ids(): | ||
| raise AuthError(error_reason=AuthErrorReason.UNRECOGNIZED_NODE) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Codex finding: Severity - High (major failure in a supported workflow); Blocking: Use the current issuer and audience constants
Settingsno longer definesvlm_network_base_urlorvlm_network_audience_url, so every request reaching this call raisesAttributeErrorbefore decoding and returns 500, including requests with valid tokens. Import and use the constants fromanyvlm.utils.constants, and cover a bearer-token request using the real settings object. The current tests supply these removed attributes throughMagicMock, which hides the failure.