Skip to content
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
5 changes: 3 additions & 2 deletions docs/source/differences-to-vws.rst
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@ successful in-process Model Target datasets include a Vuforia-shaped
``warning`` object after processing completes. This configuration is not
supported by the Flask/Docker backend.
Model Target API routes require a three-part JSON Web Token with JSON object
header and payload parts and a non-``none`` ``alg`` value, such as the token
returned by the mock OAuth2 route.
header and payload parts, a non-``none`` ``alg`` value, and a non-empty
base64url-encoded signature, such as the token returned by the mock OAuth2
route.
The mock does not verify token signatures, payload claims such as expiry, or
token revocation.

Expand Down
1 change: 1 addition & 0 deletions newsfragments/model-target-jwt-signature.change
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject Model Target bearer tokens with empty or malformed JWT signatures.
41 changes: 29 additions & 12 deletions src/mock_vws/_model_target_web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,28 @@ def _jwt_payload_error(*, bearer_token: str) -> str | None:
return None


@beartype
def _jwt_signature_error(*, bearer_token: str) -> str | None:
"""Return the Vuforia error for an invalid JSON Web Token
signature.
"""
encoded_signature = bearer_token.rpartition(".")[2]
if not encoded_signature:
return "The signature must not be empty"

try:
padding = "=" * (-len(encoded_signature) % 4)
base64.b64decode(
s=encoded_signature + padding,
altchars=b"-_",
validate=True,
)
except ValueError:
return "Signed JWT rejected: Invalid signature"

return None


@beartype
def _require_bearer_token(request: RequestData) -> _ResponseType | None:
"""Return an error response if the request has no bearer token."""
Expand Down Expand Up @@ -205,21 +227,16 @@ def _require_bearer_token(request: RequestData) -> _ResponseType | None:
target="jwt",
details=None,
)
jwt_header_error = _jwt_header_error(bearer_token=bearer_token)
if jwt_header_error is not None:
return _error_response(
status_code=HTTPStatus.UNAUTHORIZED,
code="401",
message=jwt_header_error,
target="jwt",
details=None,
)
jwt_payload_error = _jwt_payload_error(bearer_token=bearer_token)
if jwt_payload_error is not None:
jwt_error = _jwt_header_error(bearer_token=bearer_token)
if jwt_error is None:
jwt_error = _jwt_payload_error(bearer_token=bearer_token)
if jwt_error is None:
jwt_error = _jwt_signature_error(bearer_token=bearer_token)
if jwt_error is not None:
return _error_response(
status_code=HTTPStatus.UNAUTHORIZED,
code="401",
message=jwt_payload_error,
message=jwt_error,
target="jwt",
details=None,
)
Expand Down
2 changes: 1 addition & 1 deletion tests/mock_vws/test_model_target_generation_failure.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from mock_vws import MockVWS, ModelTargetGenerationFailure

_AUTHORIZATION = "Bearer eyJhbGciOiJtb2NrIn0.e30.signature"
_AUTHORIZATION = "Bearer eyJhbGciOiJtb2NrIn0.e30.c2lnbmF0dXJl"
_CREATE_URL = "https://vws.vuforia.com/modeltargets/datasets"
_REQUEST_BODY: dict[str, Any] = {
"name": "dataset-name",
Expand Down
2 changes: 1 addition & 1 deletion tests/mock_vws/test_model_target_generation_warning.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
ModelTargetGenerationWarning,
)

_AUTHORIZATION = "Bearer eyJhbGciOiJtb2NrIn0.e30.signature"
_AUTHORIZATION = "Bearer eyJhbGciOiJtb2NrIn0.e30.c2lnbmF0dXJl"
_CREATE_URL = "https://vws.vuforia.com/modeltargets/datasets"
_REQUEST_BODY: dict[str, Any] = {
"name": "dataset-name",
Expand Down
12 changes: 11 additions & 1 deletion tests/mock_vws/test_model_target_web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

_VWS_HOST = "https://vws.vuforia.com"
_DATASET_UUID = "0b12466eee5d49409a440927006ff5d8"
_MOCK_BEARER_TOKEN = "eyJhbGciOiJtb2NrIn0.e30.signature"
_MOCK_BEARER_TOKEN = "eyJhbGciOiJtb2NrIn0.e30.c2lnbmF0dXJl"


def _dataset_request(*, cad_data_url: str) -> dict[str, Any]:
Expand Down Expand Up @@ -271,6 +271,16 @@ def test_missing_bearer_token(
"Payload of JWS object is not a valid JSON object",
id="payload-not-json-object",
),
pytest.param(
"Bearer eyJhbGciOiJSUzI1NiJ9.e30.",
"The signature must not be empty",
id="blank-signature",
),
pytest.param(
"Bearer eyJhbGciOiJSUzI1NiJ9.e30.%",
"Signed JWT rejected: Invalid signature",
id="signature-not-base64",
),
],
)
def test_invalid_bearer_token(
Expand Down
2 changes: 1 addition & 1 deletion tests/mock_vws/test_requests_mock_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
processing_time_seconds,
)

_MODEL_TARGET_AUTHORIZATION = "Bearer eyJhbGciOiJtb2NrIn0.e30.signature"
_MODEL_TARGET_AUTHORIZATION = "Bearer eyJhbGciOiJtb2NrIn0.e30.c2lnbmF0dXJl"
_MODEL_TARGET_DATASET_REQUEST = {
"name": "dataset-name",
"targetSdk": "10.18",
Expand Down
2 changes: 1 addition & 1 deletion tests/mock_vws/test_respx_mock_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from mock_vws.image_matchers import ExactMatcher
from mock_vws.target import VuMarkTarget

_MODEL_TARGET_AUTHORIZATION = "Bearer eyJhbGciOiJtb2NrIn0.e30.signature"
_MODEL_TARGET_AUTHORIZATION = "Bearer eyJhbGciOiJtb2NrIn0.e30.c2lnbmF0dXJl"
_MODEL_TARGET_DATASET_REQUEST = {
"name": "dataset-name",
"targetSdk": "10.18",
Expand Down