From e5860e84ca180db8ed07168f859e6ebc769cbef2 Mon Sep 17 00:00:00 2001 From: maya Date: Tue, 23 Jul 2024 16:22:09 +0300 Subject: [PATCH 1/4] initial commit --- apigw-dynamodb-python-cdk/README.md | 74 ++ .../apigw_dynamodb_python_cdk/__init__.py | 0 .../api_key_usage_plan_construct.py | 62 ++ .../apigateway_construct.py | 204 +++++ .../apigw_dynamodb_python_cdk_stack.py | 58 ++ .../cognito_construct.py | 54 ++ .../dynamodb_construct.py | 17 + .../lambda_construct.py | 38 + .../user_pool_group_construct.py | 18 + apigw-dynamodb-python-cdk/app.py | 44 + apigw-dynamodb-python-cdk/cdk.json | 64 ++ .../example-pattern.json | 53 ++ apigw-dynamodb-python-cdk/image.png | Bin 0 -> 48148 bytes apigw-dynamodb-python-cdk/requirements.txt | 5 + .../src/PyJWT-2.8.0.dist-info/AUTHORS.rst | 7 + .../src/PyJWT-2.8.0.dist-info/INSTALLER | 1 + .../src/PyJWT-2.8.0.dist-info/LICENSE | 21 + .../src/PyJWT-2.8.0.dist-info/METADATA | 107 +++ .../src/PyJWT-2.8.0.dist-info/RECORD | 33 + .../src/PyJWT-2.8.0.dist-info/REQUESTED | 0 .../src/PyJWT-2.8.0.dist-info/WHEEL | 5 + .../src/PyJWT-2.8.0.dist-info/top_level.txt | 1 + apigw-dynamodb-python-cdk/src/jwt/__init__.py | 74 ++ .../jwt/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 1925 bytes .../__pycache__/algorithms.cpython-311.pyc | Bin 0 -> 39998 bytes .../jwt/__pycache__/api_jwk.cpython-311.pyc | Bin 0 -> 7463 bytes .../jwt/__pycache__/api_jws.cpython-311.pyc | Bin 0 -> 14276 bytes .../jwt/__pycache__/api_jwt.cpython-311.pyc | Bin 0 -> 14386 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 0 -> 3855 bytes .../src/jwt/__pycache__/help.cpython-311.pyc | Bin 0 -> 2389 bytes .../__pycache__/jwk_set_cache.cpython-311.pyc | Bin 0 -> 1938 bytes .../__pycache__/jwks_client.cpython-311.pyc | Bin 0 -> 6457 bytes .../src/jwt/__pycache__/types.cpython-311.pyc | Bin 0 -> 396 bytes .../src/jwt/__pycache__/utils.cpython-311.pyc | Bin 0 -> 6924 bytes .../jwt/__pycache__/warnings.cpython-311.pyc | Bin 0 -> 469 bytes .../src/jwt/algorithms.py | 862 ++++++++++++++++++ apigw-dynamodb-python-cdk/src/jwt/api_jwk.py | 132 +++ apigw-dynamodb-python-cdk/src/jwt/api_jws.py | 328 +++++++ apigw-dynamodb-python-cdk/src/jwt/api_jwt.py | 372 ++++++++ .../src/jwt/exceptions.py | 70 ++ apigw-dynamodb-python-cdk/src/jwt/help.py | 64 ++ .../src/jwt/jwk_set_cache.py | 31 + .../src/jwt/jwks_client.py | 124 +++ apigw-dynamodb-python-cdk/src/jwt/py.typed | 0 apigw-dynamodb-python-cdk/src/jwt/types.py | 5 + apigw-dynamodb-python-cdk/src/jwt/utils.py | 156 ++++ apigw-dynamodb-python-cdk/src/jwt/warnings.py | 2 + apigw-dynamodb-python-cdk/src/lambda.zip | Bin 0 -> 70748 bytes .../src/lambda_function.py | 105 +++ apigw-dynamodb-python-cdk/tests/__init__.py | 0 .../tests/test_apigw_dynamodb_python_stack.py | 161 ++++ apigw-dynamodb-python-cdk/vtl/deleteItem.vtl | 8 + apigw-dynamodb-python-cdk/vtl/putItem.vtl | 14 + apigw-dynamodb-python-cdk/vtl/response.vtl | 9 + apigw-dynamodb-python-cdk/vtl/scan.vtl | 15 + .../vtl/scan_request.vtl | 3 + 56 files changed, 3401 insertions(+) create mode 100644 apigw-dynamodb-python-cdk/README.md create mode 100644 apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/__init__.py create mode 100644 apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/api_key_usage_plan_construct.py create mode 100644 apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/apigateway_construct.py create mode 100644 apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/apigw_dynamodb_python_cdk_stack.py create mode 100644 apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/cognito_construct.py create mode 100644 apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/dynamodb_construct.py create mode 100644 apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/lambda_construct.py create mode 100644 apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/user_pool_group_construct.py create mode 100644 apigw-dynamodb-python-cdk/app.py create mode 100644 apigw-dynamodb-python-cdk/cdk.json create mode 100644 apigw-dynamodb-python-cdk/example-pattern.json create mode 100644 apigw-dynamodb-python-cdk/image.png create mode 100644 apigw-dynamodb-python-cdk/requirements.txt create mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/AUTHORS.rst create mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/INSTALLER create mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/LICENSE create mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/METADATA create mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/RECORD create mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/REQUESTED create mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/WHEEL create mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/top_level.txt create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__init__.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/__init__.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/algorithms.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jwk.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jws.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jwt.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/exceptions.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/help.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/jwk_set_cache.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/jwks_client.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/types.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/utils.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/warnings.cpython-311.pyc create mode 100644 apigw-dynamodb-python-cdk/src/jwt/algorithms.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/api_jwk.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/api_jws.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/api_jwt.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/exceptions.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/help.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/jwk_set_cache.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/jwks_client.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/py.typed create mode 100644 apigw-dynamodb-python-cdk/src/jwt/types.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/utils.py create mode 100644 apigw-dynamodb-python-cdk/src/jwt/warnings.py create mode 100644 apigw-dynamodb-python-cdk/src/lambda.zip create mode 100644 apigw-dynamodb-python-cdk/src/lambda_function.py create mode 100644 apigw-dynamodb-python-cdk/tests/__init__.py create mode 100644 apigw-dynamodb-python-cdk/tests/test_apigw_dynamodb_python_stack.py create mode 100644 apigw-dynamodb-python-cdk/vtl/deleteItem.vtl create mode 100644 apigw-dynamodb-python-cdk/vtl/putItem.vtl create mode 100644 apigw-dynamodb-python-cdk/vtl/response.vtl create mode 100644 apigw-dynamodb-python-cdk/vtl/scan.vtl create mode 100644 apigw-dynamodb-python-cdk/vtl/scan_request.vtl diff --git a/apigw-dynamodb-python-cdk/README.md b/apigw-dynamodb-python-cdk/README.md new file mode 100644 index 000000000..114c27fc8 --- /dev/null +++ b/apigw-dynamodb-python-cdk/README.md @@ -0,0 +1,74 @@ + +# API Gateway direct integration to DynamoDB + +This pattern shows how to create an API Gateway with direct integration to DynamoDB. +The pettern showcase transformation of request/response using VTL and CDK and implement examples for using Cognito, Lambda authorizer and API keys. + +Learn more about this pattern at Serverless Land Patterns: [Serverless Land Patterns](https://serverlessland.com/patterns/apigw-dynamodb-python-cdk). + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +![alt text](image.png) + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) (AWS CDK) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: +``` +git clone https://github.com/aws-samples/serverless-patterns/ +``` +2. Change directory +``` +cd serverless-patterns/apigw-dynamodb-python-cdk +``` +3. To manually create a virtualenv on MacOS and Linux: +``` +$ python3 -m venv .venv +``` +4. After the init process completes and the virtualenv is created, you can use the following to activate virtualenv. +``` +$ source .venv/bin/activate +``` +6. After activating your virtual environment for the first time, install the app's standard dependencies: +``` +python -m pip install -r requirements.txt +``` +7. To generate a cloudformation templates (optional) +``` +cdk synth +``` +8. To deploy AWS resources as a CDK project +``` +cdk deploy +``` + +## How it works +At the end of the deployment the CDK output will list stack outputs, and an API Gateway URL. In the customer's AWS account, a REST API along with an authorizer, Cognito user pool, and a DynamoDB table will be created. +Put resource - uses Lambda authorizer to authenticate the client and send allow/deny to API Gateway. +Get resource - uses API key to control the rate limit. Need to provide valid key for the request with x-api-key header. +Delete resource - uses Cognito to authenticate the client. Cognito token need to be provided with Authorization header. + +## Testing +1. Run pytest +``` +pytest tests/test_apigw_dynamodb_python_stack.py +``` +## Cleanup + +1. Delete the stack + ```bash + cdk destroy + ``` +1. Confirm the stack has been deleted + ```bash + cdk list + ``` +---- +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 \ No newline at end of file diff --git a/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/__init__.py b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/api_key_usage_plan_construct.py b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/api_key_usage_plan_construct.py new file mode 100644 index 000000000..d3ad2ab6f --- /dev/null +++ b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/api_key_usage_plan_construct.py @@ -0,0 +1,62 @@ +from constructs import Construct +import aws_cdk.aws_apigateway as apigateway + + +class UsagePlanConstruct(Construct): + def __init__(self, scope: Construct, id: str, apigateway_construct, plan_name, plan_config ,**kwargs) -> None: + super().__init__(scope, id, **kwargs) + + # Map the period of the usage plan from the config to apigateway.Period.XXX + period_enum = self.get_period_enum(plan_config['quota']['period']) + + # Create usage plan dynamically using the context data + usage_plan = apigateway_construct.api.add_usage_plan(plan_name, + name=plan_name, + throttle=apigateway.ThrottleSettings( + rate_limit=plan_config['throttle']['rate_limit'], + burst_limit=plan_config['throttle']['burst_limit'] + ), + quota=apigateway.QuotaSettings( + limit=plan_config['quota']['limit'], + period=period_enum + ) + ) + + # Create API key + api_key = apigateway.ApiKey(self, f"ApiKey-{plan_name}", + api_key_name=f"ApiKey-{plan_name}") + self.api_key_id = api_key.key_id + usage_plan.add_api_key(api_key) + + # If method is configured in the context assign the API key to the relevant API method + if plan_config['method']: + def get_method(method_name): + method_mapping = { # Change the method to fit your API + "GET": apigateway_construct.get_method, + "POST": apigateway_construct.put_method, + "DELETE": apigateway_construct.delete_method + } + return method_mapping.get(method_name.upper()) + usage_plan.add_api_stage( + stage=apigateway_construct.api.deployment_stage, + throttle=[apigateway.ThrottlingPerMethod( + method=get_method(plan_config['method']), + throttle=apigateway.ThrottleSettings( + rate_limit=100, + burst_limit=1 + ))] + ) + + + + + @staticmethod + def get_period_enum(period: str) -> apigateway.Period: + period_mapping = { + "DAY": apigateway.Period.DAY, + "WEEK": apigateway.Period.WEEK, + "MONTH": apigateway.Period.MONTH + } + return period_mapping.get(period.upper()) + + \ No newline at end of file diff --git a/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/apigateway_construct.py b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/apigateway_construct.py new file mode 100644 index 000000000..c2f71cd0e --- /dev/null +++ b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/apigateway_construct.py @@ -0,0 +1,204 @@ +from constructs import Construct +import aws_cdk.aws_apigateway as apigateway +import aws_cdk.aws_iam as iam +import os + +class ApiGatewayConstruct(Construct): + def __init__(self, scope: Construct, id: str, cognito_construct, dynamodb_construct, lambda_construct, vtl_dir ,**kwargs) -> None: + super().__init__(scope, id, **kwargs) + + self.vtl_dir = vtl_dir + + # Define the Cognito Authorizer + cognito_authorizer = apigateway.CognitoUserPoolsAuthorizer(self, "CognitoAuthorizer", + cognito_user_pools=[cognito_construct.user_pool] + ) + + # Define lambda authorizer + lambda_authorizer = apigateway.RequestAuthorizer(self, "LambdaAuthorizer", + handler=lambda_construct.lambda_function, + identity_sources=[apigateway.IdentitySource.header("Authorization")] + ) + + # Create IAM role + api_gateway_role = iam.Role(self, "ApiGatewayDynamoDBRole", + assumed_by=iam.ServicePrincipal("apigateway.amazonaws.com"), + inline_policies={ + "DynamoDBAccess": iam.PolicyDocument( + statements=[ + iam.PolicyStatement( + actions=["dynamodb:PutItem","dynamodb:DeleteItem", "dynamodb:Scan", "dynamodb:Query", "dynamodb:DescribeTable"], + resources=[dynamodb_construct.table.table_arn] + ) + ] + ) + } + ) + + # Define API Gateway + self.api = apigateway.RestApi(self, "MyApi", + rest_api_name="My Service", + description="This service serves my DynamoDB table.", + cloud_watch_role=True, + deploy_options=apigateway.StageOptions( + stage_name="prod", + logging_level=apigateway.MethodLoggingLevel.INFO, + data_trace_enabled=True, + metrics_enabled=True, + variables={ + "TableName": dynamodb_construct.table.table_name} + ) + ) + + # Change default response for Bad Request Body + self.api.add_gateway_response( + "BadRequestBody", + type=apigateway.ResponseType.BAD_REQUEST_BODY, + templates={ + "application/json": '{"message": "Invalid Request Body: $context.error.validationErrorString"}' + } + ) + + # Create request model schema + request_model_schema = apigateway.JsonSchema( + type=apigateway.JsonSchemaType.OBJECT, + required=["ID","FirstName", "Age"], + properties={ + "ID": {"type": apigateway.JsonSchemaType.STRING}, + "FirstName": {"type": apigateway.JsonSchemaType.STRING}, + "Age": {"type": apigateway.JsonSchemaType.NUMBER} + }, + # Allow to send additional properites - handled in putItem.vtl to construct them to the request + additional_properties=True + ) + + # Create a request validator + request_validator = apigateway.RequestValidator(self, "RequestValidator", + rest_api=self.api, + validate_request_body=True, + validate_request_parameters=False + ) + + # Create the request model + request_model = apigateway.Model(self, "RequestModel", + rest_api=self.api, + content_type="application/json", + schema=request_model_schema, + model_name="PutObjectRequestModel" + ) + + # Create integration request + integration_request = apigateway.AwsIntegration( + service="dynamodb", + action="PutItem", + options=apigateway.IntegrationOptions( + credentials_role=api_gateway_role, + request_templates={ + "application/json": + self.get_vtl_template("putItem.vtl") + }, + integration_responses=[ + apigateway.IntegrationResponse( + status_code="200", + response_templates={ + "application/json": self.get_vtl_template("response.vtl") + } + ), + ] + ) + ) + + # Create a resource and method for the API Gateway + put_resource = self.api.root.add_resource("put") + self.put_method = put_resource.add_method( + "POST", + integration_request, + authorization_type=apigateway.AuthorizationType.CUSTOM, + authorizer=lambda_authorizer, + request_validator=request_validator, + request_models={"application/json": request_model}, + method_responses=[ + apigateway.MethodResponse(status_code="200",response_models={ + "application/json": apigateway.Model.EMPTY_MODEL + } ), + ] + ) + + # Add GET method with response mapping + get_integration = apigateway.AwsIntegration( + service="dynamodb", + action="Scan", + options=apigateway.IntegrationOptions( + credentials_role=api_gateway_role, + request_templates={ + "application/json": self.get_vtl_template('scan_request.vtl') + }, + integration_responses=[ + apigateway.IntegrationResponse( + status_code="200", + response_templates={ + "application/json": self.get_vtl_template('scan.vtl') + } + ), + ] + ) + ) + + get_resource = self.api.root.add_resource('get') + self.get_method = get_resource.add_method( + "GET", get_integration, + api_key_required=True, + method_responses=[ + apigateway.MethodResponse( + status_code="200", + response_models={ + "application/json": apigateway.Model.EMPTY_MODEL + } + ), + ] + ) + + delete_resource = self.api.root.add_resource('delete') + delete_resource_id = delete_resource.add_resource('{id}') + self.delete_method = delete_resource_id.add_method( + "POST", + apigateway.AwsIntegration( + service="dynamodb", + action="DeleteItem", + options=apigateway.IntegrationOptions( + credentials_role=api_gateway_role, + request_templates={ + "application/json": + self.get_vtl_template("deleteItem.vtl") + }, + integration_responses=[ + apigateway.IntegrationResponse( + status_code="200", + response_templates={ + "application/json": '{"message": "Item deleted"}' + } + ), + ] + ) + ), + authorization_type=apigateway.AuthorizationType.COGNITO, + authorizer=cognito_authorizer, + request_validator=request_validator, + method_responses=[ + apigateway.MethodResponse( + status_code="200", + response_models={ + "application/json": apigateway.Model.EMPTY_MODEL + } + ), + ] + ) + + + def get_vtl_template(self, filename: str) -> str: + """ + Reads a VTL template from a file and returns its contents as a string. + """ + template_path = os.path.join(self.vtl_dir, filename) + with open(template_path, "r") as f: + return f.read() diff --git a/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/apigw_dynamodb_python_cdk_stack.py b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/apigw_dynamodb_python_cdk_stack.py new file mode 100644 index 000000000..69fd41c76 --- /dev/null +++ b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/apigw_dynamodb_python_cdk_stack.py @@ -0,0 +1,58 @@ +from aws_cdk import Stack +from aws_cdk import CfnOutput +from constructs import Construct + +from apigw_dynamodb_python_cdk.api_key_usage_plan_construct import UsagePlanConstruct +from apigw_dynamodb_python_cdk.apigateway_construct import ApiGatewayConstruct +from apigw_dynamodb_python_cdk.cognito_construct import CognitoConstruct +from apigw_dynamodb_python_cdk.dynamodb_construct import DynamoDBConstruct +from apigw_dynamodb_python_cdk.lambda_construct import LambdaConstruct +from apigw_dynamodb_python_cdk.user_pool_group_construct import UserPoolGroupConstruct + +class ApigwDynamodbPythonStack(Stack): + + def __init__(self, scope: Construct, id: str, **kwargs) -> None: + super().__init__(scope, id, **kwargs) + + vtl_dir = self.node.try_get_context("vtl_dir") + + + lambda_construct = LambdaConstruct(self, "LambdaConstruct") + cognito_construct = CognitoConstruct(self, "CognitoConstruct") + dynamodb_construct = DynamoDBConstruct(self, "DynamoDBConstruct") + # Passing full construct is an option, specific ID can be used - Example: dynamodb_construct.table.table_arn + apigateway_construct = ApiGatewayConstruct(self, "ApiGatewayConstruct", cognito_construct, dynamodb_construct, lambda_construct, vtl_dir) + + # Using the context defined in app.py to iterate and create multiple resources + group_names = self.node.try_get_context("group_names") + if group_names: + for group_name in group_names: + UserPoolGroupConstruct( + self, + f"UserPoolGroup{group_name}Construct", + cognito_construct, + group_name + ) + + + api_key_ids = [] + usage_plans = self.node.try_get_context("usage_plans") + if usage_plans: + for usage_plan_name, usage_plan_config in usage_plans.items(): + use_plan_construct = UsagePlanConstruct( + self, + f"ApiGateway{usage_plan_name}Construct", + apigateway_construct, + usage_plan_name, + usage_plan_config + ) + api_key_ids.append(use_plan_construct.api_key_id) + + for index, api_key_id in enumerate(api_key_ids): + CfnOutput(self, f"ApiKeyId{index}", value=api_key_id) + # Outputs - used also by the tests + CfnOutput(self, "CognitoUserPoolId", value=cognito_construct.user_pool.user_pool_id) + CfnOutput(self, "CognitoClientId", value=cognito_construct.user_pool_client.user_pool_client_id) + CfnOutput(self, "ApiUrl", value=apigateway_construct.api.url) + CfnOutput(self, "DynamoDBTableName", value=dynamodb_construct.table.table_name) + diff --git a/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/cognito_construct.py b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/cognito_construct.py new file mode 100644 index 000000000..97d8bb2cd --- /dev/null +++ b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/cognito_construct.py @@ -0,0 +1,54 @@ +import aws_cdk as cdk +from constructs import Construct +import aws_cdk.aws_cognito as cognito + + +class CognitoConstruct(Construct): + def __init__(self, scope: Construct, id: str, **kwargs) -> None: + super().__init__(scope, id, **kwargs) + + # Create Cognito user pool + self.user_pool = cognito.UserPool(self, "MyUserPool", + user_pool_name="my_user_pool", + self_sign_up_enabled=True, + auto_verify=cognito.AutoVerifiedAttrs(email=True), + sign_in_aliases=cognito.SignInAliases(email=True), + standard_attributes={ + "email": { + "required": True, + "mutable": False + } + }, + removal_policy=cdk.RemovalPolicy.DESTROY + ) + + # Create user pool client + self.user_pool_client = cognito.UserPoolClient(self, "UserPoolClient", + user_pool=self.user_pool, + generate_secret=False, + auth_flows=cognito.AuthFlow( + user_password=True, + admin_user_password=True, + # user_srp=True, + ), + o_auth=cognito.OAuthSettings( + callback_urls=["http://localhost"], + flows=cognito.OAuthFlows( + authorization_code_grant=True + ), + scopes=[ + cognito.OAuthScope.EMAIL, + cognito.OAuthScope.OPENID, + cognito.OAuthScope.COGNITO_ADMIN + ] + ), + supported_identity_providers=[cognito.UserPoolClientIdentityProvider.COGNITO] + ) + + # Define the user pool domain + cognito.UserPoolDomain(self, "UserPoolDomain_", + user_pool=self.user_pool, + cognito_domain=cognito.CognitoDomainOptions( + domain_prefix="a1faegn" # This must be unique across all AWS accounts and regions + ) + ) diff --git a/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/dynamodb_construct.py b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/dynamodb_construct.py new file mode 100644 index 000000000..c49dc55e4 --- /dev/null +++ b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/dynamodb_construct.py @@ -0,0 +1,17 @@ +import aws_cdk as cdk +from constructs import Construct +import aws_cdk.aws_dynamodb as dynamodb + + +class DynamoDBConstruct(Construct): + def __init__(self, scope: Construct, id: str, **kwargs) -> None: + super().__init__(scope, id, **kwargs) + + # Create DynamoDB table + self.table = dynamodb.Table( + self, "MyTable", + partition_key=dynamodb.Attribute(name="ID", type=dynamodb.AttributeType.STRING), + sort_key=dynamodb.Attribute(name="FirstName", type=dynamodb.AttributeType.STRING), + removal_policy=cdk.RemovalPolicy.DESTROY, # NOT recommended for production code + billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST + ) diff --git a/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/lambda_construct.py b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/lambda_construct.py new file mode 100644 index 000000000..863b8761b --- /dev/null +++ b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/lambda_construct.py @@ -0,0 +1,38 @@ +from constructs import Construct +import aws_cdk.aws_lambda as lambda_ +import aws_cdk.aws_iam as iam + +class LambdaConstruct(Construct): + def __init__(self, scope: Construct, id: str ,**kwargs) -> None: + super().__init__(scope, id, **kwargs) + + # Create lambda execution role + lambda_execution_role = iam.Role( + self, + "LambdaExecutionRole", + assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), + managed_policies=[ + iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole") + ], + inline_policies={ + "LambdaPolicy": iam.PolicyDocument( + statements=[ + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=["apigateway:GET"], + resources=["*"] + ) + ] + ) + } + ) + + # Create lambda function. + self.lambda_function = lambda_.Function(self, "LambdaFunction", + runtime=lambda_.Runtime.PYTHON_3_12, + handler="lambda_function.lambda_handler", + role=lambda_execution_role, + code=lambda_.Code.from_asset("src/lambda.zip") + ) + + \ No newline at end of file diff --git a/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/user_pool_group_construct.py b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/user_pool_group_construct.py new file mode 100644 index 000000000..32ea8b53d --- /dev/null +++ b/apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/user_pool_group_construct.py @@ -0,0 +1,18 @@ +from constructs import Construct +import aws_cdk.aws_cognito as cognito + + +class UserPoolGroupConstruct(Construct): + def __init__(self, scope: Construct, id: str, cognito_construct, group_name, **kwargs) -> None: + super().__init__(scope, id, **kwargs) + # Create user pool group. + # Required parameters - + # 1. User pool ID - taken from the cofnito construct + # 2. Group name - taken from the stack context + cognito.CfnUserPoolGroup(self, group_name, + user_pool_id=cognito_construct.user_pool.user_pool_id, + group_name=group_name, + description=f"Group created {group_name}", + precedence=1 + ) + diff --git a/apigw-dynamodb-python-cdk/app.py b/apigw-dynamodb-python-cdk/app.py new file mode 100644 index 000000000..4bfca1d5e --- /dev/null +++ b/apigw-dynamodb-python-cdk/app.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +import os +import aws_cdk as cdk +from apigw_dynamodb_python_cdk.apigw_dynamodb_python_cdk_stack import ApigwDynamodbPythonStack + + +app = cdk.App() + +vtl_dir = os.path.join(os.path.dirname(__file__), "vtl") +group_names = ["Group-FreeTier", "Group-BasicUsagePlan"] + +app.node.set_context("group_names", group_names) +app.node.set_context("vtl_dir", vtl_dir) + +usage_plans = { + "FreeTier": { + "quota": { + "limit": 500, + "period": "DAY" + }, + "throttle": { + "burst_limit": 10, + "rate_limit": 5 + }, + "method": "GET" + }, + "BasicUsagePlan": { + "quota": { + "limit": 10000, + "period": "MONTH" + }, + "throttle": { + "burst_limit": 100, + "rate_limit": 50 + }, + "method": "POST" + } +} + +app.node.set_context("usage_plans", usage_plans) + +stack = ApigwDynamodbPythonStack(app, "CapstoneDynamodbPythonStack") + +app.synth() diff --git a/apigw-dynamodb-python-cdk/cdk.json b/apigw-dynamodb-python-cdk/cdk.json new file mode 100644 index 000000000..0e9b4305b --- /dev/null +++ b/apigw-dynamodb-python-cdk/cdk.json @@ -0,0 +1,64 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true + } +} diff --git a/apigw-dynamodb-python-cdk/example-pattern.json b/apigw-dynamodb-python-cdk/example-pattern.json new file mode 100644 index 000000000..0d3da06c0 --- /dev/null +++ b/apigw-dynamodb-python-cdk/example-pattern.json @@ -0,0 +1,53 @@ +{ + "title": "API Gateway direct integration to DynamoDB", + "description": "Create direct integration with API Gateway to DynamoDB showcasing transformation of request/response using VTL and CDK and implement examples for using Cognito, Lambda authorizer and API keys.", + "language": "Python", + "level": "300", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern shows how to create an API Gateway with direct integration to DynamoDB.", + "The pettern showcase transformation of request/response using VTL and CDK and implement examples for using Cognito, Lambda authorizer and API keys." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-dynamodb-python-cdk", + "templateURL": "serverless-patterns/apigw-dynamodb-python-cdk", + "projectFolder": "apigw-dynamodb-python-cdk", + "templateFile": "apigw_dynamodb_python_cdk_stack.py" + } + }, + "resources": { + "bullets": [ + { + "text": "API Gateway Integrations", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-integration-settings.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk delete." + ] + }, + "authors": [ + { + "name": "Maya Morav Freiman", + "image": "https://avatars.githubusercontent.com/u/11615439?v=4", + "bio": "Technical Account Manager at AWS", + "linkedin": "mayaaws" + } + ] +} diff --git a/apigw-dynamodb-python-cdk/image.png b/apigw-dynamodb-python-cdk/image.png new file mode 100644 index 0000000000000000000000000000000000000000..97b7d1df123ee443c9ed9d8a53c1d829cd586af1 GIT binary patch literal 48148 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 ho`b`HzgV~zm_Kg&x-=10.0.0,<11.0.0 +pytest==8.2.2 +requests==2.31.0 +boto3 diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/AUTHORS.rst b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/AUTHORS.rst new file mode 100644 index 000000000..88e2b6ad7 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/AUTHORS.rst @@ -0,0 +1,7 @@ +Authors +======= + +``pyjwt`` is currently written and maintained by `Jose Padilla `_. +Originally written and maintained by `Jeff Lindsay `_. + +A full list of contributors can be found on GitHub’s `overview `_. diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/INSTALLER b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/LICENSE b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/LICENSE new file mode 100644 index 000000000..fd0ecbc88 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2022 José Padilla + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/METADATA b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/METADATA new file mode 100644 index 000000000..b329a46ea --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/METADATA @@ -0,0 +1,107 @@ +Metadata-Version: 2.1 +Name: PyJWT +Version: 2.8.0 +Summary: JSON Web Token implementation in Python +Home-page: https://github.com/jpadilla/pyjwt +Author: Jose Padilla +Author-email: hello@jpadilla.com +License: MIT +Keywords: json,jwt,security,signing,token,web +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Topic :: Utilities +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: AUTHORS.rst +Requires-Dist: typing-extensions ; python_version <= "3.7" +Provides-Extra: crypto +Requires-Dist: cryptography (>=3.4.0) ; extra == 'crypto' +Provides-Extra: dev +Requires-Dist: sphinx (<5.0.0,>=4.5.0) ; extra == 'dev' +Requires-Dist: sphinx-rtd-theme ; extra == 'dev' +Requires-Dist: zope.interface ; extra == 'dev' +Requires-Dist: cryptography (>=3.4.0) ; extra == 'dev' +Requires-Dist: pytest (<7.0.0,>=6.0.0) ; extra == 'dev' +Requires-Dist: coverage[toml] (==5.0.4) ; extra == 'dev' +Requires-Dist: pre-commit ; extra == 'dev' +Provides-Extra: docs +Requires-Dist: sphinx (<5.0.0,>=4.5.0) ; extra == 'docs' +Requires-Dist: sphinx-rtd-theme ; extra == 'docs' +Requires-Dist: zope.interface ; extra == 'docs' +Provides-Extra: tests +Requires-Dist: pytest (<7.0.0,>=6.0.0) ; extra == 'tests' +Requires-Dist: coverage[toml] (==5.0.4) ; extra == 'tests' + +PyJWT +===== + +.. image:: https://github.com/jpadilla/pyjwt/workflows/CI/badge.svg + :target: https://github.com/jpadilla/pyjwt/actions?query=workflow%3ACI + +.. image:: https://img.shields.io/pypi/v/pyjwt.svg + :target: https://pypi.python.org/pypi/pyjwt + +.. image:: https://codecov.io/gh/jpadilla/pyjwt/branch/master/graph/badge.svg + :target: https://codecov.io/gh/jpadilla/pyjwt + +.. image:: https://readthedocs.org/projects/pyjwt/badge/?version=stable + :target: https://pyjwt.readthedocs.io/en/stable/ + +A Python implementation of `RFC 7519 `_. Original implementation was written by `@progrium `_. + +Sponsor +------- + ++--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| |auth0-logo| | If you want to quickly add secure token-based authentication to Python projects, feel free to check Auth0's Python SDK and free plan at `auth0.com/developers `_. | ++--------------+-----------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +.. |auth0-logo| image:: https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png + +Installing +---------- + +Install with **pip**: + +.. code-block:: console + + $ pip install PyJWT + + +Usage +----- + +.. code-block:: pycon + + >>> import jwt + >>> encoded = jwt.encode({"some": "payload"}, "secret", algorithm="HS256") + >>> print(encoded) + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg + >>> jwt.decode(encoded, "secret", algorithms=["HS256"]) + {'some': 'payload'} + +Documentation +------------- + +View the full docs online at https://pyjwt.readthedocs.io/en/stable/ + + +Tests +----- + +You can run tests from the project root after cloning with: + +.. code-block:: console + + $ tox diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/RECORD b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/RECORD new file mode 100644 index 000000000..b12566709 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/RECORD @@ -0,0 +1,33 @@ +PyJWT-2.8.0.dist-info/AUTHORS.rst,sha256=klzkNGECnu2_VY7At89_xLBF3vUSDruXk3xwgUBxzwc,322 +PyJWT-2.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +PyJWT-2.8.0.dist-info/LICENSE,sha256=eXp6ICMdTEM-nxkR2xcx0GtYKLmPSZgZoDT3wPVvXOU,1085 +PyJWT-2.8.0.dist-info/METADATA,sha256=pV2XZjvithGcVesLHWAv0J4T5t8Qc66fip2sbxwoz1o,4160 +PyJWT-2.8.0.dist-info/RECORD,, +PyJWT-2.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +PyJWT-2.8.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92 +PyJWT-2.8.0.dist-info/top_level.txt,sha256=RP5DHNyJbMq2ka0FmfTgoSaQzh7e3r5XuCWCO8a00k8,4 +jwt/__init__.py,sha256=mV9lg6n4-0xiqCKaE1eEPC9a4j6sEkEYQcKghULE7kU,1670 +jwt/__pycache__/__init__.cpython-311.pyc,, +jwt/__pycache__/algorithms.cpython-311.pyc,, +jwt/__pycache__/api_jwk.cpython-311.pyc,, +jwt/__pycache__/api_jws.cpython-311.pyc,, +jwt/__pycache__/api_jwt.cpython-311.pyc,, +jwt/__pycache__/exceptions.cpython-311.pyc,, +jwt/__pycache__/help.cpython-311.pyc,, +jwt/__pycache__/jwk_set_cache.cpython-311.pyc,, +jwt/__pycache__/jwks_client.cpython-311.pyc,, +jwt/__pycache__/types.cpython-311.pyc,, +jwt/__pycache__/utils.cpython-311.pyc,, +jwt/__pycache__/warnings.cpython-311.pyc,, +jwt/algorithms.py,sha256=RDsv5Lm3bzwsiWT3TynT7JR41R6H6s_fWUGOIqd9x_I,29800 +jwt/api_jwk.py,sha256=HPxVqgBZm7RTaEXydciNBCuYNKDYOC_prTdaN9toGbo,4196 +jwt/api_jws.py,sha256=da17RrDe0PDccTbx3rx2lLezEG_c_YGw_vVHa335IOk,11099 +jwt/api_jwt.py,sha256=yF9DwF1kt3PA5n_TiU0OmHd0LtPHfe4JCE1XOfKPjw0,12638 +jwt/exceptions.py,sha256=KDC3M7cTrpR4OQXVURlVMThem0pfANSgBxRz-ttivmo,1046 +jwt/help.py,sha256=Jrp84fG43sCwmSIaDtY08I6ZR2VE7NhrTff89tYSE40,1749 +jwt/jwk_set_cache.py,sha256=hBKmN-giU7-G37L_XKgc_OZu2ah4wdbj1ZNG_GkoSE8,959 +jwt/jwks_client.py,sha256=9W8JVyGByQgoLbBN1u5iY1_jlgfnnukeOBTpqaM_9SE,4222 +jwt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jwt/types.py,sha256=VnhGv_VFu5a7_mrPoSCB7HaNLrJdhM8Sq1sSfEg0gLU,99 +jwt/utils.py,sha256=PAI05_8MHQCxWQTDlwN0hTtTIT2DTTZ28mm1x6-26UY,3903 +jwt/warnings.py,sha256=50XWOnyNsIaqzUJTk6XHNiIDykiL763GYA92MjTKmok,59 diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/REQUESTED b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/REQUESTED new file mode 100644 index 000000000..e69de29bb diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/WHEEL b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/WHEEL new file mode 100644 index 000000000..1f37c02f2 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.40.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/top_level.txt b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/top_level.txt new file mode 100644 index 000000000..27ccc9bc3 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/top_level.txt @@ -0,0 +1 @@ +jwt diff --git a/apigw-dynamodb-python-cdk/src/jwt/__init__.py b/apigw-dynamodb-python-cdk/src/jwt/__init__.py new file mode 100644 index 000000000..68d09c1c4 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/__init__.py @@ -0,0 +1,74 @@ +from .api_jwk import PyJWK, PyJWKSet +from .api_jws import ( + PyJWS, + get_algorithm_by_name, + get_unverified_header, + register_algorithm, + unregister_algorithm, +) +from .api_jwt import PyJWT, decode, encode +from .exceptions import ( + DecodeError, + ExpiredSignatureError, + ImmatureSignatureError, + InvalidAlgorithmError, + InvalidAudienceError, + InvalidIssuedAtError, + InvalidIssuerError, + InvalidKeyError, + InvalidSignatureError, + InvalidTokenError, + MissingRequiredClaimError, + PyJWKClientConnectionError, + PyJWKClientError, + PyJWKError, + PyJWKSetError, + PyJWTError, +) +from .jwks_client import PyJWKClient + +__version__ = "2.8.0" + +__title__ = "PyJWT" +__description__ = "JSON Web Token implementation in Python" +__url__ = "https://pyjwt.readthedocs.io" +__uri__ = __url__ +__doc__ = f"{__description__} <{__uri__}>" + +__author__ = "José Padilla" +__email__ = "hello@jpadilla.com" + +__license__ = "MIT" +__copyright__ = "Copyright 2015-2022 José Padilla" + + +__all__ = [ + "PyJWS", + "PyJWT", + "PyJWKClient", + "PyJWK", + "PyJWKSet", + "decode", + "encode", + "get_unverified_header", + "register_algorithm", + "unregister_algorithm", + "get_algorithm_by_name", + # Exceptions + "DecodeError", + "ExpiredSignatureError", + "ImmatureSignatureError", + "InvalidAlgorithmError", + "InvalidAudienceError", + "InvalidIssuedAtError", + "InvalidIssuerError", + "InvalidKeyError", + "InvalidSignatureError", + "InvalidTokenError", + "MissingRequiredClaimError", + "PyJWKClientConnectionError", + "PyJWKClientError", + "PyJWKError", + "PyJWKSetError", + "PyJWTError", +] diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/__init__.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa62819d91a5e5bbf29ebd41b17b770988052b90 GIT binary patch literal 1925 zcmdUv%Wm676oyI3x>0x8@>On_#AzZ279GV7dI^FCZWeZs)Tj*vn9YFJjHOXHBt0BD zmR4Hz6}l(_1nr{8bMytgDg>?!bk&`K?7Zp>DK?JVu98hj;^Ucr&YW{N7yeo-YKpkt z_^%ItNGr-8vJqcX6XV_Q!uU}^3Q{eFsz9X)kWghiVI}Dln4&3=qG^z(8IYk_kPZ8i zR*q^wqj`|01yBgJDXU0JphU}{Oe>&5tDs72phl;`G@Su6v<~WY7R=H)Fh}RXJY4__ zbP+7lC9p&rph1_xau_FNeMYZ=Yr;+g9c93Elm$0X4&1~y!3w_dlY%t7BA9l)AQ<}9uRq=7nM3CAY?7VmyWW(1U=FL%eQ$`kD=9z;T%=SuqO#BbkuiS5X9;TiEfVt2pCN51@!Ez2Y{ z>RJq^d&?3f@h!);afcIUGOTi`FY?wxC9;e0LS5?G5ACKJi^Wp8C zz3-muFYtjL)~6HdT9}IDrp&7oTi+e>GrtZUca}w z)?RP7^$&(Omsm>pg{87(X(c7(Ib?Z>f<#6lE0L2Br&eLI5>}EZOH?GP5;ciwi5ZEy z#H_@e#Jt3U#G=HKL_^?t^KOtaT>|^BUcRp(mb|FSF!6`Q#2!5QguiPJGrY4b&BD6(1*!E<+B7~iIO+`~gGcDotR(U7pP`T(DE+fY#$HuYAj6=r~ z{|C?Lca4F4bi(?F{ml&zZg98rVDsqsOUgDjhuj?=8T*Dy+%;}8@#9&0u<7^Uuzh4$ zZANwVS1Ds*S;Q=$J(7yW~@~Oij7lkRXlGp p(OYnoIV(0s+PI*Pa%ZLKQGQ&xKPsN5m-AvOVvI8KX7|Km{RKtSC))r3 literal 0 HcmV?d00001 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/algorithms.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/algorithms.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aad57249934a5f76ab172b524fabd3b509a5ebf8 GIT binary patch literal 39998 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 aGe%6x2aTPpx{A1;6lHf+nST;A0sdcPZcBLp literal 0 HcmV?d00001 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jwk.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jwk.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..819755a2da00d09cda8693f30cd77f18230d3607 GIT binary patch literal 7463 zcmcgxU2GFq7M@?nP8>V2^B+hD2&7J%qyYjcbW^f~0;QXVCV|#%@fy!0IQWk{6KGsR zBbJA>8>vx+M5MM-R@$;b^q~)xwhz0~R$6Ia?Z_*vks?J}@zA#*SPAj8=iIR;Gl{ch zwc5R&oVoYhpE>v5^Yh)CKU7y&GLV+0zn+k580H^XC?&zju#dlo%pFE%WH!dc*&xee zo{Mooj=uRI4{tsu#KoWpb%HF$9Px@^Mcf&5#$7=d)KEtbxn#*hgs%#zSOXh3tE`0C4`OTYbhEw~rs*N7j_L_TxZ~qc$wYJ+v zJ}@17uj?Fq?T^pVQ`pi|$ndgwnXU-w&j_-^$g5mNPU?o*n7f&Wb+LR;ZvNSIvGiTCXsJbJi=EhY8)Pu`S7yMzTZVQ1vNn_U0g?hNF~QDbr`PP@Z24emXt0kQZ%7z;aE(OyVHBe6XA<7 z1uG_^30Vp!B-5k83J~eC0wV^hi38SEud+r8SE%F)bsmBVLEoZRMpZg`BBJPm433^u z0Gl|G;8yFd!BVWzof9M($3bB2m6?F33#t;EAgG0;7D)q=CLpQ^M8foJq9;Y7SHqgp zb2UtQCXz8(A!^UmWY0|E%12~sVyf?;8ak+@B8U2}Tzxf84)&$BRQgKKSWhaN>H@b7 zqIMnXo4yoE@4wO$i(c$W&1jdBiJqxzT2D9?MR~eYGrBVriYB63D3tb4&);1tquW;- z!QIR7C;*pl>mxY|CEAQ!=;|pHjI>Wk7 zv#Y*c+1)3<)4sm}FL-jkQ+eO1toziOw+UPHe;r&FzV-ZB$nE?^e&;VJtI)LLE6;3gFtF@j7$`^`3#S)O7wVf9PkwP>;ljTupINEz$kulhq!+Qr!3 z_8PisG6tYt^#Pe>9(wB*Mi&D)Z+qU`o^`g9UhFF%l+k&BF6D*%H8ipdx#90KjNPkV z=QNyLF>MzOwoZ}sK^H)k!jzg!=wd7xmQ^}qatJGUau{BQN5BP85%Pd5uudL(+9r>T zUt;6EKw$DO(>BMidh0&_JJ$8 zR6|isjQ-fm=uhi8Bafp7sxY!7!f_=O(knxucv7B@Vcs1IU6~HYj2iL^G}YY@rL|}z zu0VW}sV$J#knc>9WJ)303^@ixGK2)xl?c{*ZoO-8VUYi--W-+c z8l7(hW@)D4!H9i43@po`vQ`#v3B@C&Ys+yD50rk{y@l_9<0l*@gB5ZO*mS+_q`C!% zTwMTlQ=e7GqN?_O@o@X$hVIzBDq<)Qi+%hCSpFSm5?o5ilsChwUacrJGG*58U2n1) zqM%~Ruo{k5O!lC$X`D^?8_sp?5cA=Y>zvE((uC4z*0=5aw`nB1d@M8n(jLZoE^KEA z&ob{Z^M|i7AMx)o*H{e|vY4#y)IO5T7tcHP^$_f>Zo(NCwn7|Ew~t*?Bv^V0wnLJm zGDYu^WI_u^0nH|65W}kJ`r@61*%EFrrWJyLG(G-vBTTClCF4Nd(uhJXg;VNgY+XDy znBe;LfRl{C=mZZZBmhJgP)l@2IF(WovSFfVo;n{>5)kJwC>UM3AZLqn)FY&8OIl~5 z(Q$0}Hz2c2p{@Zihzp_w!k1G3UtY*cZ{&O@^S+Z=_esD;i|yYCIq%-Q7p>d%Ds6B$ z=j+e=`m^qSKsD=(=mG=h5B16Fxi8cfW5_^i^vSFr6J z`;@ zOiYF-Lfr)fBf9VNYoA?PxVlJk-rafc?m1!A+qfuvdSm`Z*16O0Y-Vh+%@I8X^(7Y| z^V|rHeGY`SW!6P}MvlOj$WbITh*EII8Q_fKG7Bc9fok*Q5tAjM`*xVDv50N<34&PO;n- z381%hD_T8x7PuG%&w|dxI2cFaasZ5$n)ndNpgYeLuR}|wOaBZ3%uaxylg~pbJO8kNS)a-ibX~;Gn z`EC>lJUP$Nyys}vd6cf=a6m9bJqyEb#3$h5-jQ=A_dMi)Yiga*w#cP3qBw`5BZ_XE z

&3jpR)vuOOjN5LJY#0{H-Qze0k@(10jZi;#@LpPB(OYyQ?bzX+bePCARYqYaiq zG>8Rqv?1gG@WP25SZ@$ZeaE^}5MNtowjqXKC{6_a%!En}iG92es0Bv2WFBFJTuI@C zAXmv1P!?q;cnP;&k7hpxyLujO)yJZ71)y9!rB~pM3Fs0vAe%PhXQ(!#CV@8Ng)iic zWOYg#SHE|m@_wlaLJ8<^Bn*rtqs38lK?4zVR~+n5OD3Wb-GvM)6SyxLcCekzec>*q z+se+vj6S$$>JA{Fr;Tk8ecY?fJHKknH3#5q4Moo!{EUS~eX%((aIMCSYanB{-;(q! zL8-PJJ*vP?=-m^nWO`S52`x=#VGI@ToRe8XyU1 zf{LfM0$t}>mMt*5v-W3=shXv~!bYYc%h;YZ=4^KRr@#znf9@$TEm`}s?l{4+a9S!) nA5s1jYwZZv&VoPNN**Ea$wtL6>tbQwwvtE4ds3!??&ZG#ntV&i literal 0 HcmV?d00001 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jws.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jws.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a453035ea35157b5d736301de466348f2413c592 GIT binary patch literal 14276 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 literal 0 HcmV?d00001 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/exceptions.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05a67bcdd8998f2fc8cc1acf59f25b6b39b7e95e GIT binary patch literal 3855 zcmb7GO-~zF6n!&e8{1$E1Zen52ckB0RIrjlqzGDx5>ydXt1U>C*o<7yGmJYP+i%7c zEEX)X$f65&AXSmFYKz(*&|lCs%gEdQ0Y}a%tKNHU$BYN8nP<4b#~Rrc0dtt}yl&T&=Pv5hnwuaH)rri3a}oZh*L9;F2CLO56x=qaH3s+!%0o zJnf4UHxAr{r`{lOlfZrC;f9F23*40Fn+f8kft&Gg!^F)3H|OD!#N7k#W1j5k&kmZ>OQz0+oi6OBkFCm)DC7$(%eJ=clygs3Du+*tVtJ+Jlxvmr zZn|ug7oDuw=g#7jmFj_3dt6DI#!u<;v2#$e(uH4~bT`JO^0C0FOMpFK&j6f$_%&oX z9BnX!C=161S?H|79)}0ZWVtTvsB35JjM` z<0=$G9Y9kv3DAw|O(@!l1lns@iUII zD24EeTN6zIG?VRK-R8%A8h8&i5K?Y2GzAdrERA(@za$Ljpx8%JMJ0qqeCRF>G`S=( znP6!?1h}4nIAm{K$%pjNi_l(3m${Oc^)Nh#bp?`Bq(1enVcUka{{yd7artbRS)&zF zJq)ScDApr_!wR-CIhiC{fX@nCO_j?YT8jbH8GKsGTuS0Tyg>#G+1$+2i;Wdk zRbZ0Cox!jSN7HHxw??tl@p%k2Y|Kih?9IgV%h0d!m++(Plz*1Bc8BX9k>Ez~(n;e}Ew)@Own!*TA;kq`L z(G)-@fv1q|9o{b$rBMOlOScr70!WRJ)P@PUy`LHmgg{t#tDz}?+VnNGjgn>Y93G?k zbm$i-hp_3^MN=jrvYLH_W)DYV z85`7+2S2pXLP|{IfXRc?Hiecx75dg+ur>-T3jssPLtctX8Zde4nUz-Yx^p=@XXczU zXU@#|_Gu&%L{PShkLRBI5c-QUZUNpYum6P4LnI@a$)Qy3%cK~GXHzVE*&LVWQ#^xY zPUdq$-k0(-C{U9WQ=%efU0Wy>&>{m!2@WDGfV_|j+(8<@jP7xF&@vAvyfQMg z$*v)-eE^Y0gb;$*h~PI1)mc9nA&*tw^}%a(jag-6t{#&%j|M%7Qx0jc+D*{m>pIs- z*p)*l>vzX%+$xt9UG5Rw9`J!fyz`#u+#~c@SYtsVYY)HCBU|fK9Z@tg^DW$;tuJ^-f!5fba3}!A*V|B|wf6aOp7^#iZEXVkU+ay@1JxBl@vqC~^#z$-Dv!i$%pCMogDx7o?05n^$srtVqqmux{VWtT~! z86O2bOs@sgyRiK?rmtXJz;?Ut?4CYkrKvt$-`3{9f4fC5>7_GROK^|LB?%|z3OO0_H<`^OOWNW+oSn;#jT-4uqR)v;+2m0i|C0;G`_wFch%z0*8)2^tk! zk5oA-vcJFo#)mq3EA3}@+Q%yGW97*`fBQz~uD{>(_rK^%RQk^E^i5RyCQPvxniYs{ zjPC~e%s}6Zo)eXx_f4^LuQ^s}9;zZXl5mJs<2jeFuL~|;6HHgB;E1|>>w|7IM zW@ywp8Gm~F$!)78ZYCxwiEHMOPyXT9p{9Qk5HsArUlmdRY4D)xL+$;0ZT(hs7*5_z zPYy&xT?5Yt&zr|CSVy}be{Qy%t~QHdUlrAe;4^6Ae;Gf8ermqR{lQ%1JV;Bi#zERS z7(DdgEOD>{rlDjTcuE@R^nYxX*`K*>(0bO;^x_F%EJX)lB{2n{PfEk^tTf_eJcWaU zAR!sCPp5B8j?BEGJ43Dp41kLCf&#hXQ#BcKauVLO;8=h>Fd)kXxtP}tOrOw?=rwg4 z4+63IyrgP43Bw8K#%UmB^pX#JJ^0nB?`FQe_2AYP`Sl|+IKC4cuK?xGRrqtJ+wcgr zA*R#RN;;ijF^#|^+{ssasnqe#yDm&Fe&1I)r zC|Gu?g`%dn?V(eqw^ez8>9zQ%>9)P@c%}QS$sdEZZ~40*r?}db_ zigCubE`M`&gWs6m8rb-3v&n27sx-x`2o?^L8HQ!fL*95u5b@#lm0hmW0EMNZO d>iw%_r$YAdT`=uD=AXxo2=`-M5f{RfcHQt1Ey literal 0 HcmV?d00001 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/jwk_set_cache.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/jwk_set_cache.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e81369b8a4b8bff0270571508b159b3fb96d30e6 GIT binary patch literal 1938 zcmZ`(&uipV9DgsrCY|5gUsejRg#MsLkGQUKxdh5SQjmgmRHTy8ah)kL7%8m6>1>@Va&^AQW5l4l zNa3F%ML1#*I)x{sqM+bal#g9db0v>G@iWWOOrxYEZMjyi*6R0r(PzN9UV(ncYg>fD~2ZB*sv%!Z_E4<&2cshWmV+j8Iuiz(B7r>$*Ilx5A(99eFT_8~Fd^NLWDJqBzS zb?)BZ<6QBb&O~mH`zo~u*Chmy7waY+z>4A&U=^OJ9sFe6iO@l42OoiT8hNf_7hoGj zDVih+8S@MgK#)|Zr>Q;+tcxAVfbOwY!)a#vy8#E?ZBGM(MiRrV#9O~lee_$bJ$28W zy0^FFCMR3L?F;BVsm~@iU`shF1dQkq7+~zf5vYL->B;C0I_zKD3!o#=P9uvjw4wf; zhcxRo8r;=`fDO9$;+ie1k1dU;ikE;-E_c7e2__U4o7R!*0s`SdK)^9V7Zrd}Kr(LP z-Bx1yhg&~MKT7SHMR#Tq%+APb&kE<65jQjSJTrHenQLd}-OPMToWBlB^u51@l!LG+ zsDaIoq1^}?1wZ0o88T-bp30DFG)F*#jSm17?CY)T$!qJQH#K2N_%4`sX652vpgbQS z7z~}q(TVhi>ZV~jrlFO+6vgu8m`*@~J}k*N*v;|&!}anz&>xOX|5Ur^Jeh?vb?mf~ z@B4^NE#pph^qbTnbC$j7v()|*>~uSubF;Zlde}|ppQrDfrSG)U3vPPh=cQjhZKpr^ zo5eSlu^;1y;sN0K1RYnSR99u$i_3D|R2n+fld`EEkHbM z>lW4MAP23!5I+ck`;bh6j~*49&als7j5{c?8(baqMr-gsM}^j_ZeN(dQ1W&57xn$~ Iia=1yf5A$seEB_m2?wlv-p>kW+o9j#V0dJQa+5Vi66mnuxq;+T3 zm-8nBly_x=xll5c3n#<0?#@PX1IYmfvFK$ac`hNzdzD4#EBISia!_IxQKIiYsV*cK z)0M@msZ2U^T84^yaq-w}Ue)CbdV)8(BkG#T&nX$*6Sz?O7dOelOAb@V=ONPp;eSZuoh7kpm!L?`=m&#J^*#U9FPX(u93|V_O99NQuGpk71sS#YwgLP zWCN`}VR|~YaQ;5v6fNDKfDKx>KLotVtzyM=Q*Ect$ZMJ@Cly&oL|*VkEUfowG=HGS8FW@*?{NZ{}AvBswvqovI<(a zmzWYtwYo_NY6Lp;D%qjd0%^$sH8O+M=7idoiJ=mYCrdVoIYYv3M7YbfIH3cb)GYzvA3#?`=u7S|qdx#t%xe zEnxDZ#i?V>t&QhaH9dY(j-SGET9TyMjkNm`pfIT=sVVY8u z6D}NvSYYmeCzsYWvYOcbFk#8b7?%aoX-+`Os$-nT2s$nz$?ouGyAmEKE06H zKXdlnb2+?krl=Q+XQ!5?3QA$Gp2o18z58cYPp67cpPkAoC#MQ)`sutnwQ^pE{~0Zn zp?+erux2__DMeNERH_*4fcWH=l)R|+Es%?-5k}rX`EV^fP=2`<2$%VfoV9(=mU+V& ztwn~Q>fI`0<3u`8_Y5?U%QfDxBXMBksXwll*YAbLYr*5UPFI4*t@{DTM*Ke@APvO& z{6E^!$lh!3REPH)!~4r~hIgpq9jXuSqHNt0Y9P+_J_8eu%`;y-cOz99eW^P7k}>+y zEy09JD(f^blSGnRYDet60snAPhn~{Ha;}3#25zwY}YDH+J40;za@FT8eg~n$x`|o0% zwapF((SCM`={8jcOmh*9BApmO^U{e4G_PfL42)x)*NI&># zIghM$j*`9qKi792ZOsNQ1R^Q;@+`H_1WPU6bToZ&arb;$$;S13d|B2rr{hvuPsf*W zK1Ur2jGBnc7(X8$!8_qa5`t+58Caj`!1CEu8RER-O^dPo0`UQ+1N!6(vf6eoCcmQP zRg=%=(-IygeK>GcnCR5>E?F^xMseIv9%I9!be^rq!eG;hA++RGO*R=>BiN#z1&@(_ zZo2jqS$i?uY4L6O)6h?Q3ow5X)grO7qrvvOrtgX2Py6qR<9EdIYx}N0dvocV_bcLf zRXl2lN6Sa<`68bV-SzFh)t$H6{iewszPsAn>hLCM*;Z;KbxosyJ0RDSz(jh^H0>oLpO&j;vwq>PZc1q5r9r? zUq3wkQG9j~;0Yu)mj%L|)v3zKw;B9OS;8_7eUe{~~hSMjY_BIfn2& z`I9S< zB`2IsBZHuc55W7HZbeg6O;4*C+2ke2(yegsC2cNZ?#j(-9X|_AW;qMF#>za zb9IkUJ_rq$4}WsF8W=MIW97N;g~5%nFZimk%Mf-o5aT+*-1CoAMi;C8CBwf|@h*KY zL;#+fV^v|dA?&U=cjIw*Xct<@;wf54jDqA6Z9@6;tJlA;vl4R&blZ-RbG?Y+N*w67 zBgRQRdiy<7;y`!YLRVNO@D0HKA@N6-(J5Hc2f=lw#IzwSp>ybK4@g?T{n}q*{=|M1 zg6SCI%ONQvKE1!NdNQkINN$!|1;1z~j3Pk`J2`E7nrfn%I@A^mX_(wJmRQ-Dl{~i)Ow%)zIbNouFVT0-e zvL8PXQua2yNZdg{`>qArx2yhl4F5Y7?>luNR&mDYE|}aIMe2epffI#m9hk?%wbvm5 ztXo)tfvpRYt=?pf!1l=F%s^rw34@v1XCZKm|J&SwE|cI&%=Re{H4WRsQi7qbrZ{mz zO`pulG!0QyDW2|BJvrj&@mw0x6Ip9zA{;&nFKW7|JOw-1g^Aao4Zlh{csh071qnO? z?ZwzHpy1cgLHh#`nl#*vOx%e~R3no{WU}n7g`=0%Pt?u#Z#+{CA27lP%AUFqZ6K%1 zUytp)8=Jlpo4y&W#HOpULq_aS`B*JF^w-HhPhQ(`L#sw#FrqJ%U#ka(NVNCU6U3{x zo~Z<1weFr1M8kt%P%7u$Hn@a-zwA-LTYM(+rtoAX~D{oB!NtJSd?V{E1> z>^Fq{73Y4cEegUeUA1D86#^jp2P2z9ANW86TssVum@_o*!F-8ZXW=x%eB>{&Bm}^R zI84?E`$7;-cMp^N3mgjg#;y&)uYU{T>!D!H=kqy3x>ftEBH?WhWieXC1vo*p0 zQE_AOju5X1@!HVv$DW!P`q&2UTpaEKWWq6z12D}J4gy`kAwm<(d?G{+;%Yi4r&6Xn zmCEI%)hyw?snprkbhg>TX*xFfllgqs2E6ASL9ap%Kq{vGs%Z+A&<)Ib0hW zsSU(x6VtWGAkao&*ajzbL}57?T(~l6_!13)%0=W4HGGuyqwvtiiV;e5KFxY&lg=}uZF*z2fuQImV;qlZ>2p&lmf^dlO@*>6adq@^@}{>gdJ zGJ-=`0U7bq8hqwQ4US=$8uDDEzZwcvw(c6*S?RuOC|c>hYiOeKpY9qOu5{mZG*;Q} luAzOE?z@KOD%;&PG+ybx8+Mgp08QJ|2UPyUV_NAh{Tsf_;IaS! literal 0 HcmV?d00001 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/types.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/types.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39ef6cf0191c9be62025b560d44ec2d98619a58b GIT binary patch literal 396 zcmZ`#y-UMD6u--*AGEZi;N~QBXb+?*2qI`5M8QQt$WqeYrA@e`m%D3CI(6*e&`lS? zP5f^h93)f0$xZ0i$vd!%;QM}$-}~Ub)M^!=>3Qus+qn10aAv1|sOf z$a1X+xd;M^s7;}7?QVO3Tc8%P{?v*DU4w@GWl#;2qmwwx%UzE#ug_?1p9KTex@wg2 zqRMy>4~5w*Y-0-AsA$%U)aYLB~m^hmmMIGsR~KzQH`;P34IpRn3kw8TDod3D(0g&(vrf3e-| KUNtPLddv^D)NC^V literal 0 HcmV?d00001 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/utils.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd585dc4183bd85e7a1fa105e0b2594b651c5cc1 GIT binary patch literal 6924 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 literal 0 HcmV?d00001 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/warnings.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/warnings.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..141bba44f71c15afe99c0cf3cbaf0f45241a5999 GIT binary patch literal 469 zcmZusJxc>Y5S_hKyhNj5VP|8PA}(ky;0IO#u?eDrkY+h^o68;dvA5Z~c&SqOA5sZc ziuh+NC9QuT60211-X%6V%g(%+c|7L5R4QeV@svDvPh|a6%`#ROV74d07&JHs8y>($ z6rOc}H~EWbBU`(Ms@_=+R7Hzswkg3FC`cBxH5*B$HrctNL6lPaBD~DJfjGRic<6>s ztEx{7LP9H`giOkW1d*Ni3U3e+CzfAqUSX!x=MHAvO)WujYH{3+e4Fw<_8gpr@jdst zo>}jcx?r7#8K*}9ubV@`hB3au%w;uUafgap!%TW)co1XXy~8XMy(q-;Bs?fi>hH6R zE30`@uKHUtjeg(9iZN<(W65YqciT= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + + +try: + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import padding + from cryptography.hazmat.primitives.asymmetric.ec import ( + ECDSA, + SECP256K1, + SECP256R1, + SECP384R1, + SECP521R1, + EllipticCurve, + EllipticCurvePrivateKey, + EllipticCurvePrivateNumbers, + EllipticCurvePublicKey, + EllipticCurvePublicNumbers, + ) + from cryptography.hazmat.primitives.asymmetric.ed448 import ( + Ed448PrivateKey, + Ed448PublicKey, + ) + from cryptography.hazmat.primitives.asymmetric.ed25519 import ( + Ed25519PrivateKey, + Ed25519PublicKey, + ) + from cryptography.hazmat.primitives.asymmetric.rsa import ( + RSAPrivateKey, + RSAPrivateNumbers, + RSAPublicKey, + RSAPublicNumbers, + rsa_crt_dmp1, + rsa_crt_dmq1, + rsa_crt_iqmp, + rsa_recover_prime_factors, + ) + from cryptography.hazmat.primitives.serialization import ( + Encoding, + NoEncryption, + PrivateFormat, + PublicFormat, + load_pem_private_key, + load_pem_public_key, + load_ssh_public_key, + ) + + has_crypto = True +except ModuleNotFoundError: + has_crypto = False + + +if TYPE_CHECKING: + # Type aliases for convenience in algorithms method signatures + AllowedRSAKeys = RSAPrivateKey | RSAPublicKey + AllowedECKeys = EllipticCurvePrivateKey | EllipticCurvePublicKey + AllowedOKPKeys = ( + Ed25519PrivateKey | Ed25519PublicKey | Ed448PrivateKey | Ed448PublicKey + ) + AllowedKeys = AllowedRSAKeys | AllowedECKeys | AllowedOKPKeys + AllowedPrivateKeys = ( + RSAPrivateKey | EllipticCurvePrivateKey | Ed25519PrivateKey | Ed448PrivateKey + ) + AllowedPublicKeys = ( + RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey | Ed448PublicKey + ) + + +requires_cryptography = { + "RS256", + "RS384", + "RS512", + "ES256", + "ES256K", + "ES384", + "ES521", + "ES512", + "PS256", + "PS384", + "PS512", + "EdDSA", +} + + +def get_default_algorithms() -> dict[str, Algorithm]: + """ + Returns the algorithms that are implemented by the library. + """ + default_algorithms = { + "none": NoneAlgorithm(), + "HS256": HMACAlgorithm(HMACAlgorithm.SHA256), + "HS384": HMACAlgorithm(HMACAlgorithm.SHA384), + "HS512": HMACAlgorithm(HMACAlgorithm.SHA512), + } + + if has_crypto: + default_algorithms.update( + { + "RS256": RSAAlgorithm(RSAAlgorithm.SHA256), + "RS384": RSAAlgorithm(RSAAlgorithm.SHA384), + "RS512": RSAAlgorithm(RSAAlgorithm.SHA512), + "ES256": ECAlgorithm(ECAlgorithm.SHA256), + "ES256K": ECAlgorithm(ECAlgorithm.SHA256), + "ES384": ECAlgorithm(ECAlgorithm.SHA384), + "ES521": ECAlgorithm(ECAlgorithm.SHA512), + "ES512": ECAlgorithm( + ECAlgorithm.SHA512 + ), # Backward compat for #219 fix + "PS256": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256), + "PS384": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384), + "PS512": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512), + "EdDSA": OKPAlgorithm(), + } + ) + + return default_algorithms + + +class Algorithm(ABC): + """ + The interface for an algorithm used to sign and verify tokens. + """ + + def compute_hash_digest(self, bytestr: bytes) -> bytes: + """ + Compute a hash digest using the specified algorithm's hash algorithm. + + If there is no hash algorithm, raises a NotImplementedError. + """ + # lookup self.hash_alg if defined in a way that mypy can understand + hash_alg = getattr(self, "hash_alg", None) + if hash_alg is None: + raise NotImplementedError + + if ( + has_crypto + and isinstance(hash_alg, type) + and issubclass(hash_alg, hashes.HashAlgorithm) + ): + digest = hashes.Hash(hash_alg(), backend=default_backend()) + digest.update(bytestr) + return bytes(digest.finalize()) + else: + return bytes(hash_alg(bytestr).digest()) + + @abstractmethod + def prepare_key(self, key: Any) -> Any: + """ + Performs necessary validation and conversions on the key and returns + the key value in the proper format for sign() and verify(). + """ + + @abstractmethod + def sign(self, msg: bytes, key: Any) -> bytes: + """ + Returns a digital signature for the specified message + using the specified key value. + """ + + @abstractmethod + def verify(self, msg: bytes, key: Any, sig: bytes) -> bool: + """ + Verifies that the specified digital signature is valid + for the specified message and key values. + """ + + @overload + @staticmethod + @abstractmethod + def to_jwk(key_obj, as_dict: Literal[True]) -> JWKDict: + ... # pragma: no cover + + @overload + @staticmethod + @abstractmethod + def to_jwk(key_obj, as_dict: Literal[False] = False) -> str: + ... # pragma: no cover + + @staticmethod + @abstractmethod + def to_jwk(key_obj, as_dict: bool = False) -> Union[JWKDict, str]: + """ + Serializes a given key into a JWK + """ + + @staticmethod + @abstractmethod + def from_jwk(jwk: str | JWKDict) -> Any: + """ + Deserializes a given key from JWK back into a key object + """ + + +class NoneAlgorithm(Algorithm): + """ + Placeholder for use when no signing or verification + operations are required. + """ + + def prepare_key(self, key: str | None) -> None: + if key == "": + key = None + + if key is not None: + raise InvalidKeyError('When alg = "none", key value must be None.') + + return key + + def sign(self, msg: bytes, key: None) -> bytes: + return b"" + + def verify(self, msg: bytes, key: None, sig: bytes) -> bool: + return False + + @staticmethod + def to_jwk(key_obj: Any, as_dict: bool = False) -> NoReturn: + raise NotImplementedError() + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> NoReturn: + raise NotImplementedError() + + +class HMACAlgorithm(Algorithm): + """ + Performs signing and verification operations using HMAC + and the specified hash function. + """ + + SHA256: ClassVar[HashlibHash] = hashlib.sha256 + SHA384: ClassVar[HashlibHash] = hashlib.sha384 + SHA512: ClassVar[HashlibHash] = hashlib.sha512 + + def __init__(self, hash_alg: HashlibHash) -> None: + self.hash_alg = hash_alg + + def prepare_key(self, key: str | bytes) -> bytes: + key_bytes = force_bytes(key) + + if is_pem_format(key_bytes) or is_ssh_key(key_bytes): + raise InvalidKeyError( + "The specified key is an asymmetric key or x509 certificate and" + " should not be used as an HMAC secret." + ) + + return key_bytes + + @overload + @staticmethod + def to_jwk(key_obj: str | bytes, as_dict: Literal[True]) -> JWKDict: + ... # pragma: no cover + + @overload + @staticmethod + def to_jwk(key_obj: str | bytes, as_dict: Literal[False] = False) -> str: + ... # pragma: no cover + + @staticmethod + def to_jwk(key_obj: str | bytes, as_dict: bool = False) -> Union[JWKDict, str]: + jwk = { + "k": base64url_encode(force_bytes(key_obj)).decode(), + "kty": "oct", + } + + if as_dict: + return jwk + else: + return json.dumps(jwk) + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> bytes: + try: + if isinstance(jwk, str): + obj: JWKDict = json.loads(jwk) + elif isinstance(jwk, dict): + obj = jwk + else: + raise ValueError + except ValueError: + raise InvalidKeyError("Key is not valid JSON") + + if obj.get("kty") != "oct": + raise InvalidKeyError("Not an HMAC key") + + return base64url_decode(obj["k"]) + + def sign(self, msg: bytes, key: bytes) -> bytes: + return hmac.new(key, msg, self.hash_alg).digest() + + def verify(self, msg: bytes, key: bytes, sig: bytes) -> bool: + return hmac.compare_digest(sig, self.sign(msg, key)) + + +if has_crypto: + + class RSAAlgorithm(Algorithm): + """ + Performs signing and verification operations using + RSASSA-PKCS-v1_5 and the specified hash function. + """ + + SHA256: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA256 + SHA384: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA384 + SHA512: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA512 + + def __init__(self, hash_alg: type[hashes.HashAlgorithm]) -> None: + self.hash_alg = hash_alg + + def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys: + if isinstance(key, (RSAPrivateKey, RSAPublicKey)): + return key + + if not isinstance(key, (bytes, str)): + raise TypeError("Expecting a PEM-formatted key.") + + key_bytes = force_bytes(key) + + try: + if key_bytes.startswith(b"ssh-rsa"): + return cast(RSAPublicKey, load_ssh_public_key(key_bytes)) + else: + return cast( + RSAPrivateKey, load_pem_private_key(key_bytes, password=None) + ) + except ValueError: + return cast(RSAPublicKey, load_pem_public_key(key_bytes)) + + @overload + @staticmethod + def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[True]) -> JWKDict: + ... # pragma: no cover + + @overload + @staticmethod + def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[False] = False) -> str: + ... # pragma: no cover + + @staticmethod + def to_jwk( + key_obj: AllowedRSAKeys, as_dict: bool = False + ) -> Union[JWKDict, str]: + obj: dict[str, Any] | None = None + + if hasattr(key_obj, "private_numbers"): + # Private key + numbers = key_obj.private_numbers() + + obj = { + "kty": "RSA", + "key_ops": ["sign"], + "n": to_base64url_uint(numbers.public_numbers.n).decode(), + "e": to_base64url_uint(numbers.public_numbers.e).decode(), + "d": to_base64url_uint(numbers.d).decode(), + "p": to_base64url_uint(numbers.p).decode(), + "q": to_base64url_uint(numbers.q).decode(), + "dp": to_base64url_uint(numbers.dmp1).decode(), + "dq": to_base64url_uint(numbers.dmq1).decode(), + "qi": to_base64url_uint(numbers.iqmp).decode(), + } + + elif hasattr(key_obj, "verify"): + # Public key + numbers = key_obj.public_numbers() + + obj = { + "kty": "RSA", + "key_ops": ["verify"], + "n": to_base64url_uint(numbers.n).decode(), + "e": to_base64url_uint(numbers.e).decode(), + } + else: + raise InvalidKeyError("Not a public or private key") + + if as_dict: + return obj + else: + return json.dumps(obj) + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys: + try: + if isinstance(jwk, str): + obj = json.loads(jwk) + elif isinstance(jwk, dict): + obj = jwk + else: + raise ValueError + except ValueError: + raise InvalidKeyError("Key is not valid JSON") + + if obj.get("kty") != "RSA": + raise InvalidKeyError("Not an RSA key") + + if "d" in obj and "e" in obj and "n" in obj: + # Private key + if "oth" in obj: + raise InvalidKeyError( + "Unsupported RSA private key: > 2 primes not supported" + ) + + other_props = ["p", "q", "dp", "dq", "qi"] + props_found = [prop in obj for prop in other_props] + any_props_found = any(props_found) + + if any_props_found and not all(props_found): + raise InvalidKeyError( + "RSA key must include all parameters if any are present besides d" + ) + + public_numbers = RSAPublicNumbers( + from_base64url_uint(obj["e"]), + from_base64url_uint(obj["n"]), + ) + + if any_props_found: + numbers = RSAPrivateNumbers( + d=from_base64url_uint(obj["d"]), + p=from_base64url_uint(obj["p"]), + q=from_base64url_uint(obj["q"]), + dmp1=from_base64url_uint(obj["dp"]), + dmq1=from_base64url_uint(obj["dq"]), + iqmp=from_base64url_uint(obj["qi"]), + public_numbers=public_numbers, + ) + else: + d = from_base64url_uint(obj["d"]) + p, q = rsa_recover_prime_factors( + public_numbers.n, d, public_numbers.e + ) + + numbers = RSAPrivateNumbers( + d=d, + p=p, + q=q, + dmp1=rsa_crt_dmp1(d, p), + dmq1=rsa_crt_dmq1(d, q), + iqmp=rsa_crt_iqmp(p, q), + public_numbers=public_numbers, + ) + + return numbers.private_key() + elif "n" in obj and "e" in obj: + # Public key + return RSAPublicNumbers( + from_base64url_uint(obj["e"]), + from_base64url_uint(obj["n"]), + ).public_key() + else: + raise InvalidKeyError("Not a public or private key") + + def sign(self, msg: bytes, key: RSAPrivateKey) -> bytes: + return key.sign(msg, padding.PKCS1v15(), self.hash_alg()) + + def verify(self, msg: bytes, key: RSAPublicKey, sig: bytes) -> bool: + try: + key.verify(sig, msg, padding.PKCS1v15(), self.hash_alg()) + return True + except InvalidSignature: + return False + + class ECAlgorithm(Algorithm): + """ + Performs signing and verification operations using + ECDSA and the specified hash function + """ + + SHA256: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA256 + SHA384: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA384 + SHA512: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA512 + + def __init__(self, hash_alg: type[hashes.HashAlgorithm]) -> None: + self.hash_alg = hash_alg + + def prepare_key(self, key: AllowedECKeys | str | bytes) -> AllowedECKeys: + if isinstance(key, (EllipticCurvePrivateKey, EllipticCurvePublicKey)): + return key + + if not isinstance(key, (bytes, str)): + raise TypeError("Expecting a PEM-formatted key.") + + key_bytes = force_bytes(key) + + # Attempt to load key. We don't know if it's + # a Signing Key or a Verifying Key, so we try + # the Verifying Key first. + try: + if key_bytes.startswith(b"ecdsa-sha2-"): + crypto_key = load_ssh_public_key(key_bytes) + else: + crypto_key = load_pem_public_key(key_bytes) # type: ignore[assignment] + except ValueError: + crypto_key = load_pem_private_key(key_bytes, password=None) # type: ignore[assignment] + + # Explicit check the key to prevent confusing errors from cryptography + if not isinstance( + crypto_key, (EllipticCurvePrivateKey, EllipticCurvePublicKey) + ): + raise InvalidKeyError( + "Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for ECDSA algorithms" + ) + + return crypto_key + + def sign(self, msg: bytes, key: EllipticCurvePrivateKey) -> bytes: + der_sig = key.sign(msg, ECDSA(self.hash_alg())) + + return der_to_raw_signature(der_sig, key.curve) + + def verify(self, msg: bytes, key: "AllowedECKeys", sig: bytes) -> bool: + try: + der_sig = raw_to_der_signature(sig, key.curve) + except ValueError: + return False + + try: + public_key = ( + key.public_key() + if isinstance(key, EllipticCurvePrivateKey) + else key + ) + public_key.verify(der_sig, msg, ECDSA(self.hash_alg())) + return True + except InvalidSignature: + return False + + @overload + @staticmethod + def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[True]) -> JWKDict: + ... # pragma: no cover + + @overload + @staticmethod + def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[False] = False) -> str: + ... # pragma: no cover + + @staticmethod + def to_jwk( + key_obj: AllowedECKeys, as_dict: bool = False + ) -> Union[JWKDict, str]: + if isinstance(key_obj, EllipticCurvePrivateKey): + public_numbers = key_obj.public_key().public_numbers() + elif isinstance(key_obj, EllipticCurvePublicKey): + public_numbers = key_obj.public_numbers() + else: + raise InvalidKeyError("Not a public or private key") + + if isinstance(key_obj.curve, SECP256R1): + crv = "P-256" + elif isinstance(key_obj.curve, SECP384R1): + crv = "P-384" + elif isinstance(key_obj.curve, SECP521R1): + crv = "P-521" + elif isinstance(key_obj.curve, SECP256K1): + crv = "secp256k1" + else: + raise InvalidKeyError(f"Invalid curve: {key_obj.curve}") + + obj: dict[str, Any] = { + "kty": "EC", + "crv": crv, + "x": to_base64url_uint(public_numbers.x).decode(), + "y": to_base64url_uint(public_numbers.y).decode(), + } + + if isinstance(key_obj, EllipticCurvePrivateKey): + obj["d"] = to_base64url_uint( + key_obj.private_numbers().private_value + ).decode() + + if as_dict: + return obj + else: + return json.dumps(obj) + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> AllowedECKeys: + try: + if isinstance(jwk, str): + obj = json.loads(jwk) + elif isinstance(jwk, dict): + obj = jwk + else: + raise ValueError + except ValueError: + raise InvalidKeyError("Key is not valid JSON") + + if obj.get("kty") != "EC": + raise InvalidKeyError("Not an Elliptic curve key") + + if "x" not in obj or "y" not in obj: + raise InvalidKeyError("Not an Elliptic curve key") + + x = base64url_decode(obj.get("x")) + y = base64url_decode(obj.get("y")) + + curve = obj.get("crv") + curve_obj: EllipticCurve + + if curve == "P-256": + if len(x) == len(y) == 32: + curve_obj = SECP256R1() + else: + raise InvalidKeyError("Coords should be 32 bytes for curve P-256") + elif curve == "P-384": + if len(x) == len(y) == 48: + curve_obj = SECP384R1() + else: + raise InvalidKeyError("Coords should be 48 bytes for curve P-384") + elif curve == "P-521": + if len(x) == len(y) == 66: + curve_obj = SECP521R1() + else: + raise InvalidKeyError("Coords should be 66 bytes for curve P-521") + elif curve == "secp256k1": + if len(x) == len(y) == 32: + curve_obj = SECP256K1() + else: + raise InvalidKeyError( + "Coords should be 32 bytes for curve secp256k1" + ) + else: + raise InvalidKeyError(f"Invalid curve: {curve}") + + public_numbers = EllipticCurvePublicNumbers( + x=int.from_bytes(x, byteorder="big"), + y=int.from_bytes(y, byteorder="big"), + curve=curve_obj, + ) + + if "d" not in obj: + return public_numbers.public_key() + + d = base64url_decode(obj.get("d")) + if len(d) != len(x): + raise InvalidKeyError( + "D should be {} bytes for curve {}", len(x), curve + ) + + return EllipticCurvePrivateNumbers( + int.from_bytes(d, byteorder="big"), public_numbers + ).private_key() + + class RSAPSSAlgorithm(RSAAlgorithm): + """ + Performs a signature using RSASSA-PSS with MGF1 + """ + + def sign(self, msg: bytes, key: RSAPrivateKey) -> bytes: + return key.sign( + msg, + padding.PSS( + mgf=padding.MGF1(self.hash_alg()), + salt_length=self.hash_alg().digest_size, + ), + self.hash_alg(), + ) + + def verify(self, msg: bytes, key: RSAPublicKey, sig: bytes) -> bool: + try: + key.verify( + sig, + msg, + padding.PSS( + mgf=padding.MGF1(self.hash_alg()), + salt_length=self.hash_alg().digest_size, + ), + self.hash_alg(), + ) + return True + except InvalidSignature: + return False + + class OKPAlgorithm(Algorithm): + """ + Performs signing and verification operations using EdDSA + + This class requires ``cryptography>=2.6`` to be installed. + """ + + def __init__(self, **kwargs: Any) -> None: + pass + + def prepare_key(self, key: AllowedOKPKeys | str | bytes) -> AllowedOKPKeys: + if isinstance(key, (bytes, str)): + key_str = key.decode("utf-8") if isinstance(key, bytes) else key + key_bytes = key.encode("utf-8") if isinstance(key, str) else key + + if "-----BEGIN PUBLIC" in key_str: + key = load_pem_public_key(key_bytes) # type: ignore[assignment] + elif "-----BEGIN PRIVATE" in key_str: + key = load_pem_private_key(key_bytes, password=None) # type: ignore[assignment] + elif key_str[0:4] == "ssh-": + key = load_ssh_public_key(key_bytes) # type: ignore[assignment] + + # Explicit check the key to prevent confusing errors from cryptography + if not isinstance( + key, + (Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey), + ): + raise InvalidKeyError( + "Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for EdDSA algorithms" + ) + + return key + + def sign( + self, msg: str | bytes, key: Ed25519PrivateKey | Ed448PrivateKey + ) -> bytes: + """ + Sign a message ``msg`` using the EdDSA private key ``key`` + :param str|bytes msg: Message to sign + :param Ed25519PrivateKey}Ed448PrivateKey key: A :class:`.Ed25519PrivateKey` + or :class:`.Ed448PrivateKey` isinstance + :return bytes signature: The signature, as bytes + """ + msg_bytes = msg.encode("utf-8") if isinstance(msg, str) else msg + return key.sign(msg_bytes) + + def verify( + self, msg: str | bytes, key: AllowedOKPKeys, sig: str | bytes + ) -> bool: + """ + Verify a given ``msg`` against a signature ``sig`` using the EdDSA key ``key`` + + :param str|bytes sig: EdDSA signature to check ``msg`` against + :param str|bytes msg: Message to sign + :param Ed25519PrivateKey|Ed25519PublicKey|Ed448PrivateKey|Ed448PublicKey key: + A private or public EdDSA key instance + :return bool verified: True if signature is valid, False if not. + """ + try: + msg_bytes = msg.encode("utf-8") if isinstance(msg, str) else msg + sig_bytes = sig.encode("utf-8") if isinstance(sig, str) else sig + + public_key = ( + key.public_key() + if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)) + else key + ) + public_key.verify(sig_bytes, msg_bytes) + return True # If no exception was raised, the signature is valid. + except InvalidSignature: + return False + + @overload + @staticmethod + def to_jwk(key: AllowedOKPKeys, as_dict: Literal[True]) -> JWKDict: + ... # pragma: no cover + + @overload + @staticmethod + def to_jwk(key: AllowedOKPKeys, as_dict: Literal[False] = False) -> str: + ... # pragma: no cover + + @staticmethod + def to_jwk(key: AllowedOKPKeys, as_dict: bool = False) -> Union[JWKDict, str]: + if isinstance(key, (Ed25519PublicKey, Ed448PublicKey)): + x = key.public_bytes( + encoding=Encoding.Raw, + format=PublicFormat.Raw, + ) + crv = "Ed25519" if isinstance(key, Ed25519PublicKey) else "Ed448" + + obj = { + "x": base64url_encode(force_bytes(x)).decode(), + "kty": "OKP", + "crv": crv, + } + + if as_dict: + return obj + else: + return json.dumps(obj) + + if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)): + d = key.private_bytes( + encoding=Encoding.Raw, + format=PrivateFormat.Raw, + encryption_algorithm=NoEncryption(), + ) + + x = key.public_key().public_bytes( + encoding=Encoding.Raw, + format=PublicFormat.Raw, + ) + + crv = "Ed25519" if isinstance(key, Ed25519PrivateKey) else "Ed448" + obj = { + "x": base64url_encode(force_bytes(x)).decode(), + "d": base64url_encode(force_bytes(d)).decode(), + "kty": "OKP", + "crv": crv, + } + + if as_dict: + return obj + else: + return json.dumps(obj) + + raise InvalidKeyError("Not a public or private key") + + @staticmethod + def from_jwk(jwk: str | JWKDict) -> AllowedOKPKeys: + try: + if isinstance(jwk, str): + obj = json.loads(jwk) + elif isinstance(jwk, dict): + obj = jwk + else: + raise ValueError + except ValueError: + raise InvalidKeyError("Key is not valid JSON") + + if obj.get("kty") != "OKP": + raise InvalidKeyError("Not an Octet Key Pair") + + curve = obj.get("crv") + if curve != "Ed25519" and curve != "Ed448": + raise InvalidKeyError(f"Invalid curve: {curve}") + + if "x" not in obj: + raise InvalidKeyError('OKP should have "x" parameter') + x = base64url_decode(obj.get("x")) + + try: + if "d" not in obj: + if curve == "Ed25519": + return Ed25519PublicKey.from_public_bytes(x) + return Ed448PublicKey.from_public_bytes(x) + d = base64url_decode(obj.get("d")) + if curve == "Ed25519": + return Ed25519PrivateKey.from_private_bytes(d) + return Ed448PrivateKey.from_private_bytes(d) + except ValueError as err: + raise InvalidKeyError("Invalid key parameter") from err diff --git a/apigw-dynamodb-python-cdk/src/jwt/api_jwk.py b/apigw-dynamodb-python-cdk/src/jwt/api_jwk.py new file mode 100644 index 000000000..456c7f4d8 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/api_jwk.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import json +import time +from typing import Any + +from .algorithms import get_default_algorithms, has_crypto, requires_cryptography +from .exceptions import InvalidKeyError, PyJWKError, PyJWKSetError, PyJWTError +from .types import JWKDict + + +class PyJWK: + def __init__(self, jwk_data: JWKDict, algorithm: str | None = None) -> None: + self._algorithms = get_default_algorithms() + self._jwk_data = jwk_data + + kty = self._jwk_data.get("kty", None) + if not kty: + raise InvalidKeyError(f"kty is not found: {self._jwk_data}") + + if not algorithm and isinstance(self._jwk_data, dict): + algorithm = self._jwk_data.get("alg", None) + + if not algorithm: + # Determine alg with kty (and crv). + crv = self._jwk_data.get("crv", None) + if kty == "EC": + if crv == "P-256" or not crv: + algorithm = "ES256" + elif crv == "P-384": + algorithm = "ES384" + elif crv == "P-521": + algorithm = "ES512" + elif crv == "secp256k1": + algorithm = "ES256K" + else: + raise InvalidKeyError(f"Unsupported crv: {crv}") + elif kty == "RSA": + algorithm = "RS256" + elif kty == "oct": + algorithm = "HS256" + elif kty == "OKP": + if not crv: + raise InvalidKeyError(f"crv is not found: {self._jwk_data}") + if crv == "Ed25519": + algorithm = "EdDSA" + else: + raise InvalidKeyError(f"Unsupported crv: {crv}") + else: + raise InvalidKeyError(f"Unsupported kty: {kty}") + + if not has_crypto and algorithm in requires_cryptography: + raise PyJWKError(f"{algorithm} requires 'cryptography' to be installed.") + + self.Algorithm = self._algorithms.get(algorithm) + + if not self.Algorithm: + raise PyJWKError(f"Unable to find an algorithm for key: {self._jwk_data}") + + self.key = self.Algorithm.from_jwk(self._jwk_data) + + @staticmethod + def from_dict(obj: JWKDict, algorithm: str | None = None) -> "PyJWK": + return PyJWK(obj, algorithm) + + @staticmethod + def from_json(data: str, algorithm: None = None) -> "PyJWK": + obj = json.loads(data) + return PyJWK.from_dict(obj, algorithm) + + @property + def key_type(self) -> str | None: + return self._jwk_data.get("kty", None) + + @property + def key_id(self) -> str | None: + return self._jwk_data.get("kid", None) + + @property + def public_key_use(self) -> str | None: + return self._jwk_data.get("use", None) + + +class PyJWKSet: + def __init__(self, keys: list[JWKDict]) -> None: + self.keys = [] + + if not keys: + raise PyJWKSetError("The JWK Set did not contain any keys") + + if not isinstance(keys, list): + raise PyJWKSetError("Invalid JWK Set value") + + for key in keys: + try: + self.keys.append(PyJWK(key)) + except PyJWTError: + # skip unusable keys + continue + + if len(self.keys) == 0: + raise PyJWKSetError( + "The JWK Set did not contain any usable keys. Perhaps 'cryptography' is not installed?" + ) + + @staticmethod + def from_dict(obj: dict[str, Any]) -> "PyJWKSet": + keys = obj.get("keys", []) + return PyJWKSet(keys) + + @staticmethod + def from_json(data: str) -> "PyJWKSet": + obj = json.loads(data) + return PyJWKSet.from_dict(obj) + + def __getitem__(self, kid: str) -> "PyJWK": + for key in self.keys: + if key.key_id == kid: + return key + raise KeyError(f"keyset has no key for kid: {kid}") + + +class PyJWTSetWithTimestamp: + def __init__(self, jwk_set: PyJWKSet): + self.jwk_set = jwk_set + self.timestamp = time.monotonic() + + def get_jwk_set(self) -> PyJWKSet: + return self.jwk_set + + def get_timestamp(self) -> float: + return self.timestamp diff --git a/apigw-dynamodb-python-cdk/src/jwt/api_jws.py b/apigw-dynamodb-python-cdk/src/jwt/api_jws.py new file mode 100644 index 000000000..fa6708ccc --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/api_jws.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +import binascii +import json +import warnings +from typing import TYPE_CHECKING, Any + +from .algorithms import ( + Algorithm, + get_default_algorithms, + has_crypto, + requires_cryptography, +) +from .exceptions import ( + DecodeError, + InvalidAlgorithmError, + InvalidSignatureError, + InvalidTokenError, +) +from .utils import base64url_decode, base64url_encode +from .warnings import RemovedInPyjwt3Warning + +if TYPE_CHECKING: + from .algorithms import AllowedPrivateKeys, AllowedPublicKeys + + +class PyJWS: + header_typ = "JWT" + + def __init__( + self, + algorithms: list[str] | None = None, + options: dict[str, Any] | None = None, + ) -> None: + self._algorithms = get_default_algorithms() + self._valid_algs = ( + set(algorithms) if algorithms is not None else set(self._algorithms) + ) + + # Remove algorithms that aren't on the whitelist + for key in list(self._algorithms.keys()): + if key not in self._valid_algs: + del self._algorithms[key] + + if options is None: + options = {} + self.options = {**self._get_default_options(), **options} + + @staticmethod + def _get_default_options() -> dict[str, bool]: + return {"verify_signature": True} + + def register_algorithm(self, alg_id: str, alg_obj: Algorithm) -> None: + """ + Registers a new Algorithm for use when creating and verifying tokens. + """ + if alg_id in self._algorithms: + raise ValueError("Algorithm already has a handler.") + + if not isinstance(alg_obj, Algorithm): + raise TypeError("Object is not of type `Algorithm`") + + self._algorithms[alg_id] = alg_obj + self._valid_algs.add(alg_id) + + def unregister_algorithm(self, alg_id: str) -> None: + """ + Unregisters an Algorithm for use when creating and verifying tokens + Throws KeyError if algorithm is not registered. + """ + if alg_id not in self._algorithms: + raise KeyError( + "The specified algorithm could not be removed" + " because it is not registered." + ) + + del self._algorithms[alg_id] + self._valid_algs.remove(alg_id) + + def get_algorithms(self) -> list[str]: + """ + Returns a list of supported values for the 'alg' parameter. + """ + return list(self._valid_algs) + + def get_algorithm_by_name(self, alg_name: str) -> Algorithm: + """ + For a given string name, return the matching Algorithm object. + + Example usage: + + >>> jws_obj.get_algorithm_by_name("RS256") + """ + try: + return self._algorithms[alg_name] + except KeyError as e: + if not has_crypto and alg_name in requires_cryptography: + raise NotImplementedError( + f"Algorithm '{alg_name}' could not be found. Do you have cryptography installed?" + ) from e + raise NotImplementedError("Algorithm not supported") from e + + def encode( + self, + payload: bytes, + key: AllowedPrivateKeys | str | bytes, + algorithm: str | None = "HS256", + headers: dict[str, Any] | None = None, + json_encoder: type[json.JSONEncoder] | None = None, + is_payload_detached: bool = False, + sort_headers: bool = True, + ) -> str: + segments = [] + + # declare a new var to narrow the type for type checkers + algorithm_: str = algorithm if algorithm is not None else "none" + + # Prefer headers values if present to function parameters. + if headers: + headers_alg = headers.get("alg") + if headers_alg: + algorithm_ = headers["alg"] + + headers_b64 = headers.get("b64") + if headers_b64 is False: + is_payload_detached = True + + # Header + header: dict[str, Any] = {"typ": self.header_typ, "alg": algorithm_} + + if headers: + self._validate_headers(headers) + header.update(headers) + + if not header["typ"]: + del header["typ"] + + if is_payload_detached: + header["b64"] = False + elif "b64" in header: + # True is the standard value for b64, so no need for it + del header["b64"] + + json_header = json.dumps( + header, separators=(",", ":"), cls=json_encoder, sort_keys=sort_headers + ).encode() + + segments.append(base64url_encode(json_header)) + + if is_payload_detached: + msg_payload = payload + else: + msg_payload = base64url_encode(payload) + segments.append(msg_payload) + + # Segments + signing_input = b".".join(segments) + + alg_obj = self.get_algorithm_by_name(algorithm_) + key = alg_obj.prepare_key(key) + signature = alg_obj.sign(signing_input, key) + + segments.append(base64url_encode(signature)) + + # Don't put the payload content inside the encoded token when detached + if is_payload_detached: + segments[1] = b"" + encoded_string = b".".join(segments) + + return encoded_string.decode("utf-8") + + def decode_complete( + self, + jwt: str | bytes, + key: AllowedPublicKeys | str | bytes = "", + algorithms: list[str] | None = None, + options: dict[str, Any] | None = None, + detached_payload: bytes | None = None, + **kwargs, + ) -> dict[str, Any]: + if kwargs: + warnings.warn( + "passing additional kwargs to decode_complete() is deprecated " + "and will be removed in pyjwt version 3. " + f"Unsupported kwargs: {tuple(kwargs.keys())}", + RemovedInPyjwt3Warning, + ) + if options is None: + options = {} + merged_options = {**self.options, **options} + verify_signature = merged_options["verify_signature"] + + if verify_signature and not algorithms: + raise DecodeError( + 'It is required that you pass in a value for the "algorithms" argument when calling decode().' + ) + + payload, signing_input, header, signature = self._load(jwt) + + if header.get("b64", True) is False: + if detached_payload is None: + raise DecodeError( + 'It is required that you pass in a value for the "detached_payload" argument to decode a message having the b64 header set to false.' + ) + payload = detached_payload + signing_input = b".".join([signing_input.rsplit(b".", 1)[0], payload]) + + if verify_signature: + self._verify_signature(signing_input, header, signature, key, algorithms) + + return { + "payload": payload, + "header": header, + "signature": signature, + } + + def decode( + self, + jwt: str | bytes, + key: AllowedPublicKeys | str | bytes = "", + algorithms: list[str] | None = None, + options: dict[str, Any] | None = None, + detached_payload: bytes | None = None, + **kwargs, + ) -> Any: + if kwargs: + warnings.warn( + "passing additional kwargs to decode() is deprecated " + "and will be removed in pyjwt version 3. " + f"Unsupported kwargs: {tuple(kwargs.keys())}", + RemovedInPyjwt3Warning, + ) + decoded = self.decode_complete( + jwt, key, algorithms, options, detached_payload=detached_payload + ) + return decoded["payload"] + + def get_unverified_header(self, jwt: str | bytes) -> dict[str, Any]: + """Returns back the JWT header parameters as a dict() + + Note: The signature is not verified so the header parameters + should not be fully trusted until signature verification is complete + """ + headers = self._load(jwt)[2] + self._validate_headers(headers) + + return headers + + def _load(self, jwt: str | bytes) -> tuple[bytes, bytes, dict[str, Any], bytes]: + if isinstance(jwt, str): + jwt = jwt.encode("utf-8") + + if not isinstance(jwt, bytes): + raise DecodeError(f"Invalid token type. Token must be a {bytes}") + + try: + signing_input, crypto_segment = jwt.rsplit(b".", 1) + header_segment, payload_segment = signing_input.split(b".", 1) + except ValueError as err: + raise DecodeError("Not enough segments") from err + + try: + header_data = base64url_decode(header_segment) + except (TypeError, binascii.Error) as err: + raise DecodeError("Invalid header padding") from err + + try: + header = json.loads(header_data) + except ValueError as e: + raise DecodeError(f"Invalid header string: {e}") from e + + if not isinstance(header, dict): + raise DecodeError("Invalid header string: must be a json object") + + try: + payload = base64url_decode(payload_segment) + except (TypeError, binascii.Error) as err: + raise DecodeError("Invalid payload padding") from err + + try: + signature = base64url_decode(crypto_segment) + except (TypeError, binascii.Error) as err: + raise DecodeError("Invalid crypto padding") from err + + return (payload, signing_input, header, signature) + + def _verify_signature( + self, + signing_input: bytes, + header: dict[str, Any], + signature: bytes, + key: AllowedPublicKeys | str | bytes = "", + algorithms: list[str] | None = None, + ) -> None: + try: + alg = header["alg"] + except KeyError: + raise InvalidAlgorithmError("Algorithm not specified") + + if not alg or (algorithms is not None and alg not in algorithms): + raise InvalidAlgorithmError("The specified alg value is not allowed") + + try: + alg_obj = self.get_algorithm_by_name(alg) + except NotImplementedError as e: + raise InvalidAlgorithmError("Algorithm not supported") from e + prepared_key = alg_obj.prepare_key(key) + + if not alg_obj.verify(signing_input, prepared_key, signature): + raise InvalidSignatureError("Signature verification failed") + + def _validate_headers(self, headers: dict[str, Any]) -> None: + if "kid" in headers: + self._validate_kid(headers["kid"]) + + def _validate_kid(self, kid: Any) -> None: + if not isinstance(kid, str): + raise InvalidTokenError("Key ID header parameter must be a string") + + +_jws_global_obj = PyJWS() +encode = _jws_global_obj.encode +decode_complete = _jws_global_obj.decode_complete +decode = _jws_global_obj.decode +register_algorithm = _jws_global_obj.register_algorithm +unregister_algorithm = _jws_global_obj.unregister_algorithm +get_algorithm_by_name = _jws_global_obj.get_algorithm_by_name +get_unverified_header = _jws_global_obj.get_unverified_header diff --git a/apigw-dynamodb-python-cdk/src/jwt/api_jwt.py b/apigw-dynamodb-python-cdk/src/jwt/api_jwt.py new file mode 100644 index 000000000..48d739ad6 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/api_jwt.py @@ -0,0 +1,372 @@ +from __future__ import annotations + +import json +import warnings +from calendar import timegm +from collections.abc import Iterable +from datetime import datetime, timedelta, timezone +from typing import TYPE_CHECKING, Any + +from . import api_jws +from .exceptions import ( + DecodeError, + ExpiredSignatureError, + ImmatureSignatureError, + InvalidAudienceError, + InvalidIssuedAtError, + InvalidIssuerError, + MissingRequiredClaimError, +) +from .warnings import RemovedInPyjwt3Warning + +if TYPE_CHECKING: + from .algorithms import AllowedPrivateKeys, AllowedPublicKeys + + +class PyJWT: + def __init__(self, options: dict[str, Any] | None = None) -> None: + if options is None: + options = {} + self.options: dict[str, Any] = {**self._get_default_options(), **options} + + @staticmethod + def _get_default_options() -> dict[str, bool | list[str]]: + return { + "verify_signature": True, + "verify_exp": True, + "verify_nbf": True, + "verify_iat": True, + "verify_aud": True, + "verify_iss": True, + "require": [], + } + + def encode( + self, + payload: dict[str, Any], + key: AllowedPrivateKeys | str | bytes, + algorithm: str | None = "HS256", + headers: dict[str, Any] | None = None, + json_encoder: type[json.JSONEncoder] | None = None, + sort_headers: bool = True, + ) -> str: + # Check that we get a dict + if not isinstance(payload, dict): + raise TypeError( + "Expecting a dict object, as JWT only supports " + "JSON objects as payloads." + ) + + # Payload + payload = payload.copy() + for time_claim in ["exp", "iat", "nbf"]: + # Convert datetime to a intDate value in known time-format claims + if isinstance(payload.get(time_claim), datetime): + payload[time_claim] = timegm(payload[time_claim].utctimetuple()) + + json_payload = self._encode_payload( + payload, + headers=headers, + json_encoder=json_encoder, + ) + + return api_jws.encode( + json_payload, + key, + algorithm, + headers, + json_encoder, + sort_headers=sort_headers, + ) + + def _encode_payload( + self, + payload: dict[str, Any], + headers: dict[str, Any] | None = None, + json_encoder: type[json.JSONEncoder] | None = None, + ) -> bytes: + """ + Encode a given payload to the bytes to be signed. + + This method is intended to be overridden by subclasses that need to + encode the payload in a different way, e.g. compress the payload. + """ + return json.dumps( + payload, + separators=(",", ":"), + cls=json_encoder, + ).encode("utf-8") + + def decode_complete( + self, + jwt: str | bytes, + key: AllowedPublicKeys | str | bytes = "", + algorithms: list[str] | None = None, + options: dict[str, Any] | None = None, + # deprecated arg, remove in pyjwt3 + verify: bool | None = None, + # could be used as passthrough to api_jws, consider removal in pyjwt3 + detached_payload: bytes | None = None, + # passthrough arguments to _validate_claims + # consider putting in options + audience: str | Iterable[str] | None = None, + issuer: str | None = None, + leeway: float | timedelta = 0, + # kwargs + **kwargs: Any, + ) -> dict[str, Any]: + if kwargs: + warnings.warn( + "passing additional kwargs to decode_complete() is deprecated " + "and will be removed in pyjwt version 3. " + f"Unsupported kwargs: {tuple(kwargs.keys())}", + RemovedInPyjwt3Warning, + ) + options = dict(options or {}) # shallow-copy or initialize an empty dict + options.setdefault("verify_signature", True) + + # If the user has set the legacy `verify` argument, and it doesn't match + # what the relevant `options` entry for the argument is, inform the user + # that they're likely making a mistake. + if verify is not None and verify != options["verify_signature"]: + warnings.warn( + "The `verify` argument to `decode` does nothing in PyJWT 2.0 and newer. " + "The equivalent is setting `verify_signature` to False in the `options` dictionary. " + "This invocation has a mismatch between the kwarg and the option entry.", + category=DeprecationWarning, + ) + + if not options["verify_signature"]: + options.setdefault("verify_exp", False) + options.setdefault("verify_nbf", False) + options.setdefault("verify_iat", False) + options.setdefault("verify_aud", False) + options.setdefault("verify_iss", False) + + if options["verify_signature"] and not algorithms: + raise DecodeError( + 'It is required that you pass in a value for the "algorithms" argument when calling decode().' + ) + + decoded = api_jws.decode_complete( + jwt, + key=key, + algorithms=algorithms, + options=options, + detached_payload=detached_payload, + ) + + payload = self._decode_payload(decoded) + + merged_options = {**self.options, **options} + self._validate_claims( + payload, merged_options, audience=audience, issuer=issuer, leeway=leeway + ) + + decoded["payload"] = payload + return decoded + + def _decode_payload(self, decoded: dict[str, Any]) -> Any: + """ + Decode the payload from a JWS dictionary (payload, signature, header). + + This method is intended to be overridden by subclasses that need to + decode the payload in a different way, e.g. decompress compressed + payloads. + """ + try: + payload = json.loads(decoded["payload"]) + except ValueError as e: + raise DecodeError(f"Invalid payload string: {e}") + if not isinstance(payload, dict): + raise DecodeError("Invalid payload string: must be a json object") + return payload + + def decode( + self, + jwt: str | bytes, + key: AllowedPublicKeys | str | bytes = "", + algorithms: list[str] | None = None, + options: dict[str, Any] | None = None, + # deprecated arg, remove in pyjwt3 + verify: bool | None = None, + # could be used as passthrough to api_jws, consider removal in pyjwt3 + detached_payload: bytes | None = None, + # passthrough arguments to _validate_claims + # consider putting in options + audience: str | Iterable[str] | None = None, + issuer: str | None = None, + leeway: float | timedelta = 0, + # kwargs + **kwargs: Any, + ) -> Any: + if kwargs: + warnings.warn( + "passing additional kwargs to decode() is deprecated " + "and will be removed in pyjwt version 3. " + f"Unsupported kwargs: {tuple(kwargs.keys())}", + RemovedInPyjwt3Warning, + ) + decoded = self.decode_complete( + jwt, + key, + algorithms, + options, + verify=verify, + detached_payload=detached_payload, + audience=audience, + issuer=issuer, + leeway=leeway, + ) + return decoded["payload"] + + def _validate_claims( + self, + payload: dict[str, Any], + options: dict[str, Any], + audience=None, + issuer=None, + leeway: float | timedelta = 0, + ) -> None: + if isinstance(leeway, timedelta): + leeway = leeway.total_seconds() + + if audience is not None and not isinstance(audience, (str, Iterable)): + raise TypeError("audience must be a string, iterable or None") + + self._validate_required_claims(payload, options) + + now = datetime.now(tz=timezone.utc).timestamp() + + if "iat" in payload and options["verify_iat"]: + self._validate_iat(payload, now, leeway) + + if "nbf" in payload and options["verify_nbf"]: + self._validate_nbf(payload, now, leeway) + + if "exp" in payload and options["verify_exp"]: + self._validate_exp(payload, now, leeway) + + if options["verify_iss"]: + self._validate_iss(payload, issuer) + + if options["verify_aud"]: + self._validate_aud( + payload, audience, strict=options.get("strict_aud", False) + ) + + def _validate_required_claims( + self, + payload: dict[str, Any], + options: dict[str, Any], + ) -> None: + for claim in options["require"]: + if payload.get(claim) is None: + raise MissingRequiredClaimError(claim) + + def _validate_iat( + self, + payload: dict[str, Any], + now: float, + leeway: float, + ) -> None: + try: + iat = int(payload["iat"]) + except ValueError: + raise InvalidIssuedAtError("Issued At claim (iat) must be an integer.") + if iat > (now + leeway): + raise ImmatureSignatureError("The token is not yet valid (iat)") + + def _validate_nbf( + self, + payload: dict[str, Any], + now: float, + leeway: float, + ) -> None: + try: + nbf = int(payload["nbf"]) + except ValueError: + raise DecodeError("Not Before claim (nbf) must be an integer.") + + if nbf > (now + leeway): + raise ImmatureSignatureError("The token is not yet valid (nbf)") + + def _validate_exp( + self, + payload: dict[str, Any], + now: float, + leeway: float, + ) -> None: + try: + exp = int(payload["exp"]) + except ValueError: + raise DecodeError("Expiration Time claim (exp) must be an" " integer.") + + if exp <= (now - leeway): + raise ExpiredSignatureError("Signature has expired") + + def _validate_aud( + self, + payload: dict[str, Any], + audience: str | Iterable[str] | None, + *, + strict: bool = False, + ) -> None: + if audience is None: + if "aud" not in payload or not payload["aud"]: + return + # Application did not specify an audience, but + # the token has the 'aud' claim + raise InvalidAudienceError("Invalid audience") + + if "aud" not in payload or not payload["aud"]: + # Application specified an audience, but it could not be + # verified since the token does not contain a claim. + raise MissingRequiredClaimError("aud") + + audience_claims = payload["aud"] + + # In strict mode, we forbid list matching: the supplied audience + # must be a string, and it must exactly match the audience claim. + if strict: + # Only a single audience is allowed in strict mode. + if not isinstance(audience, str): + raise InvalidAudienceError("Invalid audience (strict)") + + # Only a single audience claim is allowed in strict mode. + if not isinstance(audience_claims, str): + raise InvalidAudienceError("Invalid claim format in token (strict)") + + if audience != audience_claims: + raise InvalidAudienceError("Audience doesn't match (strict)") + + return + + if isinstance(audience_claims, str): + audience_claims = [audience_claims] + if not isinstance(audience_claims, list): + raise InvalidAudienceError("Invalid claim format in token") + if any(not isinstance(c, str) for c in audience_claims): + raise InvalidAudienceError("Invalid claim format in token") + + if isinstance(audience, str): + audience = [audience] + + if all(aud not in audience_claims for aud in audience): + raise InvalidAudienceError("Audience doesn't match") + + def _validate_iss(self, payload: dict[str, Any], issuer: Any) -> None: + if issuer is None: + return + + if "iss" not in payload: + raise MissingRequiredClaimError("iss") + + if payload["iss"] != issuer: + raise InvalidIssuerError("Invalid issuer") + + +_jwt_global_obj = PyJWT() +encode = _jwt_global_obj.encode +decode_complete = _jwt_global_obj.decode_complete +decode = _jwt_global_obj.decode diff --git a/apigw-dynamodb-python-cdk/src/jwt/exceptions.py b/apigw-dynamodb-python-cdk/src/jwt/exceptions.py new file mode 100644 index 000000000..8ac6ecf74 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/exceptions.py @@ -0,0 +1,70 @@ +class PyJWTError(Exception): + """ + Base class for all exceptions + """ + + pass + + +class InvalidTokenError(PyJWTError): + pass + + +class DecodeError(InvalidTokenError): + pass + + +class InvalidSignatureError(DecodeError): + pass + + +class ExpiredSignatureError(InvalidTokenError): + pass + + +class InvalidAudienceError(InvalidTokenError): + pass + + +class InvalidIssuerError(InvalidTokenError): + pass + + +class InvalidIssuedAtError(InvalidTokenError): + pass + + +class ImmatureSignatureError(InvalidTokenError): + pass + + +class InvalidKeyError(PyJWTError): + pass + + +class InvalidAlgorithmError(InvalidTokenError): + pass + + +class MissingRequiredClaimError(InvalidTokenError): + def __init__(self, claim: str) -> None: + self.claim = claim + + def __str__(self) -> str: + return f'Token is missing the "{self.claim}" claim' + + +class PyJWKError(PyJWTError): + pass + + +class PyJWKSetError(PyJWTError): + pass + + +class PyJWKClientError(PyJWTError): + pass + + +class PyJWKClientConnectionError(PyJWKClientError): + pass diff --git a/apigw-dynamodb-python-cdk/src/jwt/help.py b/apigw-dynamodb-python-cdk/src/jwt/help.py new file mode 100644 index 000000000..80b0ca56e --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/help.py @@ -0,0 +1,64 @@ +import json +import platform +import sys +from typing import Dict + +from . import __version__ as pyjwt_version + +try: + import cryptography + + cryptography_version = cryptography.__version__ +except ModuleNotFoundError: + cryptography_version = "" + + +def info() -> Dict[str, Dict[str, str]]: + """ + Generate information for a bug report. + Based on the requests package help utility module. + """ + try: + platform_info = { + "system": platform.system(), + "release": platform.release(), + } + except OSError: + platform_info = {"system": "Unknown", "release": "Unknown"} + + implementation = platform.python_implementation() + + if implementation == "CPython": + implementation_version = platform.python_version() + elif implementation == "PyPy": + pypy_version_info = sys.pypy_version_info # type: ignore[attr-defined] + implementation_version = ( + f"{pypy_version_info.major}." + f"{pypy_version_info.minor}." + f"{pypy_version_info.micro}" + ) + if pypy_version_info.releaselevel != "final": + implementation_version = "".join( + [implementation_version, pypy_version_info.releaselevel] + ) + else: + implementation_version = "Unknown" + + return { + "platform": platform_info, + "implementation": { + "name": implementation, + "version": implementation_version, + }, + "cryptography": {"version": cryptography_version}, + "pyjwt": {"version": pyjwt_version}, + } + + +def main() -> None: + """Pretty-print the bug information as JSON.""" + print(json.dumps(info(), sort_keys=True, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/apigw-dynamodb-python-cdk/src/jwt/jwk_set_cache.py b/apigw-dynamodb-python-cdk/src/jwt/jwk_set_cache.py new file mode 100644 index 000000000..243256305 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/jwk_set_cache.py @@ -0,0 +1,31 @@ +import time +from typing import Optional + +from .api_jwk import PyJWKSet, PyJWTSetWithTimestamp + + +class JWKSetCache: + def __init__(self, lifespan: int) -> None: + self.jwk_set_with_timestamp: Optional[PyJWTSetWithTimestamp] = None + self.lifespan = lifespan + + def put(self, jwk_set: PyJWKSet) -> None: + if jwk_set is not None: + self.jwk_set_with_timestamp = PyJWTSetWithTimestamp(jwk_set) + else: + # clear cache + self.jwk_set_with_timestamp = None + + def get(self) -> Optional[PyJWKSet]: + if self.jwk_set_with_timestamp is None or self.is_expired(): + return None + + return self.jwk_set_with_timestamp.get_jwk_set() + + def is_expired(self) -> bool: + return ( + self.jwk_set_with_timestamp is not None + and self.lifespan > -1 + and time.monotonic() + > self.jwk_set_with_timestamp.get_timestamp() + self.lifespan + ) diff --git a/apigw-dynamodb-python-cdk/src/jwt/jwks_client.py b/apigw-dynamodb-python-cdk/src/jwt/jwks_client.py new file mode 100644 index 000000000..f19b10acb --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/jwks_client.py @@ -0,0 +1,124 @@ +import json +import urllib.request +from functools import lru_cache +from ssl import SSLContext +from typing import Any, Dict, List, Optional +from urllib.error import URLError + +from .api_jwk import PyJWK, PyJWKSet +from .api_jwt import decode_complete as decode_token +from .exceptions import PyJWKClientConnectionError, PyJWKClientError +from .jwk_set_cache import JWKSetCache + + +class PyJWKClient: + def __init__( + self, + uri: str, + cache_keys: bool = False, + max_cached_keys: int = 16, + cache_jwk_set: bool = True, + lifespan: int = 300, + headers: Optional[Dict[str, Any]] = None, + timeout: int = 30, + ssl_context: Optional[SSLContext] = None, + ): + if headers is None: + headers = {} + self.uri = uri + self.jwk_set_cache: Optional[JWKSetCache] = None + self.headers = headers + self.timeout = timeout + self.ssl_context = ssl_context + + if cache_jwk_set: + # Init jwt set cache with default or given lifespan. + # Default lifespan is 300 seconds (5 minutes). + if lifespan <= 0: + raise PyJWKClientError( + f'Lifespan must be greater than 0, the input is "{lifespan}"' + ) + self.jwk_set_cache = JWKSetCache(lifespan) + else: + self.jwk_set_cache = None + + if cache_keys: + # Cache signing keys + # Ignore mypy (https://github.com/python/mypy/issues/2427) + self.get_signing_key = lru_cache(maxsize=max_cached_keys)(self.get_signing_key) # type: ignore + + def fetch_data(self) -> Any: + jwk_set: Any = None + try: + r = urllib.request.Request(url=self.uri, headers=self.headers) + with urllib.request.urlopen( + r, timeout=self.timeout, context=self.ssl_context + ) as response: + jwk_set = json.load(response) + except (URLError, TimeoutError) as e: + raise PyJWKClientConnectionError( + f'Fail to fetch data from the url, err: "{e}"' + ) + else: + return jwk_set + finally: + if self.jwk_set_cache is not None: + self.jwk_set_cache.put(jwk_set) + + def get_jwk_set(self, refresh: bool = False) -> PyJWKSet: + data = None + if self.jwk_set_cache is not None and not refresh: + data = self.jwk_set_cache.get() + + if data is None: + data = self.fetch_data() + + if not isinstance(data, dict): + raise PyJWKClientError("The JWKS endpoint did not return a JSON object") + + return PyJWKSet.from_dict(data) + + def get_signing_keys(self, refresh: bool = False) -> List[PyJWK]: + jwk_set = self.get_jwk_set(refresh) + signing_keys = [ + jwk_set_key + for jwk_set_key in jwk_set.keys + if jwk_set_key.public_key_use in ["sig", None] and jwk_set_key.key_id + ] + + if not signing_keys: + raise PyJWKClientError("The JWKS endpoint did not contain any signing keys") + + return signing_keys + + def get_signing_key(self, kid: str) -> PyJWK: + signing_keys = self.get_signing_keys() + signing_key = self.match_kid(signing_keys, kid) + + if not signing_key: + # If no matching signing key from the jwk set, refresh the jwk set and try again. + signing_keys = self.get_signing_keys(refresh=True) + signing_key = self.match_kid(signing_keys, kid) + + if not signing_key: + raise PyJWKClientError( + f'Unable to find a signing key that matches: "{kid}"' + ) + + return signing_key + + def get_signing_key_from_jwt(self, token: str) -> PyJWK: + unverified = decode_token(token, options={"verify_signature": False}) + header = unverified["header"] + return self.get_signing_key(header.get("kid")) + + @staticmethod + def match_kid(signing_keys: List[PyJWK], kid: str) -> Optional[PyJWK]: + signing_key = None + + for key in signing_keys: + if key.key_id == kid: + signing_key = key + break + + return signing_key diff --git a/apigw-dynamodb-python-cdk/src/jwt/py.typed b/apigw-dynamodb-python-cdk/src/jwt/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/apigw-dynamodb-python-cdk/src/jwt/types.py b/apigw-dynamodb-python-cdk/src/jwt/types.py new file mode 100644 index 000000000..7d9935205 --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/types.py @@ -0,0 +1,5 @@ +from typing import Any, Callable, Dict + +JWKDict = Dict[str, Any] + +HashlibHash = Callable[..., Any] diff --git a/apigw-dynamodb-python-cdk/src/jwt/utils.py b/apigw-dynamodb-python-cdk/src/jwt/utils.py new file mode 100644 index 000000000..81c5ee41a --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/utils.py @@ -0,0 +1,156 @@ +import base64 +import binascii +import re +from typing import Union + +try: + from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve + from cryptography.hazmat.primitives.asymmetric.utils import ( + decode_dss_signature, + encode_dss_signature, + ) +except ModuleNotFoundError: + pass + + +def force_bytes(value: Union[bytes, str]) -> bytes: + if isinstance(value, str): + return value.encode("utf-8") + elif isinstance(value, bytes): + return value + else: + raise TypeError("Expected a string value") + + +def base64url_decode(input: Union[bytes, str]) -> bytes: + input_bytes = force_bytes(input) + + rem = len(input_bytes) % 4 + + if rem > 0: + input_bytes += b"=" * (4 - rem) + + return base64.urlsafe_b64decode(input_bytes) + + +def base64url_encode(input: bytes) -> bytes: + return base64.urlsafe_b64encode(input).replace(b"=", b"") + + +def to_base64url_uint(val: int) -> bytes: + if val < 0: + raise ValueError("Must be a positive integer") + + int_bytes = bytes_from_int(val) + + if len(int_bytes) == 0: + int_bytes = b"\x00" + + return base64url_encode(int_bytes) + + +def from_base64url_uint(val: Union[bytes, str]) -> int: + data = base64url_decode(force_bytes(val)) + return int.from_bytes(data, byteorder="big") + + +def number_to_bytes(num: int, num_bytes: int) -> bytes: + padded_hex = "%0*x" % (2 * num_bytes, num) + return binascii.a2b_hex(padded_hex.encode("ascii")) + + +def bytes_to_number(string: bytes) -> int: + return int(binascii.b2a_hex(string), 16) + + +def bytes_from_int(val: int) -> bytes: + remaining = val + byte_length = 0 + + while remaining != 0: + remaining >>= 8 + byte_length += 1 + + return val.to_bytes(byte_length, "big", signed=False) + + +def der_to_raw_signature(der_sig: bytes, curve: "EllipticCurve") -> bytes: + num_bits = curve.key_size + num_bytes = (num_bits + 7) // 8 + + r, s = decode_dss_signature(der_sig) + + return number_to_bytes(r, num_bytes) + number_to_bytes(s, num_bytes) + + +def raw_to_der_signature(raw_sig: bytes, curve: "EllipticCurve") -> bytes: + num_bits = curve.key_size + num_bytes = (num_bits + 7) // 8 + + if len(raw_sig) != 2 * num_bytes: + raise ValueError("Invalid signature") + + r = bytes_to_number(raw_sig[:num_bytes]) + s = bytes_to_number(raw_sig[num_bytes:]) + + return bytes(encode_dss_signature(r, s)) + + +# Based on https://github.com/hynek/pem/blob/7ad94db26b0bc21d10953f5dbad3acfdfacf57aa/src/pem/_core.py#L224-L252 +_PEMS = { + b"CERTIFICATE", + b"TRUSTED CERTIFICATE", + b"PRIVATE KEY", + b"PUBLIC KEY", + b"ENCRYPTED PRIVATE KEY", + b"OPENSSH PRIVATE KEY", + b"DSA PRIVATE KEY", + b"RSA PRIVATE KEY", + b"RSA PUBLIC KEY", + b"EC PRIVATE KEY", + b"DH PARAMETERS", + b"NEW CERTIFICATE REQUEST", + b"CERTIFICATE REQUEST", + b"SSH2 PUBLIC KEY", + b"SSH2 ENCRYPTED PRIVATE KEY", + b"X509 CRL", +} + +_PEM_RE = re.compile( + b"----[- ]BEGIN (" + + b"|".join(_PEMS) + + b""")[- ]----\r? +.+?\r? +----[- ]END \\1[- ]----\r?\n?""", + re.DOTALL, +) + + +def is_pem_format(key: bytes) -> bool: + return bool(_PEM_RE.search(key)) + + +# Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46 +_CERT_SUFFIX = b"-cert-v01@openssh.com" +_SSH_PUBKEY_RC = re.compile(rb"\A(\S+)[ \t]+(\S+)") +_SSH_KEY_FORMATS = [ + b"ssh-ed25519", + b"ssh-rsa", + b"ssh-dss", + b"ecdsa-sha2-nistp256", + b"ecdsa-sha2-nistp384", + b"ecdsa-sha2-nistp521", +] + + +def is_ssh_key(key: bytes) -> bool: + if any(string_value in key for string_value in _SSH_KEY_FORMATS): + return True + + ssh_pubkey_match = _SSH_PUBKEY_RC.match(key) + if ssh_pubkey_match: + key_type = ssh_pubkey_match.group(1) + if _CERT_SUFFIX == key_type[-len(_CERT_SUFFIX) :]: + return True + + return False diff --git a/apigw-dynamodb-python-cdk/src/jwt/warnings.py b/apigw-dynamodb-python-cdk/src/jwt/warnings.py new file mode 100644 index 000000000..8762a8cbb --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/jwt/warnings.py @@ -0,0 +1,2 @@ +class RemovedInPyjwt3Warning(DeprecationWarning): + pass diff --git a/apigw-dynamodb-python-cdk/src/lambda.zip b/apigw-dynamodb-python-cdk/src/lambda.zip new file mode 100644 index 0000000000000000000000000000000000000000..57cc47d8dbf0fc9d20f7738a1d4b7a7d407bede7 GIT binary patch literal 70748 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 literal 0 HcmV?d00001 diff --git a/apigw-dynamodb-python-cdk/src/lambda_function.py b/apigw-dynamodb-python-cdk/src/lambda_function.py new file mode 100644 index 000000000..5612bf38e --- /dev/null +++ b/apigw-dynamodb-python-cdk/src/lambda_function.py @@ -0,0 +1,105 @@ +# A simple request-based authorizer example to demonstrate how to use request +# parameters to allow or deny a request. In this example, a request is +# authorized if the client-supplied headerauth1 header, QueryString1 +# query parameter, and stage variable of StageVar1 all match +# specified values of 'headerValue1', 'queryValue1', and 'stageValue1', +# respectively. + +import json +import jwt +import boto3 + +apigateway_client = boto3.client('apigateway') + +def lambda_handler(event, context): + # Retrieve request parameters from the Lambda function input: + headers = event['headers'] + # queryStringParameters = event['queryStringParameters'] + # pathParameters = event['pathParameters'] + # stageVariables = event['stageVariables'] + + # Parse the input for the parameter values + tmp = event['methodArn'].split(':') + apiGatewayArnTmp = tmp[5].split('/') + awsAccountId = tmp[4] + region = tmp[3] + restApiId = apiGatewayArnTmp[0] + stage = apiGatewayArnTmp[1] + method = apiGatewayArnTmp[2] + resource = '/' + + if (apiGatewayArnTmp[3]): + resource += apiGatewayArnTmp[3] + + # Perform authorization to return the Allow policy for correct parameters + # and the 'Unauthorized' error, otherwise. + + authResponse = {} + condition = {} + condition['IpAddress'] = {} + id_token = json.dumps(event['headers']['Authorization']) + try: + token = id_token.split()[1].strip('"') + decoded = jwt.decode(token, options={"verify_signature": False}) + groups = decoded['cognito:groups'] + for group in groups: + if 'Group-' in group: + # api_key = get_api_key_value(f'ApiKey-{group.split('-')[1]}') + api_key = get_api_key(f'ApiKey-{group.split('-')[1]}') + response = generateAllow('me', event['methodArn'], api_key) + print('authorized') + return json.loads(response) + else: + print('unauthorized') + response = generateDeny('me', event['methodArn'], api_key) + return json.loads(response) + + except: + token = None + api_key = None + response = generateDeny('me', event['methodArn'], api_key) + return json.loads(response) + + + # Help function to generate IAM policy + +def generatePolicy(principalId, effect, resource, api_key): + authResponse = {} + authResponse['principalId'] = principalId + if (effect and resource): + policyDocument = {} + policyDocument['Version'] = '2012-10-17' + policyDocument['Statement'] = [] + statementOne = {} + statementOne['Action'] = 'execute-api:Invoke' + statementOne['Effect'] = effect + statementOne['Resource'] = resource + policyDocument['Statement'] = [statementOne] + authResponse['policyDocument'] = policyDocument + authResponse['usageIdentifierKey'] = api_key + + + authResponse_JSON = json.dumps(authResponse) + print(authResponse_JSON) + + return authResponse_JSON + + +def generateAllow(principalId, resource, api_key): + return generatePolicy(principalId, 'Allow', resource, api_key) + + +def generateDeny(principalId, resource, api_key): + return generatePolicy(principalId, 'Deny', resource, api_key) + + +def get_api_key(key_name): + try: + response = apigateway_client.get_api_keys(nameQuery=key_name, includeValues=True) + api_keys = response['items'] + if api_keys: + return api_keys[0]['value'] + return None + except ClientError as e: + print(e) + return None \ No newline at end of file diff --git a/apigw-dynamodb-python-cdk/tests/__init__.py b/apigw-dynamodb-python-cdk/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apigw-dynamodb-python-cdk/tests/test_apigw_dynamodb_python_stack.py b/apigw-dynamodb-python-cdk/tests/test_apigw_dynamodb_python_stack.py new file mode 100644 index 000000000..54f8f0b88 --- /dev/null +++ b/apigw-dynamodb-python-cdk/tests/test_apigw_dynamodb_python_stack.py @@ -0,0 +1,161 @@ +import pytest +import requests +import boto3 +from botocore.exceptions import ClientError + +def get_stack_outputs(stack_name): + client = boto3.client('cloudformation') + response = client.describe_stacks(StackName=stack_name) + outputs = response['Stacks'][0]['Outputs'] + + return {output['OutputKey']: output['OutputValue'] for output in outputs} + +@pytest.fixture(scope="module") +def config(): + stack_name = "ApigwDynamodbPythonStack" + output = get_stack_outputs(stack_name) + return output + +def get_api_key_value(apiKey): + client = boto3.client('apigateway') + response = client.get_api_key( + apiKey=apiKey, + includeValue=True + ) + return response['value'] + +def create_cognito_user(username, password, client_id, pool_id, group_name): + client = boto3.client('cognito-idp') + + try: + response = client.admin_create_user( + UserPoolId=pool_id, + Username=username, + UserAttributes=[ + { + 'Name': 'email', + 'Value': username + } + ], + # TemporaryPassword=password, + MessageAction='SUPPRESS' + ) + response = client.admin_set_user_password( + UserPoolId=pool_id, + Username=username, + Password=password, + Permanent=True + ) + client.admin_add_user_to_group( + UserPoolId=pool_id, + Username=username, + GroupName=group_name + ) + return response + except ClientError as e: + print(f"Error creating user: {e}") + return None + +# Function to authenticate user and retrieve token +def authenticate_user(username, password, client_id): + client = boto3.client('cognito-idp') + + try: + response = client.initiate_auth( + ClientId=client_id, + AuthFlow='USER_PASSWORD_AUTH', + AuthParameters={ + 'USERNAME': username, + 'PASSWORD': password, + } + ) + return response['AuthenticationResult']['IdToken'] + except ClientError as e: + print(f"Error authenticating user: {e}") + return None + +# Function to delete a user from Cognito +def delete_cognito_user(username, pool_id): + client = boto3.client('cognito-idp') + + try: + client.admin_delete_user( + UserPoolId=pool_id, + Username=username + ) + except ClientError as e: + print(f"Error deleting user: {e}") + +@pytest.fixture(scope="module") +def token(config): + usernameBasic = 'testuser@mail.com' # Choose a unique username + usernameFree = "testuser1@mail.com" + password = 'TestPassword123!' # Ensure this meets Cognito password policy + client_id=config['CognitoClientId'] + pool_id=config['CognitoUserPoolId'] + # Create user + create_cognito_user(usernameFree, password, client_id, pool_id, "Group-FreeTier") + create_cognito_user(usernameBasic, password, client_id, pool_id, "Group-BasicUsagePlan") + + # Authenticate user and get token + user_token_free = authenticate_user(usernameFree, password, client_id) + user_token_basic = authenticate_user(usernameBasic, password, client_id) + + + # Finalizer to delete user after tests + yield user_token_free, user_token_basic # This is where the test will use the token + delete_cognito_user(usernameFree, pool_id) # This will run after the test is done + delete_cognito_user(usernameBasic, pool_id) + + +def test_put_authorized(token, config): + api_url = config['ApiUrl'] + api_url = f"{api_url}/put" + token_data = token[1] + headers = {'Authorization': f'Bearer {token_data}', 'Content-Type': 'application/json'} + payload = {"ID": "aa", "FirstName": "test", "Age": 22} + response = requests.post(api_url, headers=headers, json=payload) + + assert response.status_code == 200 + assert response.json()['body'] == {"message": "Item Added Successfully"} + +def test_put_unauthorized(config): + api_url = config['ApiUrl'] + api_url = f"{api_url}/put" + headers = {'Authorization': f'Bearer invalid_token', 'Content-Type': 'application/json'} + payload = {"ID": "aa", "FirstName": "test", "Age": 22} + + response = requests.post(api_url, headers=headers, json=payload) + + assert response.status_code == 403 + +def test_get_with_api_key(config): + api_url = config['ApiUrl'] + api_url = f"{api_url}/get" + api_key_value = get_api_key_value(config['ApiKeyId0']) + headers = {'x-api-key': api_key_value, 'Content-Type': 'application/json'} + + response = requests.get(api_url, headers=headers) + + assert response.status_code == 200 + assert response.json()['body'] == {"ID": "aa", "FirstName": "test", "Age": '22'} + +def test_get_without_api_key(config): + api_url = config['ApiUrl'] + api_url = f"{api_url}/get" + headers = {'Content-Type': 'application/json'} + + response = requests.get(api_url, headers=headers) + print(response.text) + + assert response.status_code == 403 + assert response.json() == {'message': 'Forbidden'} + +def test_delete_item_authorized(token, config): + api_url = config['ApiUrl'] + api_url = f"{api_url}/delete/aa" + headers = {'Authorization': f'Bearer {token[0]}', 'Content-Type': 'application/json'} + payload = {'ID': {'S': 'aa'}, 'FirstName': {'S': 'test'}} + + response = requests.post(api_url, headers=headers, json=payload) + assert response.status_code == 200 diff --git a/apigw-dynamodb-python-cdk/vtl/deleteItem.vtl b/apigw-dynamodb-python-cdk/vtl/deleteItem.vtl new file mode 100644 index 000000000..fedb6c3d5 --- /dev/null +++ b/apigw-dynamodb-python-cdk/vtl/deleteItem.vtl @@ -0,0 +1,8 @@ +#set($inputRoot = $input.path('$')) +{ + "TableName": "$stageVariables.TableName", + "Key": { + "ID": { "S": "$inputRoot.ID.S" }, + "FirstName": {"S": "$inputRoot.FirstName.S"} + } +} diff --git a/apigw-dynamodb-python-cdk/vtl/putItem.vtl b/apigw-dynamodb-python-cdk/vtl/putItem.vtl new file mode 100644 index 000000000..2325cdd58 --- /dev/null +++ b/apigw-dynamodb-python-cdk/vtl/putItem.vtl @@ -0,0 +1,14 @@ +#set($inputRoot = $input.path('$')) +{ + "TableName": "$stageVariables.TableName", + "Item": { + "ID": { "S": "$inputRoot.ID" }, + "FirstName": { "S": "$inputRoot.FirstName" }, + "Age": { "N": "$inputRoot.Age" } + #foreach($key in $inputRoot.keySet()) + #if ($key != 'FirstName' && $key != 'Age' && $key != 'ID') + ,"$key": { "S": "$inputRoot.get($key)" } + #end + #end + } +} diff --git a/apigw-dynamodb-python-cdk/vtl/response.vtl b/apigw-dynamodb-python-cdk/vtl/response.vtl new file mode 100644 index 000000000..d2a6d65bf --- /dev/null +++ b/apigw-dynamodb-python-cdk/vtl/response.vtl @@ -0,0 +1,9 @@ +## Response mapping template for DynamoDB PutItem operation +#set($inputRoot = $input.path('$')) +{ + "statusCode": "200", + "body": {"message": "Item Added Successfully"}, + "headers": { + "Content-Type": "application/json" + } +} diff --git a/apigw-dynamodb-python-cdk/vtl/scan.vtl b/apigw-dynamodb-python-cdk/vtl/scan.vtl new file mode 100644 index 000000000..bb00f9450 --- /dev/null +++ b/apigw-dynamodb-python-cdk/vtl/scan.vtl @@ -0,0 +1,15 @@ +#set($inputRoot = $input.path('$')) +{ + "statusCode": "200", + "headers": { + "Content-Type": "application/json" + }, + "body": +#foreach($elem in $inputRoot.Items) + { + "ID" : "$elem.ID.S", + "FirstName": "$elem.FirstName.S", + "Age" : "$elem.Age.N" + }#if($foreach.hasNext),#end +#end +} \ No newline at end of file diff --git a/apigw-dynamodb-python-cdk/vtl/scan_request.vtl b/apigw-dynamodb-python-cdk/vtl/scan_request.vtl new file mode 100644 index 000000000..2dd1b6f12 --- /dev/null +++ b/apigw-dynamodb-python-cdk/vtl/scan_request.vtl @@ -0,0 +1,3 @@ +{ + "TableName": "$stageVariables.TableName" +} \ No newline at end of file From 8e90e0a53d88aea7add65c1e6a2f28a0e4ffd8a7 Mon Sep 17 00:00:00 2001 From: maya Date: Tue, 23 Jul 2024 16:25:07 +0300 Subject: [PATCH 2/4] rename stack --- apigw-dynamodb-python-cdk/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigw-dynamodb-python-cdk/app.py b/apigw-dynamodb-python-cdk/app.py index 4bfca1d5e..1e5e23fbf 100644 --- a/apigw-dynamodb-python-cdk/app.py +++ b/apigw-dynamodb-python-cdk/app.py @@ -39,6 +39,6 @@ app.node.set_context("usage_plans", usage_plans) -stack = ApigwDynamodbPythonStack(app, "CapstoneDynamodbPythonStack") +stack = ApigwDynamodbPythonStack(app, "ApigwDynamodbPythonStack") app.synth() From c5613f19a3652808e03c13f83dfcfed32ec2a6b9 Mon Sep 17 00:00:00 2001 From: maya Date: Tue, 23 Jul 2024 16:33:33 +0300 Subject: [PATCH 3/4] readme --- apigw-dynamodb-python-cdk/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apigw-dynamodb-python-cdk/README.md b/apigw-dynamodb-python-cdk/README.md index 114c27fc8..f601cdbfd 100644 --- a/apigw-dynamodb-python-cdk/README.md +++ b/apigw-dynamodb-python-cdk/README.md @@ -28,11 +28,11 @@ cd serverless-patterns/apigw-dynamodb-python-cdk ``` 3. To manually create a virtualenv on MacOS and Linux: ``` -$ python3 -m venv .venv +python3 -m venv .venv ``` 4. After the init process completes and the virtualenv is created, you can use the following to activate virtualenv. ``` -$ source .venv/bin/activate +source .venv/bin/activate ``` 6. After activating your virtual environment for the first time, install the app's standard dependencies: ``` From 8dbd7ea7553116c580605572e3c7436f14dd770f Mon Sep 17 00:00:00 2001 From: maya Date: Thu, 5 Sep 2024 14:28:16 +0300 Subject: [PATCH 4/4] remove dependencies and update README and example-pattern --- apigw-dynamodb-python-cdk/README.md | 12 +- .../example-pattern.json | 2 +- .../src/PyJWT-2.8.0.dist-info/AUTHORS.rst | 7 - .../src/PyJWT-2.8.0.dist-info/INSTALLER | 1 - .../src/PyJWT-2.8.0.dist-info/LICENSE | 21 - .../src/PyJWT-2.8.0.dist-info/METADATA | 107 --- .../src/PyJWT-2.8.0.dist-info/RECORD | 33 - .../src/PyJWT-2.8.0.dist-info/REQUESTED | 0 .../src/PyJWT-2.8.0.dist-info/WHEEL | 5 - .../src/PyJWT-2.8.0.dist-info/top_level.txt | 1 - apigw-dynamodb-python-cdk/src/jwt/__init__.py | 74 -- .../jwt/__pycache__/__init__.cpython-311.pyc | Bin 1925 -> 0 bytes .../__pycache__/algorithms.cpython-311.pyc | Bin 39998 -> 0 bytes .../jwt/__pycache__/api_jwk.cpython-311.pyc | Bin 7463 -> 0 bytes .../jwt/__pycache__/api_jws.cpython-311.pyc | Bin 14276 -> 0 bytes .../jwt/__pycache__/api_jwt.cpython-311.pyc | Bin 14386 -> 0 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 3855 -> 0 bytes .../src/jwt/__pycache__/help.cpython-311.pyc | Bin 2389 -> 0 bytes .../__pycache__/jwk_set_cache.cpython-311.pyc | Bin 1938 -> 0 bytes .../__pycache__/jwks_client.cpython-311.pyc | Bin 6457 -> 0 bytes .../src/jwt/__pycache__/types.cpython-311.pyc | Bin 396 -> 0 bytes .../src/jwt/__pycache__/utils.cpython-311.pyc | Bin 6924 -> 0 bytes .../jwt/__pycache__/warnings.cpython-311.pyc | Bin 469 -> 0 bytes .../src/jwt/algorithms.py | 862 ------------------ apigw-dynamodb-python-cdk/src/jwt/api_jwk.py | 132 --- apigw-dynamodb-python-cdk/src/jwt/api_jws.py | 328 ------- apigw-dynamodb-python-cdk/src/jwt/api_jwt.py | 372 -------- .../src/jwt/exceptions.py | 70 -- apigw-dynamodb-python-cdk/src/jwt/help.py | 64 -- .../src/jwt/jwk_set_cache.py | 31 - .../src/jwt/jwks_client.py | 124 --- apigw-dynamodb-python-cdk/src/jwt/py.typed | 0 apigw-dynamodb-python-cdk/src/jwt/types.py | 5 - apigw-dynamodb-python-cdk/src/jwt/utils.py | 156 ---- apigw-dynamodb-python-cdk/src/jwt/warnings.py | 2 - apigw-dynamodb-python-cdk/src/lambda.zip | Bin 70748 -> 0 bytes 36 files changed, 11 insertions(+), 2398 deletions(-) delete mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/AUTHORS.rst delete mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/INSTALLER delete mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/LICENSE delete mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/METADATA delete mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/RECORD delete mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/REQUESTED delete mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/WHEEL delete mode 100644 apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/top_level.txt delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__init__.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/__init__.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/algorithms.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jwk.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jws.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jwt.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/exceptions.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/help.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/jwk_set_cache.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/jwks_client.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/types.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/utils.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/__pycache__/warnings.cpython-311.pyc delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/algorithms.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/api_jwk.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/api_jws.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/api_jwt.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/exceptions.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/help.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/jwk_set_cache.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/jwks_client.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/py.typed delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/types.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/utils.py delete mode 100644 apigw-dynamodb-python-cdk/src/jwt/warnings.py delete mode 100644 apigw-dynamodb-python-cdk/src/lambda.zip diff --git a/apigw-dynamodb-python-cdk/README.md b/apigw-dynamodb-python-cdk/README.md index f601cdbfd..f2fc02998 100644 --- a/apigw-dynamodb-python-cdk/README.md +++ b/apigw-dynamodb-python-cdk/README.md @@ -38,11 +38,19 @@ source .venv/bin/activate ``` python -m pip install -r requirements.txt ``` -7. To generate a cloudformation templates (optional) +7. Install jwt package for Lambda: +``` +cd src; pip install pyjwt --target . +``` +8. Zip the Lambda function and dependencies +``` +zip -r lambda.zip . +``` +9. To generate a cloudformation templates (optional) ``` cdk synth ``` -8. To deploy AWS resources as a CDK project +10. To deploy AWS resources as a CDK project ``` cdk deploy ``` diff --git a/apigw-dynamodb-python-cdk/example-pattern.json b/apigw-dynamodb-python-cdk/example-pattern.json index 0d3da06c0..bbc50fe66 100644 --- a/apigw-dynamodb-python-cdk/example-pattern.json +++ b/apigw-dynamodb-python-cdk/example-pattern.json @@ -1,6 +1,6 @@ { "title": "API Gateway direct integration to DynamoDB", - "description": "Create direct integration with API Gateway to DynamoDB showcasing transformation of request/response using VTL and CDK and implement examples for using Cognito, Lambda authorizer and API keys.", + "description": "Direct integration with API Gateway to DynamoDB with transformation using VTL and CDK and examples for Cognito, Lambda authorizer and API keys.", "language": "Python", "level": "300", "framework": "CDK", diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/AUTHORS.rst b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/AUTHORS.rst deleted file mode 100644 index 88e2b6ad7..000000000 --- a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/AUTHORS.rst +++ /dev/null @@ -1,7 +0,0 @@ -Authors -======= - -``pyjwt`` is currently written and maintained by `Jose Padilla `_. -Originally written and maintained by `Jeff Lindsay `_. - -A full list of contributors can be found on GitHub’s `overview `_. diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/INSTALLER b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/INSTALLER deleted file mode 100644 index a1b589e38..000000000 --- a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/LICENSE b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/LICENSE deleted file mode 100644 index fd0ecbc88..000000000 --- a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015-2022 José Padilla - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/METADATA b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/METADATA deleted file mode 100644 index b329a46ea..000000000 --- a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/METADATA +++ /dev/null @@ -1,107 +0,0 @@ -Metadata-Version: 2.1 -Name: PyJWT -Version: 2.8.0 -Summary: JSON Web Token implementation in Python -Home-page: https://github.com/jpadilla/pyjwt -Author: Jose Padilla -Author-email: hello@jpadilla.com -License: MIT -Keywords: json,jwt,security,signing,token,web -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: Natural Language :: English -Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Topic :: Utilities -Requires-Python: >=3.7 -Description-Content-Type: text/x-rst -License-File: LICENSE -License-File: AUTHORS.rst -Requires-Dist: typing-extensions ; python_version <= "3.7" -Provides-Extra: crypto -Requires-Dist: cryptography (>=3.4.0) ; extra == 'crypto' -Provides-Extra: dev -Requires-Dist: sphinx (<5.0.0,>=4.5.0) ; extra == 'dev' -Requires-Dist: sphinx-rtd-theme ; extra == 'dev' -Requires-Dist: zope.interface ; extra == 'dev' -Requires-Dist: cryptography (>=3.4.0) ; extra == 'dev' -Requires-Dist: pytest (<7.0.0,>=6.0.0) ; extra == 'dev' -Requires-Dist: coverage[toml] (==5.0.4) ; extra == 'dev' -Requires-Dist: pre-commit ; extra == 'dev' -Provides-Extra: docs -Requires-Dist: sphinx (<5.0.0,>=4.5.0) ; extra == 'docs' -Requires-Dist: sphinx-rtd-theme ; extra == 'docs' -Requires-Dist: zope.interface ; extra == 'docs' -Provides-Extra: tests -Requires-Dist: pytest (<7.0.0,>=6.0.0) ; extra == 'tests' -Requires-Dist: coverage[toml] (==5.0.4) ; extra == 'tests' - -PyJWT -===== - -.. image:: https://github.com/jpadilla/pyjwt/workflows/CI/badge.svg - :target: https://github.com/jpadilla/pyjwt/actions?query=workflow%3ACI - -.. image:: https://img.shields.io/pypi/v/pyjwt.svg - :target: https://pypi.python.org/pypi/pyjwt - -.. image:: https://codecov.io/gh/jpadilla/pyjwt/branch/master/graph/badge.svg - :target: https://codecov.io/gh/jpadilla/pyjwt - -.. image:: https://readthedocs.org/projects/pyjwt/badge/?version=stable - :target: https://pyjwt.readthedocs.io/en/stable/ - -A Python implementation of `RFC 7519 `_. Original implementation was written by `@progrium `_. - -Sponsor -------- - -+--------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ -| |auth0-logo| | If you want to quickly add secure token-based authentication to Python projects, feel free to check Auth0's Python SDK and free plan at `auth0.com/developers `_. | -+--------------+-----------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - -.. |auth0-logo| image:: https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png - -Installing ----------- - -Install with **pip**: - -.. code-block:: console - - $ pip install PyJWT - - -Usage ------ - -.. code-block:: pycon - - >>> import jwt - >>> encoded = jwt.encode({"some": "payload"}, "secret", algorithm="HS256") - >>> print(encoded) - eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg - >>> jwt.decode(encoded, "secret", algorithms=["HS256"]) - {'some': 'payload'} - -Documentation -------------- - -View the full docs online at https://pyjwt.readthedocs.io/en/stable/ - - -Tests ------ - -You can run tests from the project root after cloning with: - -.. code-block:: console - - $ tox diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/RECORD b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/RECORD deleted file mode 100644 index b12566709..000000000 --- a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/RECORD +++ /dev/null @@ -1,33 +0,0 @@ -PyJWT-2.8.0.dist-info/AUTHORS.rst,sha256=klzkNGECnu2_VY7At89_xLBF3vUSDruXk3xwgUBxzwc,322 -PyJWT-2.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -PyJWT-2.8.0.dist-info/LICENSE,sha256=eXp6ICMdTEM-nxkR2xcx0GtYKLmPSZgZoDT3wPVvXOU,1085 -PyJWT-2.8.0.dist-info/METADATA,sha256=pV2XZjvithGcVesLHWAv0J4T5t8Qc66fip2sbxwoz1o,4160 -PyJWT-2.8.0.dist-info/RECORD,, -PyJWT-2.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -PyJWT-2.8.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92 -PyJWT-2.8.0.dist-info/top_level.txt,sha256=RP5DHNyJbMq2ka0FmfTgoSaQzh7e3r5XuCWCO8a00k8,4 -jwt/__init__.py,sha256=mV9lg6n4-0xiqCKaE1eEPC9a4j6sEkEYQcKghULE7kU,1670 -jwt/__pycache__/__init__.cpython-311.pyc,, -jwt/__pycache__/algorithms.cpython-311.pyc,, -jwt/__pycache__/api_jwk.cpython-311.pyc,, -jwt/__pycache__/api_jws.cpython-311.pyc,, -jwt/__pycache__/api_jwt.cpython-311.pyc,, -jwt/__pycache__/exceptions.cpython-311.pyc,, -jwt/__pycache__/help.cpython-311.pyc,, -jwt/__pycache__/jwk_set_cache.cpython-311.pyc,, -jwt/__pycache__/jwks_client.cpython-311.pyc,, -jwt/__pycache__/types.cpython-311.pyc,, -jwt/__pycache__/utils.cpython-311.pyc,, -jwt/__pycache__/warnings.cpython-311.pyc,, -jwt/algorithms.py,sha256=RDsv5Lm3bzwsiWT3TynT7JR41R6H6s_fWUGOIqd9x_I,29800 -jwt/api_jwk.py,sha256=HPxVqgBZm7RTaEXydciNBCuYNKDYOC_prTdaN9toGbo,4196 -jwt/api_jws.py,sha256=da17RrDe0PDccTbx3rx2lLezEG_c_YGw_vVHa335IOk,11099 -jwt/api_jwt.py,sha256=yF9DwF1kt3PA5n_TiU0OmHd0LtPHfe4JCE1XOfKPjw0,12638 -jwt/exceptions.py,sha256=KDC3M7cTrpR4OQXVURlVMThem0pfANSgBxRz-ttivmo,1046 -jwt/help.py,sha256=Jrp84fG43sCwmSIaDtY08I6ZR2VE7NhrTff89tYSE40,1749 -jwt/jwk_set_cache.py,sha256=hBKmN-giU7-G37L_XKgc_OZu2ah4wdbj1ZNG_GkoSE8,959 -jwt/jwks_client.py,sha256=9W8JVyGByQgoLbBN1u5iY1_jlgfnnukeOBTpqaM_9SE,4222 -jwt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -jwt/types.py,sha256=VnhGv_VFu5a7_mrPoSCB7HaNLrJdhM8Sq1sSfEg0gLU,99 -jwt/utils.py,sha256=PAI05_8MHQCxWQTDlwN0hTtTIT2DTTZ28mm1x6-26UY,3903 -jwt/warnings.py,sha256=50XWOnyNsIaqzUJTk6XHNiIDykiL763GYA92MjTKmok,59 diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/REQUESTED b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/REQUESTED deleted file mode 100644 index e69de29bb..000000000 diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/WHEEL b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/WHEEL deleted file mode 100644 index 1f37c02f2..000000000 --- a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: bdist_wheel (0.40.0) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/top_level.txt b/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/top_level.txt deleted file mode 100644 index 27ccc9bc3..000000000 --- a/apigw-dynamodb-python-cdk/src/PyJWT-2.8.0.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -jwt diff --git a/apigw-dynamodb-python-cdk/src/jwt/__init__.py b/apigw-dynamodb-python-cdk/src/jwt/__init__.py deleted file mode 100644 index 68d09c1c4..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -from .api_jwk import PyJWK, PyJWKSet -from .api_jws import ( - PyJWS, - get_algorithm_by_name, - get_unverified_header, - register_algorithm, - unregister_algorithm, -) -from .api_jwt import PyJWT, decode, encode -from .exceptions import ( - DecodeError, - ExpiredSignatureError, - ImmatureSignatureError, - InvalidAlgorithmError, - InvalidAudienceError, - InvalidIssuedAtError, - InvalidIssuerError, - InvalidKeyError, - InvalidSignatureError, - InvalidTokenError, - MissingRequiredClaimError, - PyJWKClientConnectionError, - PyJWKClientError, - PyJWKError, - PyJWKSetError, - PyJWTError, -) -from .jwks_client import PyJWKClient - -__version__ = "2.8.0" - -__title__ = "PyJWT" -__description__ = "JSON Web Token implementation in Python" -__url__ = "https://pyjwt.readthedocs.io" -__uri__ = __url__ -__doc__ = f"{__description__} <{__uri__}>" - -__author__ = "José Padilla" -__email__ = "hello@jpadilla.com" - -__license__ = "MIT" -__copyright__ = "Copyright 2015-2022 José Padilla" - - -__all__ = [ - "PyJWS", - "PyJWT", - "PyJWKClient", - "PyJWK", - "PyJWKSet", - "decode", - "encode", - "get_unverified_header", - "register_algorithm", - "unregister_algorithm", - "get_algorithm_by_name", - # Exceptions - "DecodeError", - "ExpiredSignatureError", - "ImmatureSignatureError", - "InvalidAlgorithmError", - "InvalidAudienceError", - "InvalidIssuedAtError", - "InvalidIssuerError", - "InvalidKeyError", - "InvalidSignatureError", - "InvalidTokenError", - "MissingRequiredClaimError", - "PyJWKClientConnectionError", - "PyJWKClientError", - "PyJWKError", - "PyJWKSetError", - "PyJWTError", -] diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/__init__.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index fa62819d91a5e5bbf29ebd41b17b770988052b90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1925 zcmdUv%Wm676oyI3x>0x8@>On_#AzZ279GV7dI^FCZWeZs)Tj*vn9YFJjHOXHBt0BD zmR4Hz6}l(_1nr{8bMytgDg>?!bk&`K?7Zp>DK?JVu98hj;^Ucr&YW{N7yeo-YKpkt z_^%ItNGr-8vJqcX6XV_Q!uU}^3Q{eFsz9X)kWghiVI}Dln4&3=qG^z(8IYk_kPZ8i zR*q^wqj`|01yBgJDXU0JphU}{Oe>&5tDs72phl;`G@Su6v<~WY7R=H)Fh}RXJY4__ zbP+7lC9p&rph1_xau_FNeMYZ=Yr;+g9c93Elm$0X4&1~y!3w_dlY%t7BA9l)AQ<}9uRq=7nM3CAY?7VmyWW(1U=FL%eQ$`kD=9z;T%=SuqO#BbkuiS5X9;TiEfVt2pCN51@!Ez2Y{ z>RJq^d&?3f@h!);afcIUGOTi`FY?wxC9;e0LS5?G5ACKJi^Wp8C zz3-muFYtjL)~6HdT9}IDrp&7oTi+e>GrtZUca}w z)?RP7^$&(Omsm>pg{87(X(c7(Ib?Z>f<#6lE0L2Br&eLI5>}EZOH?GP5;ciwi5ZEy z#H_@e#Jt3U#G=HKL_^?t^KOtaT>|^BUcRp(mb|FSF!6`Q#2!5QguiPJGrY4b&BD6(1*!E<+B7~iIO+`~gGcDotR(U7pP`T(DE+fY#$HuYAj6=r~ z{|C?Lca4F4bi(?F{ml&zZg98rVDsqsOUgDjhuj?=8T*Dy+%;}8@#9&0u<7^Uuzh4$ zZANwVS1Ds*S;Q=$J(7yW~@~Oij7lkRXlGp p(OYnoIV(0s+PI*Pa%ZLKQGQ&xKPsN5m-AvOVvI8KX7|Km{RKtSC))r3 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/algorithms.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/algorithms.cpython-311.pyc deleted file mode 100644 index aad57249934a5f76ab172b524fabd3b509a5ebf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39998 zcmd^o3sf9edS*5Kpc`nqp&vl=YTjx+K!DH#Nk|Bxw~%bf^0;jeLR3parU6wqlEom8 z#^Y?T$H`#FiSb0<``)_uf8GDy`(39qhr=~J`s(SkagO_IdQmQ`{9w9U zXy&+=IiBN9gIv@UHks%-YcMOE#qQ>?ncXd63%gsxR@}{lwrF-ZJ8BQxS$NB!BbpP= ziROlLqj}-Hs59)0=7;m6uCOcW4!aS?%G(ADqJ`l?_MAQFiF(7{s4whe@Ag4|v?yE@ zEe;n)OTs15KsXRB4VOmC!e!C&aCx*MToJ7dSDLtp^;}S?0`GW&)k4TdiE>bas&JKw z8?v#tYWdAUwaFc!S7HdiLxbY>2K3uVT;ngQ4i^@M`w#8e9`y8(te- z7hV@_3^zub!cEcj;q}qxaC5XJ+!AdKw?;RFH$>aQZ7jTdaAR~+cvEz9cr$x17~B$V z54T6RhPSf!!oh9P?cwdwj&MhGM|ekcXLx6{Gu#>772Xx?3U@_!hj+7Zp262{_y_jf$)Lo!SKQ8q3|Ju@rDl%IcvB`Pc0`{@$L&BeuNvcT;!fKKf+x! zMRJ7J$o9yNv22bDABnU?Hn69ok@m<|dWvjfa5Hm#AaW#fl)XQ|-XBEF7WUrGAPXBl z7TF=El1f32d;BcN`+v_Aeu!Vimpo$$pUC3&a(v(!jxUX5zlk387XIZc{4nz?gI{^% zVT0e7m|q3_Di`@Z5;-iiGyfp`s}}i(nO`;hY8LrD%KU2KR~HEz@_dZ>)x)o0QM||b ztYhf?$GA}FV=8{gl+5iL8XAuG#RrCmVm+a(q`7lfS2DlvR4gv^^~a-;_}O7Tl$~@Q zf8=O)Z`Z!=u7mq~_9E2KrKG)UurC%n(I+JBJ;M)1;-kV)()!R4Qc7C-`(p8=efUB| z7#!~7KQXyI1F_zbNVFFLqkZvY4qjuiv%OD5E+xH`7Luft(@7StpHy zAKT?t%sw;_j|hE(Xi#^mj>nV&!Og`-PWO!t#*s+>6OkbvPPVhCa3qGO?2$g69~e3l z%1K(gyLKPzOgfHrcO7kM+jy{9eS9$W*t&Ux^4QkWj7Mkp;NZYWe4xK;RJagH7U^D( z3IiAV;t@1=vNV%N&uH{iM2IE*de6~Qg9H5(DUiWE6$hWr)y;3%uvwEwzU-hxN05SU zzNM|Ld5b2HTX9l@*^|x(k9BH-6r^9366TSU5+mtW-&B7g*4NuF#C!SZNOLkT{dQjS zHgG;Vk}M%V0bL4xvv))oh(>x(qo)rGF?4f#H}x!hjAUNVFrI}=Xj=4VXR362BvJb!pA>`Z%#LSWV*FAx3F7FGj2R} z4C_%j$Wb}SQ8~zivg3oY_O7% zSWi0l9q#N(zuJ!N>m+PLcS1LGCw!9wvIIN|v^SizjgIiZW=X=m>6p3Qnq*vrh>smh z`{o@vcvO7@aVu0PU!U4QF~SVNdjm}=Y}s_7PiQ(lJP48-YdUwP>C({oCxvsT&$Vrc z^=^od^lxlCe_=~h*w8i>9~nE}bi8R~U}RmqPdF2auiMx*dbW40<$MzwwrS)Nn3bWX za~I=H>D(GeE+zd43W_e1;Ix;}i-L^cKURSvUEzM7!xdD#;Cjh5Ur_PtWvQ}NEZ86w zY*_AX!C?*97C3-}#sBks&R%$jE41d!*#kH2RkQZ0+a+~x3`+G|#FBQYq(O{Ga}h3V;yFlFCP-9eycJxtC1T~vdHXXK{kRI-;9kM!EOO6=dnNB&dpI;=@7WMd8B-!LtZe4~Dq8smlJNZ?D;-9KR8N=w_vAsO>odKuz!)_B&HY%Ohn+7wsuCs52c z8rR@@ zz0Y;CKF!PKar3l>LrAATZZhKdc#+1}Jgq5l+FhTUK80hPHU%oRF^_3Y{Z^W0$TIdS z>o7!lx`v}8qj6B3AW0EHe&9?b7DrzJNlX0FNQ7Ksqo?{wz!ZWM#JLZttAi(Lp;w_2KEjs>@D#>= zdSD3R5VJ@sWOv+!k#=INK4vzJvsjyYDY=GB{zxna-X_Q-e5O!f2&;d12skT7`i0=|5bK`EiFwJgNlZO?2cqAeaW`wNH5G3KkRUr*FTop=p!=7v0wJhO0N-M+wVuXs651l(AL`J}wktjCi zOMhUE^Vzkr+s8Fy+uCT>W zVc{}L9HWv`AbCjJWHLGCPFGxZ`C|mrR6QHqE=k7soGAs)qsN8O$m2o*f-O4^AO~DC zr3#dl%tmp0hfkdoo~EK`yPOT0qjMLZ&=y2?`4A!RIsmS43wF+tH(_QUW6!;Or6ha$ z24j)O@paeBzzND%94!SVH zkQUZGg|Cwr%awZhHPar&<2lvi8=Bx0RSy0evMbeGm{eU^A)QHkZ|_habZNcGoZjB( zFh4p-&w0JQ=STYnQ$E?fz5H;0Z?7OwY+_11CWva3L$pYG2?8*uB=a1x^pvayIWmmc^R4z$xI48E(&f zS$56){QmiK)`B^!XCcc(xhNo&3*8Ezd5muP&wOdYM&33&XN<#QFCVdxz|v-N*1?kN}H#8%cnCG4H_L%9rqYxZ5Jxe1I?I z3yo4$*bbjENNENsD(v9>;5f>K2a&~Cm<_|DgOHTYGJOFfrVx>W7taFWfORG6Mochh z3&ZyV{frS|@raF(=Q)^mMu?mr9S|bCOi<)uafj zUaMh$qN8BxPes6V4@DXU(o_l;0JPMFjh?X*3X#S+8fNCNDSQav3OARVKUw|UmWeHM zh5m`ITe&#hUQl&J8B<=46}hf zR;Vu@eWL$HAqjDyV8}KmC&F+Rp_}`&Aw7F!Y;c7VqJD@hWzx^^$e353r%e0*I|Wv9 z^-hekT=xjS4rdwZK21+VlXYL5zApRX6?j-qG5)lP(cx<*VSp0KOorTfTD z3xMGZi5}FFhXxSt1vQYpG6d53t5N07LB;OJzj_f>#bvt7;WMX^@DdgAiRk(ZjYT((m!ZGAM zc3LJJima+o300q#A`?LkiI9abQkt{46jU#*kg_0ndUU9tCJ1FBGUiie#2+Q~L~2(1 zap5@1AUs6i1cB28+yvBNlo{c85$0uX+%(RSrdqyuNFEBBp@bA+m!7q>ff%xZp@DdB z@0eew`fpU+VPL_q`|B!B_Vo-7HNd)(V|h` zbva8r2M8@5a!!-Pm@BS!O41SPeFc|R*T@^Rbs(o?&5+eI#vj+o)Wo_&{1l0 zq%hDg^YcRR$+q=dg8dO84)GQI0n}*txUsXtqk}wn2u5_7+O?0xr^XA$BK=TSGy?tj zP+OshKrsOqfFgoZqa__A3bH2ClR`p9V05#|a9b^f$j^wvt=+6l+) z+~TPcF}Fs_tpTs<$bVz(Mo4VfCN*qhub|;T8wFeLn9ZL2PdEUeDQNO@ICn|HUMzo1 zhEW-9`#LgMk!*Wkfzqq5iA=^ksDvWZ+B7n)z$6P2Ds(*vp>?_?rDrcE$Y#v97%2(g zr@Eaaz(@uOE4tjMkFowrbVlD_b-k0*_dosHG?Cp!mmbZLKK*JIbm%4ldUR-miRDOL z=Pb&Tv7gIKp?Mg_FIh>`6Hv#+FNLgv2jN+tgGy62gRz{8!63oPk48sgA+yZAu#ZcG zQQzm7=%2C0%Bi~@K?LY8ZCqjX8*66Hh}9dV>WyN-CaGZ4&4P~Ef)24@r&O?W&QrW# zHM=k$1TbMn-^v5IGdpVLJpPFTw>?EKwNLfGu;Zm2P(#WefJ(w7L>;?g`sSwRHchrY z+wpwI)I+nm)rs6{7P?g|+#nT_BK)=sn0LDCb-U;aNv=@B9=b~kiKjaqoq5~`d4bM) zGgG$c#3B{1JcgipymAf|mM@wE8rNFLpbMmJ2&dI9t%Rq$9;>9NKFw&HjrR2?%|nrk z=@|`SDcMVprit-sr>{_p^g(G{p68#OGEW_wu6=!-=vpti*57n(nssdwU0WpAmV|wa zJf4YL*U4nm(NSj8UE*w(tTTpKW_*dZ$kGo-{hLx(v+x1ZPv+AE45{tqsTo;abOk;c zB|m+QWyxSSe+$T^>piu<@=8BId0V4h>5GVN~^Ytu5FTQTf)ALRhgt6;;0O4 znj!;HC)UU~HW>>u*EF4F%pDD4on(^^^qiUq4L=A{j83Qh(~=L-vc-}QFUL}aby++k zS546tiC@e=gZLHlW|Ebyz^z>x{#}x2C@etdO?{`J>rxnP!y0x$OT-lSbw8%fv z(lS@OW(7_Q7L&D#L@_O}Ijle_df=Bea~V{58XX=IQ+0TTkd#MWN{FKysr;1S5C|Ps zAU_>Y1wL{`?g*4`UA@35K=mhwpvl@!IcNc-&+X)v5y)ej8n_?->XqWbO?R`<*vkrz zRtSPNRd7^^UO1b%D}tj+dc$_+E(?zJOmK8S1SG*RCz2bx2tlkf=IX6z{@{*ih~o$6e%_^3vO zl@yGm)*U_Ab!^>*=H50Yaoks&q)hMCsv;%Li$6-kF%oMe@lsX!J)Tj*ycbwH!sik^ zh3}$Wgc$%G<)?u@=F^&rsgmYl^r*YU>_E~itDV^W{NV}w$^9?}=X+(HgM-5tBRpb* zkBMP&MuIkdz$TD#fYg5-a6xl}m8DlzN zll@9~jsu%q2R4~#UFy=6FU`3wlJ52C_Gdi;8Bk%ULzbR<72Qw5lqF6o{l4JQ?!)V3 zGXS!v1P|NDw7IaRUI&}*Yo?@q1m+?ahXp?5l=<5AD2~uVfRR0-vq{Hsj3b$+rAjBE z^Y0Vlv5T;X7U)X~9Rx_rCH#oMb^_M{^s*I`=ZkgST_esPMQ@CemYWz3aJg?d-yTm? zZWCSGB^M!OXX#u~U^3^nr+ljajqbNwr?-io^^#{jdF?XI`zsT{oua=}@^>cOowti? zrn|)ARZ{V)iT&W5od-;Fp0b;shFMR;OpfSjl{~Frp9}o+rIl~webm_ben@OQBsCrq zOAkw>hZEkzw@WMMi|amW*!tf7#L?r4hKIz4hopvwU@}A5&#M|ECZIy%ki1 z112q;FDiZY&_}Dczc(PR-Y2czCl>9OiuNbmsULW3L;#Z=BaO%hfsMP`xDN|z2)3>5 zsx|+l#ssJlRKHFwN}WYFlF?!^Pe}uhR&1rXz~!_79$ov(N(hoU(@F@Zd!maWnmu2j z5+YN!XOXQiM>RW1AhQmFSRga;&G!JQj?j!zy;B$2;^{{OEZGl(i`Ilu!W!j+%Vs`{ zH=i+SMdfjGT3AkN9`xelv_|)~RN70Xsp81bouQl{_; z%Il11LOhir5zdgC33g$!Zquzm)y+WjY@k^Tv`T^2$!zF)%Btpq^;1vIm#m)IB(3R`)^v#_ zyQPxdH%kuAmK+pI4ofA6@9%jXX8UbYN!!hmZL=lY#F7rFq=Uu!Y#xC%GkdQek}dgf z26oN{c8Y;rQefBoXZBd|u z;jWj7U}ilqp)f`SH-q&2y5pCj34YG;*)+ij{{?D-pLMK^rC+Hgn72ObxWIuGc$2rK zo?hd#Q%_fUJ3Se+$qplmI)f$T-!WY_O*l@Q_}pjgl8%5owVDy-=~7 zr~yGptIswpoq|4FBVuR`Wgy*saWqmfD4y-fCM}AA{(hd1J z(@88_uKX2-bFDC(dxhZ&mI^m+xnB4tSgN;Fw(7DK_Vq=|*X6k7TH&%yOYX)k=hS7A z<2JmhgiXI_1nbVZ%h{Lh+IC0y>~VX%DxEL6=)YFHM(1lc_||EBk(S;!Q$5G6)1Gvk z^pl?Hz%qC?f`mizm=gB=>iXxjH(jdqld*id(d5zhzo$(hZ;$Xth%A%-|A42l{L~B& z_z0r$GQB2NfDv*`)x^(^wLLTx8yy)L7D$4ofEuQtJ=hU!AqUuDvw3heB<3Mix-a}S zrTCu-+y+=Q!-uvv-6cWrw@oi|XRu)MaT{61J)3obdm-!jYzTFu6qi8AoXl!mpR|yL zzd#&9O2WRvRKWLgwha!%;-vfU7z^nHJ!L|vaogZR%OM41E-;_!+HfFg0_MA z@DLdY#0GdY62D}#gtF!4PdD4gRO`PZ@Lvi1ErI_65b_zO;DjHN*B=vL`Ue_r!jA!x zW|RY9a8PI^H&Va|%>*_PP&q$g1HHBp*a(oss)zKd^qlm%bMTo&pp&`i7;Gv0FnCP_ z#tl0oj1KWh7c#^WdDTHKHK{d>wPRn8q#V*&vTLWT$Nw$38MK1R*ZUp%Qa=OMz?UMe z#*TTW4Z*sU2FcSv3XQt?K*iP7)5l(Ey4p0^HQ6=q!~7gp(D+tMKB&XZ1$A?Qa`Lzx zs7-ilF(2qDC&k5%G?@3-CF-|}{tn6Ck#KjAu40EgAL#ac^Wt+CCr6)s>iMUpeY39m zgsYx~-YEJvN&Za<_a>M`PWDfXJv%nlKYeWK+$-m1j@{^*3117}!m66C$!(L{<^!R* z;v>+bl#(8$RMw-wp1*QyqM{wY$=o@m52EkgpbjWjbV(ImlezQNb<MS}EhxYw z*@Ad6!BC9UDw^g4>t_0I)LcI+1~y57P4{^%Uw^}MiqSdoStHq+VQqkIkd+l6-cgj2& zm<(W^H#VI!opZaW!O(VwS2SfYHDz((|FChM@)uXXu=b_33)$v^N-WdT^TRO`slSfN zVkh^3Gtk)}PZrCnN76*e(8xJi0?6S_&2I+0#C{;H9coqO`@CXP6v8UP2?LH6M0JY&pg#lDP3E%5N5p^ zizoj@Q{7DQPrb{y5T+p0RJRbMkYvyx{7V845ja7BX10ETDeOlm8i6kQs$T}UqWQxV zwu6>y0OW9+JKniGVN2L|%RkY+dv4SA6*yrU*-?j2t^>9L2kGw@!Fn#IaL(bwY*AAm?#sqb8`g6={ zv+b(xH<_(wB>XF6oY5Fsk9u^|Lf{W&I!Qv)lE%=>%}IMT8h6`M=9#g_+2ToPr_NJXMCxITl>ZQk>aC$bpQr+Fbd0Hp@TfC3;YiFYM+oGwPtTpM~mF>zJgAWVCn8Ain4iR$r0%S*? ztz1hDA9ZIOW!icbG89%5Ai^eFXMRLaJ_7%aKnH>C1elPxjh=|OgKVFtXX?J!4L2SUt9D3LJ0=fI9=NT2RVBPbY=T;W^X1JqLhtj5@`Gae zL8<)UWDfli({qUCr6EyxE9TsRkIFZEwDC})>@Y+QFRbMOKunypUrtf|oc9u(i)aTO z(s^eU6he&iRwVMuV`T99L14o!7x$s7bk{obhigrMnq{~2y>!eysofYx_#Hf~l(}EZ zT2hb2^@(Ba(%w%DJ|uIoewV$xZIZ(}IfU|UWN z0eAT_G?`_n07%KA_-dGZu>T5^kMvYcD%-|JBNXV<6TA>sQY=S0O?uLQ#vz!%Ubc=~ zr)~OB^4qutgoB22>O$AhDqv`5;+7~%|;BA}+!nNt_guPn}JYKoPqdyYdh zMA%ZuMx)92kPH~&QG1L0rD$CXixZ=YGR;tG=^>>tR;cYY zvJ6yA{pL^LNivYxSw0_VoQd7&dS|~F=#T;(li8Emik@d_L1)3{aF%0S7gtZ$eALh( z7VVIVb}X2)d{7s?-1B0Oyu1cFqgy3q3)$F(1x$EB=}mXlth;KuVW#z6>ka-7qd$oL z{_s1);`%+(`aPn1ujJmFaPM8Is6~zPw6B<(%BeTMVX8NOud`~Gll#z_v#ZqnVZa2a znIHW+0)H_kF}fC&En-4w(X?sG7ikESA+ttZW&+7`I%bKw0UN0!fCgr1xiugSD?^Xd zNOke0AA6Y4-=bq9bV9yiJo{FrTVX~5#5km`42FOh3Ce|~0P48cZbE6u8zwzqnuhdc z)x>lw=>Zd<+0*D&;Bu!5yP4W~|$Rkf+q^ZN-B9zR&QZvX@ zq?VrQ2-Fjx@-m|n!AAt*U3&G?6Ir7qoyv>_+XzZyQexGhOqPq6;t;Wj5zS@fMV@z< zz^k6R$hfeTsLN}Jg@tc+4m)x+d{omey0%I#XxyzZpqQ&#b+f81h4^-;)v?|a!l za^i-c=-(&#_a)r>pxP-egT2PQuYUR|sbQ1o+bsDu-}LR6_3aRSoszF}X&+dF)DXj5 zlRwdgP0tK%XLgp(m#&@jmfrNPnf0y_z3U|UZ6)h5^a{TT2Of>$vHHDQ4kqbT^oa7?6Z<;o%adELJ_z z(!Uu@NtRj|Y17Bjzro;SsdxtaJD7V|czbFSmi^0VAas6aeSZB}PiCdku;Jtdpb20W z&kz>F47s(Y0R~pWI%yip)lJps=+lLDpoPy(rMFL40s}GB`7Kwvyi{t1taL6d=`ogW z=*POUIWv?k{|haf%TP9r)@4!IhIM6gXDD027g{#ut19hQO5a$%7L~0^SGGc9*>t|1 zaZ4uFCSIbq>?i`dBbj@zaS zw7DwJ`uc;R(1coY92V(YMgMj=Ti2frV^-&97ze54(G|KjE?zDM7#*8dj&!!0UH)ox9NzjAp|F}`vc zi&U8=f@TqAws0w98MaD2X&#IW37J^WHAo<7rn5kDm^q5P#|hJI&@{I2a{z2z^i6su z_R>(^q=S6|w&f$^LvJBW%nraCv8Y8VYDu_Tmg)mA zn`(ac!t)ni^-MkRivOxV^_~57bKXrq_PqDNANhaef8S?Nx09B>X&wd-`7h>AouBER z%op7)lDp+b4+5st^?#FJ^4t^8MxT!^ShJjE!k=Qm^$7n9P_b5^wac;%gBefINiuaL zj}Y>V$SQg|NPyfHZ%*c*_?b)wQmgYR)%)5#qyuHPdr12~r4}c%-2}IRslwNwzj#6;)*)Slm@cFRXr;Rl8pv7?sk0Rh$r#iE)PT}`yHky`qFoUfYTY7%% zSdf;u1`qGu)2zlb4iDlZ*znNJW;x&rN@cqH_pEqJT4OC?s-`nwLpt&WwW>vV*537; zsGg@>S)BA1ch?Q8DOVO>&r71YvwE)KT(sz!)5+Xe9}e*XQ*kDK7RZGSuqu>3X~rQa zNei_tn?_6K%3GD-2(huqQhT#_@oBVYr!Y&p-y^<&c6(DT>0kpk?=E^NcfPptYToqm z?>_#W$6xDxy;nA;oD0@|*ZCdiYp&N_QgAD_X)3@0Bmgj2WD}SLzL%XZI_XT4JI-uE zq5z~}%9nd$3o=?_MlAm-+JUXP#08pMWQ}xlBxj~oaQBpy{wY)g*AvTZG@dxAdih z^JO(x56yVLU;e%FYn9h4#j-6@*_OH5Ro^}Qox`skdHsk~y8|7=2g_UlbPS&#dN*Im z%ZFb){KAo!j@)tCslNa$Fi;2$;fibtNzeRr=a$ax+y~opcIBEs%rOD>2>+Yv`+op1 zLVlZ`X%bwsEoYi68AuQzHHO*#In{X&-ma*B*wC5G3DO z<7csNB0G2^nA##a7Cd=UHX=DA^o^Xo)Umy#apTF8bSw=dQ+9C2;GnXRG6_IQ+Y=XY zK2uDymHAZ^vmawXz34fYh>8sl#^wIlUhF)ag|!HkVUrQ<8B?l7*H5U^kYX5^GtWJ- zTmI@~x1eq>f_ND$yOixkQog#`N>$PPcPNQrwsPgBUEkKtBkAJll?;Lp$`+MGqUh>n zoKJ-PfXT3VfbNK%4Cz|b#54~X)5W~wIM(Y3ZtYy&xIrZe6?rQU%L;@m)T(O6&DcJ~ zqR$^;%c0=HA-TVnmDULI*(vp&OK0n_C?7-XY1C(0YX+@hnRauM(}uDbOu_7W79+Jk zx1;Btbu@l@-R7}^b#(3O-n+jic=VxNhxT`k6>6Ox+<&6;c(*_{Vgh;~_c1l|n1wba z`Q_M*(Ls>uV|`+Z%@|Jw_y&P@0g^WPh*a4`Y#TXu5NIUOL|`p}bp&XVPOw8^4^@+`U zrRKefz|pCiZ?FE=>gk8Yz&a_gE)iJwvtyIl^R3(8>;Gx1*t%D0-J2*oI@R^<1K&C@ zeNilHlFFJAWlad=xaFyzX-IgQ@uO8N17cva6xd9Qf#!UnnTHd;4fv7m$ffr;Bm(=y zz&e@c~r-BQ$J!1*fV%o4)y4V z8bcw}5f@#VhdQ|Vj0nd#+=&nFBe75=HH7Jp!O$z%7gSDeIe8K%<$yAzPq1OcMZ3xn zv;^JjtoK&SRk9?Ldm?Ol<0s znz|Cjho;Qm&iPi(^d_-*ja0lQQM~47o{9a+5}y5{r(W{ZC-PFu`I%mXR2DkZJ7S|) zKP3~pMRT$DBX%qAsd{4!o#pSM5n#@|?_`D%V7_eCk(aDE@gu2AyY^?1z4m7@^s`~! zMU$?QK4yKan7cq5yrs5-YN)3guPh~Y^!=G}G@hUnnXptfbpVet8vD-lQJ;mo+AlE* z@tk3#Vz{UyRM$rr4N=5zmw87`vS11O&qi>9jv?>mllp~6`?xZi6;o?C=*Da6UGZkv zKvxzTKIv7^v`Y#Lo2nSK#`41*hi0Wt8j0{2>^MA%77VA`SvmGyl=yF@e=JGuRG_Vpnn@g=7^df}!n@d?jYSCm``KCMS zGbL+toKYobkYRMC(IZBjuah-oBD+Sdw4j@)HcTYzIs0i+V=lVhgiBgt=dp7LZ@p@U zNeOM~M3!~Fbd+ZD1>=X@5S{dz2nt|Gt33jsAq$O6^YD4J2P1pgD z^db#9;Tz|c&~dH90_FvxU_aTM{4I*2#$(o^iiIbW8%bnL=B0L#kf+Tzlau+uzKa5} zndu`7WV24vuCWclWZ{#@S=zu;PN1EFY$3qt91}=wX_PA$jH)5e#ot26Mav16M8)~Q zd9fRajI)(!E6$IC>)&lmRCbE4U6O0p0_S$FfhP5)w|3TBJ6~RR^|ATV2B~xnj1+x! zq%tL4y03831R_@3IMe@8YnNEFTdLWe@INp)FgftE2JFOz1GiVdz2-*WwZ`j>V#yY% zWXsKxowFr7#gbi8$*#H5;9O}1rdDZT5x}Gwv#Q=wTJE$49$4d4T6wj7y8o3OS9c`5 zt5cMBYwWjQ%Ed3(|k2og2tC5wL2~UDo zYa(`z=C9bbSdpPMm|Qj-afzvG$rjAjmref$8bIx-lrOFN>|%&{tQ-VKG2A-acOeq2ezH2ffs$|wl^OHxACgZcA&E-e zTnQ7|X_W_~6F)@YOnV*{l-N7MrO^m*j7lj`eOdwqJNL%-;;3fS;DMq!II0{r0_&X2J9DD7t z*B=wTEt0oo5{VU0?Vt73B|LRlPau`x{McbsJZ3*`_ zWg*}`wj5Z2tn8`gc)trv_OJ%{Q(6tYkL^;u5uf&H&5mc>8COj#|N8cj{=HI6J^Kes) zkJA1WOP1uB8%v4QZAgj5XZ|FE8jkQ)HeE3UdX7S}M81(R5lUM2{|)kAbh09=L|(=z z-1Lqpq;y=djnQKin81&K5o2^}*A?{-``yq4FM&Lhqkv3vw7}+bV5Nv2H0F|ZjvfSx zX(Ct;HvBTpgch>aIy%w)j8L@!VjhplhC)FH_0cp`1Erm0Z<+U@rovPx`-X?;`O9>; z^6Am|C=SEv6@Z60;im*XAV3Hdr-fgNVUM^|{ep$uh)+t|a1aC}27y!?a@QqGRd&#+ zkV8+o1W1TW=0u+Ck1)v~CZG6Zr9&iQ@-a9>4wIM`8yJkq_}D}7XhER7F@c|+Xfp@- zXgs2J^2Wk>P%yW6{2N zBOTTq8RBC?B?YO-6omFO9EjsU@|X~$K%wOWHKvaX6sjov+7*Xy#2JCrh$1g;tEDFr z=;Q@&4fMpU51D=OYI630VJ#m+8{wz^j+S;18r04&V>sB5ahbsuoGOU?O z&1{BR>1hLjHUflF*)D|lZ7${bb#@T5K$4>@_%RWXiI3aJZ99Pu0uKWuJ%`zW;$Ten z(8h%F=?w|;HWSVyOqI+{Z4>~8C6>e)?Ci*pqw17fELotqDcayz(x+HB^lDo}Ap0x% zc<~46=@@}W2!sicAqm4`hBJ%q*|*8(o_7eRP$2xc*i#sQpgbni99Nyt|88;lSJ>Yi zmwSc%X&!Q}u)jI3CXwkk#~n?4_TMe8A))`x>0edfg{&M?)&lpd0>zvqTQYklFU*?D z6Xxr^$B9S3Uih=xe^xGv{5CXxuG&cV9Vt%U+TwTPxbv&U{t0cf4OA+K()lIh@pt zZ5*qnw~K}AZ*+)-omURtvilOnO`^Ri(b6T_yWSrb?GI7NB4pt3C5qOG_H~K%J4O4> z_d7)U1LWmj$Z?infG8lr`4)neCKxj7!EVz&lL=f+W;kiS!+bu``k%-uHQ{XN#b9#( z9di97?X|7TWZJ%f?d38-3Qj(sX#EdmaTZuGJZ?w17A*Z)R`5!h;pC+|%;yuW{{d?} z%L+^9%y4qx4)gg$>py5Mqf8cqskPX?-&+1j+H2dXEGw}`OW&wa%=eRJ0xew3mFp*0 aGe%6x2aTPpx{A1;6lHf+nST;A0sdcPZcBLp diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jwk.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jwk.cpython-311.pyc deleted file mode 100644 index 819755a2da00d09cda8693f30cd77f18230d3607..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7463 zcmcgxU2GFq7M@?nP8>V2^B+hD2&7J%qyYjcbW^f~0;QXVCV|#%@fy!0IQWk{6KGsR zBbJA>8>vx+M5MM-R@$;b^q~)xwhz0~R$6Ia?Z_*vks?J}@zA#*SPAj8=iIR;Gl{ch zwc5R&oVoYhpE>v5^Yh)CKU7y&GLV+0zn+k580H^XC?&zju#dlo%pFE%WH!dc*&xee zo{Mooj=uRI4{tsu#KoWpb%HF$9Px@^Mcf&5#$7=d)KEtbxn#*hgs%#zSOXh3tE`0C4`OTYbhEw~rs*N7j_L_TxZ~qc$wYJ+v zJ}@17uj?Fq?T^pVQ`pi|$ndgwnXU-w&j_-^$g5mNPU?o*n7f&Wb+LR;ZvNSIvGiTCXsJbJi=EhY8)Pu`S7yMzTZVQ1vNn_U0g?hNF~QDbr`PP@Z24emXt0kQZ%7z;aE(OyVHBe6XA<7 z1uG_^30Vp!B-5k83J~eC0wV^hi38SEud+r8SE%F)bsmBVLEoZRMpZg`BBJPm433^u z0Gl|G;8yFd!BVWzof9M($3bB2m6?F33#t;EAgG0;7D)q=CLpQ^M8foJq9;Y7SHqgp zb2UtQCXz8(A!^UmWY0|E%12~sVyf?;8ak+@B8U2}Tzxf84)&$BRQgKKSWhaN>H@b7 zqIMnXo4yoE@4wO$i(c$W&1jdBiJqxzT2D9?MR~eYGrBVriYB63D3tb4&);1tquW;- z!QIR7C;*pl>mxY|CEAQ!=;|pHjI>Wk7 zv#Y*c+1)3<)4sm}FL-jkQ+eO1toziOw+UPHe;r&FzV-ZB$nE?^e&;VJtI)LLE6;3gFtF@j7$`^`3#S)O7wVf9PkwP>;ljTupINEz$kulhq!+Qr!3 z_8PisG6tYt^#Pe>9(wB*Mi&D)Z+qU`o^`g9UhFF%l+k&BF6D*%H8ipdx#90KjNPkV z=QNyLF>MzOwoZ}sK^H)k!jzg!=wd7xmQ^}qatJGUau{BQN5BP85%Pd5uudL(+9r>T zUt;6EKw$DO(>BMidh0&_JJ$8 zR6|isjQ-fm=uhi8Bafp7sxY!7!f_=O(knxucv7B@Vcs1IU6~HYj2iL^G}YY@rL|}z zu0VW}sV$J#knc>9WJ)303^@ixGK2)xl?c{*ZoO-8VUYi--W-+c z8l7(hW@)D4!H9i43@po`vQ`#v3B@C&Ys+yD50rk{y@l_9<0l*@gB5ZO*mS+_q`C!% zTwMTlQ=e7GqN?_O@o@X$hVIzBDq<)Qi+%hCSpFSm5?o5ilsChwUacrJGG*58U2n1) zqM%~Ruo{k5O!lC$X`D^?8_sp?5cA=Y>zvE((uC4z*0=5aw`nB1d@M8n(jLZoE^KEA z&ob{Z^M|i7AMx)o*H{e|vY4#y)IO5T7tcHP^$_f>Zo(NCwn7|Ew~t*?Bv^V0wnLJm zGDYu^WI_u^0nH|65W}kJ`r@61*%EFrrWJyLG(G-vBTTClCF4Nd(uhJXg;VNgY+XDy znBe;LfRl{C=mZZZBmhJgP)l@2IF(WovSFfVo;n{>5)kJwC>UM3AZLqn)FY&8OIl~5 z(Q$0}Hz2c2p{@Zihzp_w!k1G3UtY*cZ{&O@^S+Z=_esD;i|yYCIq%-Q7p>d%Ds6B$ z=j+e=`m^qSKsD=(=mG=h5B16Fxi8cfW5_^i^vSFr6J z`;@ zOiYF-Lfr)fBf9VNYoA?PxVlJk-rafc?m1!A+qfuvdSm`Z*16O0Y-Vh+%@I8X^(7Y| z^V|rHeGY`SW!6P}MvlOj$WbITh*EII8Q_fKG7Bc9fok*Q5tAjM`*xVDv50N<34&PO;n- z381%hD_T8x7PuG%&w|dxI2cFaasZ5$n)ndNpgYeLuR}|wOaBZ3%uaxylg~pbJO8kNS)a-ibX~;Gn z`EC>lJUP$Nyys}vd6cf=a6m9bJqyEb#3$h5-jQ=A_dMi)Yiga*w#cP3qBw`5BZ_XE z

&3jpR)vuOOjN5LJY#0{H-Qze0k@(10jZi;#@LpPB(OYyQ?bzX+bePCARYqYaiq zG>8Rqv?1gG@WP25SZ@$ZeaE^}5MNtowjqXKC{6_a%!En}iG92es0Bv2WFBFJTuI@C zAXmv1P!?q;cnP;&k7hpxyLujO)yJZ71)y9!rB~pM3Fs0vAe%PhXQ(!#CV@8Ng)iic zWOYg#SHE|m@_wlaLJ8<^Bn*rtqs38lK?4zVR~+n5OD3Wb-GvM)6SyxLcCekzec>*q z+se+vj6S$$>JA{Fr;Tk8ecY?fJHKknH3#5q4Moo!{EUS~eX%((aIMCSYanB{-;(q! zL8-PJJ*vP?=-m^nWO`S52`x=#VGI@ToRe8XyU1 zf{LfM0$t}>mMt*5v-W3=shXv~!bYYc%h;YZ=4^KRr@#znf9@$TEm`}s?l{4+a9S!) nA5s1jYwZZv&VoPNN**Ea$wtL6>tbQwwvtE4ds3!??&ZG#ntV&i diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jws.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/api_jws.cpython-311.pyc deleted file mode 100644 index a453035ea35157b5d736301de466348f2413c592..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14276 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 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/exceptions.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/exceptions.cpython-311.pyc deleted file mode 100644 index 05a67bcdd8998f2fc8cc1acf59f25b6b39b7e95e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3855 zcmb7GO-~zF6n!&e8{1$E1Zen52ckB0RIrjlqzGDx5>ydXt1U>C*o<7yGmJYP+i%7c zEEX)X$f65&AXSmFYKz(*&|lCs%gEdQ0Y}a%tKNHU$BYN8nP<4b#~Rrc0dtt}yl&T&=Pv5hnwuaH)rri3a}oZh*L9;F2CLO56x=qaH3s+!%0o zJnf4UHxAr{r`{lOlfZrC;f9F23*40Fn+f8kft&Gg!^F)3H|OD!#N7k#W1j5k&kmZ>OQz0+oi6OBkFCm)DC7$(%eJ=clygs3Du+*tVtJ+Jlxvmr zZn|ug7oDuw=g#7jmFj_3dt6DI#!u<;v2#$e(uH4~bT`JO^0C0FOMpFK&j6f$_%&oX z9BnX!C=161S?H|79)}0ZWVtTvsB35JjM` z<0=$G9Y9kv3DAw|O(@!l1lns@iUII zD24EeTN6zIG?VRK-R8%A8h8&i5K?Y2GzAdrERA(@za$Ljpx8%JMJ0qqeCRF>G`S=( znP6!?1h}4nIAm{K$%pjNi_l(3m${Oc^)Nh#bp?`Bq(1enVcUka{{yd7artbRS)&zF zJq)ScDApr_!wR-CIhiC{fX@nCO_j?YT8jbH8GKsGTuS0Tyg>#G+1$+2i;Wdk zRbZ0Cox!jSN7HHxw??tl@p%k2Y|Kih?9IgV%h0d!m++(Plz*1Bc8BX9k>Ez~(n;e}Ew)@Own!*TA;kq`L z(G)-@fv1q|9o{b$rBMOlOScr70!WRJ)P@PUy`LHmgg{t#tDz}?+VnNGjgn>Y93G?k zbm$i-hp_3^MN=jrvYLH_W)DYV z85`7+2S2pXLP|{IfXRc?Hiecx75dg+ur>-T3jssPLtctX8Zde4nUz-Yx^p=@XXczU zXU@#|_Gu&%L{PShkLRBI5c-QUZUNpYum6P4LnI@a$)Qy3%cK~GXHzVE*&LVWQ#^xY zPUdq$-k0(-C{U9WQ=%efU0Wy>&>{m!2@WDGfV_|j+(8<@jP7xF&@vAvyfQMg z$*v)-eE^Y0gb;$*h~PI1)mc9nA&*tw^}%a(jag-6t{#&%j|M%7Qx0jc+D*{m>pIs- z*p)*l>vzX%+$xt9UG5Rw9`J!fyz`#u+#~c@SYtsVYY)HCBU|fK9Z@tg^DW$;tuJ^-f!5fba3}!A*V|B|wf6aOp7^#iZEXVkU+ay@1JxBl@vqC~^#z$-Dv!i$%pCMogDx7o?05n^$srtVqqmux{VWtT~! z86O2bOs@sgyRiK?rmtXJz;?Ut?4CYkrKvt$-`3{9f4fC5>7_GROK^|LB?%|z3OO0_H<`^OOWNW+oSn;#jT-4uqR)v;+2m0i|C0;G`_wFch%z0*8)2^tk! zk5oA-vcJFo#)mq3EA3}@+Q%yGW97*`fBQz~uD{>(_rK^%RQk^E^i5RyCQPvxniYs{ zjPC~e%s}6Zo)eXx_f4^LuQ^s}9;zZXl5mJs<2jeFuL~|;6HHgB;E1|>>w|7IM zW@ywp8Gm~F$!)78ZYCxwiEHMOPyXT9p{9Qk5HsArUlmdRY4D)xL+$;0ZT(hs7*5_z zPYy&xT?5Yt&zr|CSVy}be{Qy%t~QHdUlrAe;4^6Ae;Gf8ermqR{lQ%1JV;Bi#zERS z7(DdgEOD>{rlDjTcuE@R^nYxX*`K*>(0bO;^x_F%EJX)lB{2n{PfEk^tTf_eJcWaU zAR!sCPp5B8j?BEGJ43Dp41kLCf&#hXQ#BcKauVLO;8=h>Fd)kXxtP}tOrOw?=rwg4 z4+63IyrgP43Bw8K#%UmB^pX#JJ^0nB?`FQe_2AYP`Sl|+IKC4cuK?xGRrqtJ+wcgr zA*R#RN;;ijF^#|^+{ssasnqe#yDm&Fe&1I)r zC|Gu?g`%dn?V(eqw^ez8>9zQ%>9)P@c%}QS$sdEZZ~40*r?}db_ zigCubE`M`&gWs6m8rb-3v&n27sx-x`2o?^L8HQ!fL*95u5b@#lm0hmW0EMNZO d>iw%_r$YAdT`=uD=AXxo2=`-M5f{RfcHQt1Ey diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/jwk_set_cache.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/jwk_set_cache.cpython-311.pyc deleted file mode 100644 index e81369b8a4b8bff0270571508b159b3fb96d30e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1938 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 diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/types.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/types.cpython-311.pyc deleted file mode 100644 index 39ef6cf0191c9be62025b560d44ec2d98619a58b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 396 zcmZ`#y-UMD6u--*AGEZi;N~QBXb+?*2qI`5M8QQt$WqeYrA@e`m%D3CI(6*e&`lS? zP5f^h93)f0$xZ0i$vd!%;QM}$-}~Ub)M^!=>3Qus+qn10aAv1|sOf z$a1X+xd;M^s7;}7?QVO3Tc8%P{?v*DU4w@GWl#;2qmwwx%UzE#ug_?1p9KTex@wg2 zqRMy>4~5w*Y-0-AsA$%U)aYLB~m^hmmMIGsR~KzQH`;P34IpRn3kw8TDod3D(0g&(vrf3e-| KUNtPLddv^D)NC^V diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/utils.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/utils.cpython-311.pyc deleted file mode 100644 index cd585dc4183bd85e7a1fa105e0b2594b651c5cc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6924 zcmbt2Yi}FZl{3TPb1XhY*_373rX$HVEm5+bwo+TJB2lqoTXFQrHmU=L;*2E96iM9~ z+Ll12OyIUkkZ#m&3$q^r5G^*VHZ9)bpCIhVAo^7A~)I1y_}T32oJHWtf#(!T1LcdR<+oh*|Q+8D{z&JYXByvL;Y`^*E& zd(3@Sv`V}rNJkdAMOdC~rmpKlNyuf#h6?*_b3rFTKyBMOqYT0a7HAJh*U z-wWe60KQRl1HGr($0mBA%`f`sm`KwI^`@FJ{ZKzDHUa*qc#O_t69dq8Ol&r_9f!8S zG9$M9k)3bm7>Qds*q(SoY%R7NY__H#HGc`*{t7-LZ9pcTLnSlff4XkQGv;yUgG-F~+8@~YGc|qNpzmuUJHv$9pW)C`Ay&7}r4s2B z6dEGQWFn&^;$vBIPYQ8*lPJa0q7)TnIVvZXQZXeW6I1As@2QiIDe%E2a?x=oX#Ii^^Hh0fI^vgWPM)Q`C@*g$V)B;j%lP&@ZW z0@icpu|8%)mZ=SER#^;R-UvhS#?)JPNZZdmWoTd1#r5} zV2f^DSXU%jch0V7qzEBt5@LxR=96*T!SBVM3`yLJDN@h980lF|Cq;?KJu6E+>#4N| zWMy$>a6pa@D4F=s;M%?6RWdNRp=37JdS-hviA-3DktIn94-ICQqZ@r|J;}sEPi9?N zPN#ZS?khc6C6Sc7GwZsrm?m*4N|!>sFfo4eO#pf3sol5T@r7TtpVaIpD~#3JU-mS8 zp8GUccvJPXYo7L!qy4GVza9BvP<5WsoM$SGh0<&t*>wj>&AqC-PjmN`9DRE*9-~_m zsZ3Tm{KWqi2;|0WCYR$x?k>s^LxdbFT0}O-ZGd6hu)@~> z6s!WJ{j_p0V*Pj^`25r}9EhM~?cteR4e^vkx+N*4blXBqmWBp&UzQ~0*dp+7Xuwc| zkcBh?xh~NC8obplL|T<`x?nI!z`ioN*BAi0M~!|2l^P9$JOu#w-*~bhsg3PgV|)I_ zuB&0|uIg&hTrGunRM)Be*td4q);k~H&)+XQ{M$-lt=Rwj`)c5v>NvM0>%fpP$wUf3aEpJj?1g^}sRz>xryO}wgV zft^U1WGpU$@FUX<;eUxi1~EAe6zxdW1^^gj>@uPX7c}9*H$vYNq3`j)*M3#FstH$1 z{8c)+ZUI#^Z5@vRzrEi8c*Nwuo!x~JEY?$Nb6(bXlLXeW#w=O&@&q$b3q?550+gud z)l^SOC65Z-M$4_NtOOUNU@VwP%LyfMPYNbdinJsVDv%(fy6asur__GvZmP!7MUq~P z0v$O9Q&66TI1+@i?u1rD9%Wq3z9jmTbo7NVX0hv8K%a+Cz5yW5l=o%rsk^y!d{A`{ zY3`wtV`$fVsvxW0HqG0XpWFqa;g3Fi_+i2K#NJY}x0IVsY&my@6NO1tII9U~OZ?fF z#F?LO4JE zRB(Sj^65ySeWzn<#H_QrC6!&(odAr3i&HUzp)eirPJ{}?Wvs~MSTuDxW|1=tVy!^e z=r#8WO}3c%YOYby+~7MdUU={fm!Vr@ay*d;S#-xjB1Ox(J=V7nU6vk@BR~Lp-whe* zL1I@XCW?}XNV6gIz~&Yt5=ALQmq>B3%`3A!03lw?_>%YIc(^hKfR(;+1x1I`q z4dm*rZ(74oTEja_YU_~JI;0AhG~rT-zhvmW&G5t~)j#0a1PuLZXPgpyHa1|MuFa-CNydhi^Of ziD%1+o^MTcG;awNDaqjWp))iG4)`Z~_KWvDF9TXg+RNWfK z@Rjf|&EZ=03%~$R*%io*L<0<#M&jHC$ib%Nhiudxq~JHLwIN52&soe8WWrkOu{am? zrq9r4kgEVt-|kB5u!jwapg>cF3E9XPmd63;4r(tFifpJJOLSJ2ZGeI31CrriA!?^e zM`HJ1MC}9(K5|}u69BN^)AZSg`Rirh@xth@TZ?DF*n_1PP8aQ}a9$J6e>9#S&9ChW zEuf+aqv@~LzV7`xp>|%^I*!aLm>zG6B@| zm`N{`HhhyQ2wxIn@Zeqr2aQK#c=t9ep#dQc2#}kA$M~HJ0=bSQG{0mL!CL@cmIFK& z(ATP77`HcqG3dKUo(J$MIRKA;>7(4k-1b^w_}80H?Cm9cI}H)KRCic&hf9v|t|L&e z7hT2UMVHpxsXDqeM^}FIDQ$H^du{8k;QhO=qZr%qslHCl*J(Cxi@Z3js8R=&i_X zWO`=L4K0zeU@Z|gP0x%5YsSTReH@!=#=ncc(o zw{J(LfZjANw$Dz_&CEu|gEjs3Fqn=^jZMFI8!@#j!O(gt@-Ma22eY&Qj;`p?t}<>S z4C$xg;QMbx-kh8Yo~MyzPVNFs2%8bzaLv`-c@5w`ZDeXZc&E1peNDH3Njlus-iD#- zTrC*J{d9OBQtx)_HRVt26qrGNA)QX@_UP@%t>|aC^@n!J2AHe9fKDQwuM>EoD z6mGk#F@-Dw`XeN11%Mvc^E>+^_ix<$v8*bzYeIX8Z>NS!M%IVp5>dkUF82O7osm+q zyxbj6ugZ93xifnH&P-?Mr@=eQ`<=8HvdJikKopk5zQMuXVHu4#)`=WDD8p-sjHfNd zML8Chmt%e5R6*iX27VDTQYEc2OD1gA?D zfap)?Zn^G@`Vk|VjM8@G#enOHxXDkRc-*Cqdfau%n<6-B*(7cq}Q8 zbRVGq4SLBKF+KkW^AC`|l4~6P#VcD^3jHPFlu@mPR0wAYhKLg(yKaf4)^%?bv^okl zFFNt|^sUj^8Qo1=z-VFP^q6i1L60x%)+Lh8X7nRbv?bA*xrvF%pHSBUf!PNzqVC0& zX0i(yQ5aolaG}7f6b;Tk*bf)rA!~nEBnv(9@+@xwcn%t5ch#==@BL#?RbAs6K>N66 zAFnX1wW;j!Zz;Qhj&A~;PXe7gV`|`{7J%1@rm{VNZ%55#f7>_y&=Y@X=Zxy_()?Xp zj;BJPaQWY7{@SW`3~3!hrFQsVhQD@V_z47c!f@Fi*dF*+@NNxKK84!oQv<LhqrdRLsRXO#33iDj18yO(1du;XrcXIrJ0Yq>inudl93@PHTZmf|!r)O-H< zF7#E+h{(_p>;7A5F`Ja8(#k|Sn-b|I-SE?B9_a1TxM$+KhH-f~i={9EyaXAZy&X$8 z5TH-it!VCMbUWCeOd=`i{7O0j&r#!V&yL=_NkpWJBS0fZ@KR0E2vP{}+(-g}4*{D0 z?;TfG=`u&i&!CF&y!>BapDPxYWy?%Mp8m>=Cr^K6#+j$TGSgf#|0}$mb(ZOnIH)6EqJ zciM`tYbU!a4AdV(56J3)i(@Yr{MdSV$H*QZrL}tCV%e~`gV*0)Vp c?JJ15Zr>|`j87H6b+<8pw02tl%ywG-517FM$^ZZW diff --git a/apigw-dynamodb-python-cdk/src/jwt/__pycache__/warnings.cpython-311.pyc b/apigw-dynamodb-python-cdk/src/jwt/__pycache__/warnings.cpython-311.pyc deleted file mode 100644 index 141bba44f71c15afe99c0cf3cbaf0f45241a5999..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 469 zcmZusJxc>Y5S_hKyhNj5VP|8PA}(ky;0IO#u?eDrkY+h^o68;dvA5Z~c&SqOA5sZc ziuh+NC9QuT60211-X%6V%g(%+c|7L5R4QeV@svDvPh|a6%`#ROV74d07&JHs8y>($ z6rOc}H~EWbBU`(Ms@_=+R7Hzswkg3FC`cBxH5*B$HrctNL6lPaBD~DJfjGRic<6>s ztEx{7LP9H`giOkW1d*Ni3U3e+CzfAqUSX!x=MHAvO)WujYH{3+e4Fw<_8gpr@jdst zo>}jcx?r7#8K*}9ubV@`hB3au%w;uUafgap!%TW)co1XXy~8XMy(q-;Bs?fi>hH6R zE30`@uKHUtjeg(9iZN<(W65YqciT= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - - -try: - from cryptography.exceptions import InvalidSignature - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives.asymmetric import padding - from cryptography.hazmat.primitives.asymmetric.ec import ( - ECDSA, - SECP256K1, - SECP256R1, - SECP384R1, - SECP521R1, - EllipticCurve, - EllipticCurvePrivateKey, - EllipticCurvePrivateNumbers, - EllipticCurvePublicKey, - EllipticCurvePublicNumbers, - ) - from cryptography.hazmat.primitives.asymmetric.ed448 import ( - Ed448PrivateKey, - Ed448PublicKey, - ) - from cryptography.hazmat.primitives.asymmetric.ed25519 import ( - Ed25519PrivateKey, - Ed25519PublicKey, - ) - from cryptography.hazmat.primitives.asymmetric.rsa import ( - RSAPrivateKey, - RSAPrivateNumbers, - RSAPublicKey, - RSAPublicNumbers, - rsa_crt_dmp1, - rsa_crt_dmq1, - rsa_crt_iqmp, - rsa_recover_prime_factors, - ) - from cryptography.hazmat.primitives.serialization import ( - Encoding, - NoEncryption, - PrivateFormat, - PublicFormat, - load_pem_private_key, - load_pem_public_key, - load_ssh_public_key, - ) - - has_crypto = True -except ModuleNotFoundError: - has_crypto = False - - -if TYPE_CHECKING: - # Type aliases for convenience in algorithms method signatures - AllowedRSAKeys = RSAPrivateKey | RSAPublicKey - AllowedECKeys = EllipticCurvePrivateKey | EllipticCurvePublicKey - AllowedOKPKeys = ( - Ed25519PrivateKey | Ed25519PublicKey | Ed448PrivateKey | Ed448PublicKey - ) - AllowedKeys = AllowedRSAKeys | AllowedECKeys | AllowedOKPKeys - AllowedPrivateKeys = ( - RSAPrivateKey | EllipticCurvePrivateKey | Ed25519PrivateKey | Ed448PrivateKey - ) - AllowedPublicKeys = ( - RSAPublicKey | EllipticCurvePublicKey | Ed25519PublicKey | Ed448PublicKey - ) - - -requires_cryptography = { - "RS256", - "RS384", - "RS512", - "ES256", - "ES256K", - "ES384", - "ES521", - "ES512", - "PS256", - "PS384", - "PS512", - "EdDSA", -} - - -def get_default_algorithms() -> dict[str, Algorithm]: - """ - Returns the algorithms that are implemented by the library. - """ - default_algorithms = { - "none": NoneAlgorithm(), - "HS256": HMACAlgorithm(HMACAlgorithm.SHA256), - "HS384": HMACAlgorithm(HMACAlgorithm.SHA384), - "HS512": HMACAlgorithm(HMACAlgorithm.SHA512), - } - - if has_crypto: - default_algorithms.update( - { - "RS256": RSAAlgorithm(RSAAlgorithm.SHA256), - "RS384": RSAAlgorithm(RSAAlgorithm.SHA384), - "RS512": RSAAlgorithm(RSAAlgorithm.SHA512), - "ES256": ECAlgorithm(ECAlgorithm.SHA256), - "ES256K": ECAlgorithm(ECAlgorithm.SHA256), - "ES384": ECAlgorithm(ECAlgorithm.SHA384), - "ES521": ECAlgorithm(ECAlgorithm.SHA512), - "ES512": ECAlgorithm( - ECAlgorithm.SHA512 - ), # Backward compat for #219 fix - "PS256": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256), - "PS384": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384), - "PS512": RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512), - "EdDSA": OKPAlgorithm(), - } - ) - - return default_algorithms - - -class Algorithm(ABC): - """ - The interface for an algorithm used to sign and verify tokens. - """ - - def compute_hash_digest(self, bytestr: bytes) -> bytes: - """ - Compute a hash digest using the specified algorithm's hash algorithm. - - If there is no hash algorithm, raises a NotImplementedError. - """ - # lookup self.hash_alg if defined in a way that mypy can understand - hash_alg = getattr(self, "hash_alg", None) - if hash_alg is None: - raise NotImplementedError - - if ( - has_crypto - and isinstance(hash_alg, type) - and issubclass(hash_alg, hashes.HashAlgorithm) - ): - digest = hashes.Hash(hash_alg(), backend=default_backend()) - digest.update(bytestr) - return bytes(digest.finalize()) - else: - return bytes(hash_alg(bytestr).digest()) - - @abstractmethod - def prepare_key(self, key: Any) -> Any: - """ - Performs necessary validation and conversions on the key and returns - the key value in the proper format for sign() and verify(). - """ - - @abstractmethod - def sign(self, msg: bytes, key: Any) -> bytes: - """ - Returns a digital signature for the specified message - using the specified key value. - """ - - @abstractmethod - def verify(self, msg: bytes, key: Any, sig: bytes) -> bool: - """ - Verifies that the specified digital signature is valid - for the specified message and key values. - """ - - @overload - @staticmethod - @abstractmethod - def to_jwk(key_obj, as_dict: Literal[True]) -> JWKDict: - ... # pragma: no cover - - @overload - @staticmethod - @abstractmethod - def to_jwk(key_obj, as_dict: Literal[False] = False) -> str: - ... # pragma: no cover - - @staticmethod - @abstractmethod - def to_jwk(key_obj, as_dict: bool = False) -> Union[JWKDict, str]: - """ - Serializes a given key into a JWK - """ - - @staticmethod - @abstractmethod - def from_jwk(jwk: str | JWKDict) -> Any: - """ - Deserializes a given key from JWK back into a key object - """ - - -class NoneAlgorithm(Algorithm): - """ - Placeholder for use when no signing or verification - operations are required. - """ - - def prepare_key(self, key: str | None) -> None: - if key == "": - key = None - - if key is not None: - raise InvalidKeyError('When alg = "none", key value must be None.') - - return key - - def sign(self, msg: bytes, key: None) -> bytes: - return b"" - - def verify(self, msg: bytes, key: None, sig: bytes) -> bool: - return False - - @staticmethod - def to_jwk(key_obj: Any, as_dict: bool = False) -> NoReturn: - raise NotImplementedError() - - @staticmethod - def from_jwk(jwk: str | JWKDict) -> NoReturn: - raise NotImplementedError() - - -class HMACAlgorithm(Algorithm): - """ - Performs signing and verification operations using HMAC - and the specified hash function. - """ - - SHA256: ClassVar[HashlibHash] = hashlib.sha256 - SHA384: ClassVar[HashlibHash] = hashlib.sha384 - SHA512: ClassVar[HashlibHash] = hashlib.sha512 - - def __init__(self, hash_alg: HashlibHash) -> None: - self.hash_alg = hash_alg - - def prepare_key(self, key: str | bytes) -> bytes: - key_bytes = force_bytes(key) - - if is_pem_format(key_bytes) or is_ssh_key(key_bytes): - raise InvalidKeyError( - "The specified key is an asymmetric key or x509 certificate and" - " should not be used as an HMAC secret." - ) - - return key_bytes - - @overload - @staticmethod - def to_jwk(key_obj: str | bytes, as_dict: Literal[True]) -> JWKDict: - ... # pragma: no cover - - @overload - @staticmethod - def to_jwk(key_obj: str | bytes, as_dict: Literal[False] = False) -> str: - ... # pragma: no cover - - @staticmethod - def to_jwk(key_obj: str | bytes, as_dict: bool = False) -> Union[JWKDict, str]: - jwk = { - "k": base64url_encode(force_bytes(key_obj)).decode(), - "kty": "oct", - } - - if as_dict: - return jwk - else: - return json.dumps(jwk) - - @staticmethod - def from_jwk(jwk: str | JWKDict) -> bytes: - try: - if isinstance(jwk, str): - obj: JWKDict = json.loads(jwk) - elif isinstance(jwk, dict): - obj = jwk - else: - raise ValueError - except ValueError: - raise InvalidKeyError("Key is not valid JSON") - - if obj.get("kty") != "oct": - raise InvalidKeyError("Not an HMAC key") - - return base64url_decode(obj["k"]) - - def sign(self, msg: bytes, key: bytes) -> bytes: - return hmac.new(key, msg, self.hash_alg).digest() - - def verify(self, msg: bytes, key: bytes, sig: bytes) -> bool: - return hmac.compare_digest(sig, self.sign(msg, key)) - - -if has_crypto: - - class RSAAlgorithm(Algorithm): - """ - Performs signing and verification operations using - RSASSA-PKCS-v1_5 and the specified hash function. - """ - - SHA256: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA256 - SHA384: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA384 - SHA512: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA512 - - def __init__(self, hash_alg: type[hashes.HashAlgorithm]) -> None: - self.hash_alg = hash_alg - - def prepare_key(self, key: AllowedRSAKeys | str | bytes) -> AllowedRSAKeys: - if isinstance(key, (RSAPrivateKey, RSAPublicKey)): - return key - - if not isinstance(key, (bytes, str)): - raise TypeError("Expecting a PEM-formatted key.") - - key_bytes = force_bytes(key) - - try: - if key_bytes.startswith(b"ssh-rsa"): - return cast(RSAPublicKey, load_ssh_public_key(key_bytes)) - else: - return cast( - RSAPrivateKey, load_pem_private_key(key_bytes, password=None) - ) - except ValueError: - return cast(RSAPublicKey, load_pem_public_key(key_bytes)) - - @overload - @staticmethod - def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[True]) -> JWKDict: - ... # pragma: no cover - - @overload - @staticmethod - def to_jwk(key_obj: AllowedRSAKeys, as_dict: Literal[False] = False) -> str: - ... # pragma: no cover - - @staticmethod - def to_jwk( - key_obj: AllowedRSAKeys, as_dict: bool = False - ) -> Union[JWKDict, str]: - obj: dict[str, Any] | None = None - - if hasattr(key_obj, "private_numbers"): - # Private key - numbers = key_obj.private_numbers() - - obj = { - "kty": "RSA", - "key_ops": ["sign"], - "n": to_base64url_uint(numbers.public_numbers.n).decode(), - "e": to_base64url_uint(numbers.public_numbers.e).decode(), - "d": to_base64url_uint(numbers.d).decode(), - "p": to_base64url_uint(numbers.p).decode(), - "q": to_base64url_uint(numbers.q).decode(), - "dp": to_base64url_uint(numbers.dmp1).decode(), - "dq": to_base64url_uint(numbers.dmq1).decode(), - "qi": to_base64url_uint(numbers.iqmp).decode(), - } - - elif hasattr(key_obj, "verify"): - # Public key - numbers = key_obj.public_numbers() - - obj = { - "kty": "RSA", - "key_ops": ["verify"], - "n": to_base64url_uint(numbers.n).decode(), - "e": to_base64url_uint(numbers.e).decode(), - } - else: - raise InvalidKeyError("Not a public or private key") - - if as_dict: - return obj - else: - return json.dumps(obj) - - @staticmethod - def from_jwk(jwk: str | JWKDict) -> AllowedRSAKeys: - try: - if isinstance(jwk, str): - obj = json.loads(jwk) - elif isinstance(jwk, dict): - obj = jwk - else: - raise ValueError - except ValueError: - raise InvalidKeyError("Key is not valid JSON") - - if obj.get("kty") != "RSA": - raise InvalidKeyError("Not an RSA key") - - if "d" in obj and "e" in obj and "n" in obj: - # Private key - if "oth" in obj: - raise InvalidKeyError( - "Unsupported RSA private key: > 2 primes not supported" - ) - - other_props = ["p", "q", "dp", "dq", "qi"] - props_found = [prop in obj for prop in other_props] - any_props_found = any(props_found) - - if any_props_found and not all(props_found): - raise InvalidKeyError( - "RSA key must include all parameters if any are present besides d" - ) - - public_numbers = RSAPublicNumbers( - from_base64url_uint(obj["e"]), - from_base64url_uint(obj["n"]), - ) - - if any_props_found: - numbers = RSAPrivateNumbers( - d=from_base64url_uint(obj["d"]), - p=from_base64url_uint(obj["p"]), - q=from_base64url_uint(obj["q"]), - dmp1=from_base64url_uint(obj["dp"]), - dmq1=from_base64url_uint(obj["dq"]), - iqmp=from_base64url_uint(obj["qi"]), - public_numbers=public_numbers, - ) - else: - d = from_base64url_uint(obj["d"]) - p, q = rsa_recover_prime_factors( - public_numbers.n, d, public_numbers.e - ) - - numbers = RSAPrivateNumbers( - d=d, - p=p, - q=q, - dmp1=rsa_crt_dmp1(d, p), - dmq1=rsa_crt_dmq1(d, q), - iqmp=rsa_crt_iqmp(p, q), - public_numbers=public_numbers, - ) - - return numbers.private_key() - elif "n" in obj and "e" in obj: - # Public key - return RSAPublicNumbers( - from_base64url_uint(obj["e"]), - from_base64url_uint(obj["n"]), - ).public_key() - else: - raise InvalidKeyError("Not a public or private key") - - def sign(self, msg: bytes, key: RSAPrivateKey) -> bytes: - return key.sign(msg, padding.PKCS1v15(), self.hash_alg()) - - def verify(self, msg: bytes, key: RSAPublicKey, sig: bytes) -> bool: - try: - key.verify(sig, msg, padding.PKCS1v15(), self.hash_alg()) - return True - except InvalidSignature: - return False - - class ECAlgorithm(Algorithm): - """ - Performs signing and verification operations using - ECDSA and the specified hash function - """ - - SHA256: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA256 - SHA384: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA384 - SHA512: ClassVar[type[hashes.HashAlgorithm]] = hashes.SHA512 - - def __init__(self, hash_alg: type[hashes.HashAlgorithm]) -> None: - self.hash_alg = hash_alg - - def prepare_key(self, key: AllowedECKeys | str | bytes) -> AllowedECKeys: - if isinstance(key, (EllipticCurvePrivateKey, EllipticCurvePublicKey)): - return key - - if not isinstance(key, (bytes, str)): - raise TypeError("Expecting a PEM-formatted key.") - - key_bytes = force_bytes(key) - - # Attempt to load key. We don't know if it's - # a Signing Key or a Verifying Key, so we try - # the Verifying Key first. - try: - if key_bytes.startswith(b"ecdsa-sha2-"): - crypto_key = load_ssh_public_key(key_bytes) - else: - crypto_key = load_pem_public_key(key_bytes) # type: ignore[assignment] - except ValueError: - crypto_key = load_pem_private_key(key_bytes, password=None) # type: ignore[assignment] - - # Explicit check the key to prevent confusing errors from cryptography - if not isinstance( - crypto_key, (EllipticCurvePrivateKey, EllipticCurvePublicKey) - ): - raise InvalidKeyError( - "Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for ECDSA algorithms" - ) - - return crypto_key - - def sign(self, msg: bytes, key: EllipticCurvePrivateKey) -> bytes: - der_sig = key.sign(msg, ECDSA(self.hash_alg())) - - return der_to_raw_signature(der_sig, key.curve) - - def verify(self, msg: bytes, key: "AllowedECKeys", sig: bytes) -> bool: - try: - der_sig = raw_to_der_signature(sig, key.curve) - except ValueError: - return False - - try: - public_key = ( - key.public_key() - if isinstance(key, EllipticCurvePrivateKey) - else key - ) - public_key.verify(der_sig, msg, ECDSA(self.hash_alg())) - return True - except InvalidSignature: - return False - - @overload - @staticmethod - def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[True]) -> JWKDict: - ... # pragma: no cover - - @overload - @staticmethod - def to_jwk(key_obj: AllowedECKeys, as_dict: Literal[False] = False) -> str: - ... # pragma: no cover - - @staticmethod - def to_jwk( - key_obj: AllowedECKeys, as_dict: bool = False - ) -> Union[JWKDict, str]: - if isinstance(key_obj, EllipticCurvePrivateKey): - public_numbers = key_obj.public_key().public_numbers() - elif isinstance(key_obj, EllipticCurvePublicKey): - public_numbers = key_obj.public_numbers() - else: - raise InvalidKeyError("Not a public or private key") - - if isinstance(key_obj.curve, SECP256R1): - crv = "P-256" - elif isinstance(key_obj.curve, SECP384R1): - crv = "P-384" - elif isinstance(key_obj.curve, SECP521R1): - crv = "P-521" - elif isinstance(key_obj.curve, SECP256K1): - crv = "secp256k1" - else: - raise InvalidKeyError(f"Invalid curve: {key_obj.curve}") - - obj: dict[str, Any] = { - "kty": "EC", - "crv": crv, - "x": to_base64url_uint(public_numbers.x).decode(), - "y": to_base64url_uint(public_numbers.y).decode(), - } - - if isinstance(key_obj, EllipticCurvePrivateKey): - obj["d"] = to_base64url_uint( - key_obj.private_numbers().private_value - ).decode() - - if as_dict: - return obj - else: - return json.dumps(obj) - - @staticmethod - def from_jwk(jwk: str | JWKDict) -> AllowedECKeys: - try: - if isinstance(jwk, str): - obj = json.loads(jwk) - elif isinstance(jwk, dict): - obj = jwk - else: - raise ValueError - except ValueError: - raise InvalidKeyError("Key is not valid JSON") - - if obj.get("kty") != "EC": - raise InvalidKeyError("Not an Elliptic curve key") - - if "x" not in obj or "y" not in obj: - raise InvalidKeyError("Not an Elliptic curve key") - - x = base64url_decode(obj.get("x")) - y = base64url_decode(obj.get("y")) - - curve = obj.get("crv") - curve_obj: EllipticCurve - - if curve == "P-256": - if len(x) == len(y) == 32: - curve_obj = SECP256R1() - else: - raise InvalidKeyError("Coords should be 32 bytes for curve P-256") - elif curve == "P-384": - if len(x) == len(y) == 48: - curve_obj = SECP384R1() - else: - raise InvalidKeyError("Coords should be 48 bytes for curve P-384") - elif curve == "P-521": - if len(x) == len(y) == 66: - curve_obj = SECP521R1() - else: - raise InvalidKeyError("Coords should be 66 bytes for curve P-521") - elif curve == "secp256k1": - if len(x) == len(y) == 32: - curve_obj = SECP256K1() - else: - raise InvalidKeyError( - "Coords should be 32 bytes for curve secp256k1" - ) - else: - raise InvalidKeyError(f"Invalid curve: {curve}") - - public_numbers = EllipticCurvePublicNumbers( - x=int.from_bytes(x, byteorder="big"), - y=int.from_bytes(y, byteorder="big"), - curve=curve_obj, - ) - - if "d" not in obj: - return public_numbers.public_key() - - d = base64url_decode(obj.get("d")) - if len(d) != len(x): - raise InvalidKeyError( - "D should be {} bytes for curve {}", len(x), curve - ) - - return EllipticCurvePrivateNumbers( - int.from_bytes(d, byteorder="big"), public_numbers - ).private_key() - - class RSAPSSAlgorithm(RSAAlgorithm): - """ - Performs a signature using RSASSA-PSS with MGF1 - """ - - def sign(self, msg: bytes, key: RSAPrivateKey) -> bytes: - return key.sign( - msg, - padding.PSS( - mgf=padding.MGF1(self.hash_alg()), - salt_length=self.hash_alg().digest_size, - ), - self.hash_alg(), - ) - - def verify(self, msg: bytes, key: RSAPublicKey, sig: bytes) -> bool: - try: - key.verify( - sig, - msg, - padding.PSS( - mgf=padding.MGF1(self.hash_alg()), - salt_length=self.hash_alg().digest_size, - ), - self.hash_alg(), - ) - return True - except InvalidSignature: - return False - - class OKPAlgorithm(Algorithm): - """ - Performs signing and verification operations using EdDSA - - This class requires ``cryptography>=2.6`` to be installed. - """ - - def __init__(self, **kwargs: Any) -> None: - pass - - def prepare_key(self, key: AllowedOKPKeys | str | bytes) -> AllowedOKPKeys: - if isinstance(key, (bytes, str)): - key_str = key.decode("utf-8") if isinstance(key, bytes) else key - key_bytes = key.encode("utf-8") if isinstance(key, str) else key - - if "-----BEGIN PUBLIC" in key_str: - key = load_pem_public_key(key_bytes) # type: ignore[assignment] - elif "-----BEGIN PRIVATE" in key_str: - key = load_pem_private_key(key_bytes, password=None) # type: ignore[assignment] - elif key_str[0:4] == "ssh-": - key = load_ssh_public_key(key_bytes) # type: ignore[assignment] - - # Explicit check the key to prevent confusing errors from cryptography - if not isinstance( - key, - (Ed25519PrivateKey, Ed25519PublicKey, Ed448PrivateKey, Ed448PublicKey), - ): - raise InvalidKeyError( - "Expecting a EllipticCurvePrivateKey/EllipticCurvePublicKey. Wrong key provided for EdDSA algorithms" - ) - - return key - - def sign( - self, msg: str | bytes, key: Ed25519PrivateKey | Ed448PrivateKey - ) -> bytes: - """ - Sign a message ``msg`` using the EdDSA private key ``key`` - :param str|bytes msg: Message to sign - :param Ed25519PrivateKey}Ed448PrivateKey key: A :class:`.Ed25519PrivateKey` - or :class:`.Ed448PrivateKey` isinstance - :return bytes signature: The signature, as bytes - """ - msg_bytes = msg.encode("utf-8") if isinstance(msg, str) else msg - return key.sign(msg_bytes) - - def verify( - self, msg: str | bytes, key: AllowedOKPKeys, sig: str | bytes - ) -> bool: - """ - Verify a given ``msg`` against a signature ``sig`` using the EdDSA key ``key`` - - :param str|bytes sig: EdDSA signature to check ``msg`` against - :param str|bytes msg: Message to sign - :param Ed25519PrivateKey|Ed25519PublicKey|Ed448PrivateKey|Ed448PublicKey key: - A private or public EdDSA key instance - :return bool verified: True if signature is valid, False if not. - """ - try: - msg_bytes = msg.encode("utf-8") if isinstance(msg, str) else msg - sig_bytes = sig.encode("utf-8") if isinstance(sig, str) else sig - - public_key = ( - key.public_key() - if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)) - else key - ) - public_key.verify(sig_bytes, msg_bytes) - return True # If no exception was raised, the signature is valid. - except InvalidSignature: - return False - - @overload - @staticmethod - def to_jwk(key: AllowedOKPKeys, as_dict: Literal[True]) -> JWKDict: - ... # pragma: no cover - - @overload - @staticmethod - def to_jwk(key: AllowedOKPKeys, as_dict: Literal[False] = False) -> str: - ... # pragma: no cover - - @staticmethod - def to_jwk(key: AllowedOKPKeys, as_dict: bool = False) -> Union[JWKDict, str]: - if isinstance(key, (Ed25519PublicKey, Ed448PublicKey)): - x = key.public_bytes( - encoding=Encoding.Raw, - format=PublicFormat.Raw, - ) - crv = "Ed25519" if isinstance(key, Ed25519PublicKey) else "Ed448" - - obj = { - "x": base64url_encode(force_bytes(x)).decode(), - "kty": "OKP", - "crv": crv, - } - - if as_dict: - return obj - else: - return json.dumps(obj) - - if isinstance(key, (Ed25519PrivateKey, Ed448PrivateKey)): - d = key.private_bytes( - encoding=Encoding.Raw, - format=PrivateFormat.Raw, - encryption_algorithm=NoEncryption(), - ) - - x = key.public_key().public_bytes( - encoding=Encoding.Raw, - format=PublicFormat.Raw, - ) - - crv = "Ed25519" if isinstance(key, Ed25519PrivateKey) else "Ed448" - obj = { - "x": base64url_encode(force_bytes(x)).decode(), - "d": base64url_encode(force_bytes(d)).decode(), - "kty": "OKP", - "crv": crv, - } - - if as_dict: - return obj - else: - return json.dumps(obj) - - raise InvalidKeyError("Not a public or private key") - - @staticmethod - def from_jwk(jwk: str | JWKDict) -> AllowedOKPKeys: - try: - if isinstance(jwk, str): - obj = json.loads(jwk) - elif isinstance(jwk, dict): - obj = jwk - else: - raise ValueError - except ValueError: - raise InvalidKeyError("Key is not valid JSON") - - if obj.get("kty") != "OKP": - raise InvalidKeyError("Not an Octet Key Pair") - - curve = obj.get("crv") - if curve != "Ed25519" and curve != "Ed448": - raise InvalidKeyError(f"Invalid curve: {curve}") - - if "x" not in obj: - raise InvalidKeyError('OKP should have "x" parameter') - x = base64url_decode(obj.get("x")) - - try: - if "d" not in obj: - if curve == "Ed25519": - return Ed25519PublicKey.from_public_bytes(x) - return Ed448PublicKey.from_public_bytes(x) - d = base64url_decode(obj.get("d")) - if curve == "Ed25519": - return Ed25519PrivateKey.from_private_bytes(d) - return Ed448PrivateKey.from_private_bytes(d) - except ValueError as err: - raise InvalidKeyError("Invalid key parameter") from err diff --git a/apigw-dynamodb-python-cdk/src/jwt/api_jwk.py b/apigw-dynamodb-python-cdk/src/jwt/api_jwk.py deleted file mode 100644 index 456c7f4d8..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/api_jwk.py +++ /dev/null @@ -1,132 +0,0 @@ -from __future__ import annotations - -import json -import time -from typing import Any - -from .algorithms import get_default_algorithms, has_crypto, requires_cryptography -from .exceptions import InvalidKeyError, PyJWKError, PyJWKSetError, PyJWTError -from .types import JWKDict - - -class PyJWK: - def __init__(self, jwk_data: JWKDict, algorithm: str | None = None) -> None: - self._algorithms = get_default_algorithms() - self._jwk_data = jwk_data - - kty = self._jwk_data.get("kty", None) - if not kty: - raise InvalidKeyError(f"kty is not found: {self._jwk_data}") - - if not algorithm and isinstance(self._jwk_data, dict): - algorithm = self._jwk_data.get("alg", None) - - if not algorithm: - # Determine alg with kty (and crv). - crv = self._jwk_data.get("crv", None) - if kty == "EC": - if crv == "P-256" or not crv: - algorithm = "ES256" - elif crv == "P-384": - algorithm = "ES384" - elif crv == "P-521": - algorithm = "ES512" - elif crv == "secp256k1": - algorithm = "ES256K" - else: - raise InvalidKeyError(f"Unsupported crv: {crv}") - elif kty == "RSA": - algorithm = "RS256" - elif kty == "oct": - algorithm = "HS256" - elif kty == "OKP": - if not crv: - raise InvalidKeyError(f"crv is not found: {self._jwk_data}") - if crv == "Ed25519": - algorithm = "EdDSA" - else: - raise InvalidKeyError(f"Unsupported crv: {crv}") - else: - raise InvalidKeyError(f"Unsupported kty: {kty}") - - if not has_crypto and algorithm in requires_cryptography: - raise PyJWKError(f"{algorithm} requires 'cryptography' to be installed.") - - self.Algorithm = self._algorithms.get(algorithm) - - if not self.Algorithm: - raise PyJWKError(f"Unable to find an algorithm for key: {self._jwk_data}") - - self.key = self.Algorithm.from_jwk(self._jwk_data) - - @staticmethod - def from_dict(obj: JWKDict, algorithm: str | None = None) -> "PyJWK": - return PyJWK(obj, algorithm) - - @staticmethod - def from_json(data: str, algorithm: None = None) -> "PyJWK": - obj = json.loads(data) - return PyJWK.from_dict(obj, algorithm) - - @property - def key_type(self) -> str | None: - return self._jwk_data.get("kty", None) - - @property - def key_id(self) -> str | None: - return self._jwk_data.get("kid", None) - - @property - def public_key_use(self) -> str | None: - return self._jwk_data.get("use", None) - - -class PyJWKSet: - def __init__(self, keys: list[JWKDict]) -> None: - self.keys = [] - - if not keys: - raise PyJWKSetError("The JWK Set did not contain any keys") - - if not isinstance(keys, list): - raise PyJWKSetError("Invalid JWK Set value") - - for key in keys: - try: - self.keys.append(PyJWK(key)) - except PyJWTError: - # skip unusable keys - continue - - if len(self.keys) == 0: - raise PyJWKSetError( - "The JWK Set did not contain any usable keys. Perhaps 'cryptography' is not installed?" - ) - - @staticmethod - def from_dict(obj: dict[str, Any]) -> "PyJWKSet": - keys = obj.get("keys", []) - return PyJWKSet(keys) - - @staticmethod - def from_json(data: str) -> "PyJWKSet": - obj = json.loads(data) - return PyJWKSet.from_dict(obj) - - def __getitem__(self, kid: str) -> "PyJWK": - for key in self.keys: - if key.key_id == kid: - return key - raise KeyError(f"keyset has no key for kid: {kid}") - - -class PyJWTSetWithTimestamp: - def __init__(self, jwk_set: PyJWKSet): - self.jwk_set = jwk_set - self.timestamp = time.monotonic() - - def get_jwk_set(self) -> PyJWKSet: - return self.jwk_set - - def get_timestamp(self) -> float: - return self.timestamp diff --git a/apigw-dynamodb-python-cdk/src/jwt/api_jws.py b/apigw-dynamodb-python-cdk/src/jwt/api_jws.py deleted file mode 100644 index fa6708ccc..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/api_jws.py +++ /dev/null @@ -1,328 +0,0 @@ -from __future__ import annotations - -import binascii -import json -import warnings -from typing import TYPE_CHECKING, Any - -from .algorithms import ( - Algorithm, - get_default_algorithms, - has_crypto, - requires_cryptography, -) -from .exceptions import ( - DecodeError, - InvalidAlgorithmError, - InvalidSignatureError, - InvalidTokenError, -) -from .utils import base64url_decode, base64url_encode -from .warnings import RemovedInPyjwt3Warning - -if TYPE_CHECKING: - from .algorithms import AllowedPrivateKeys, AllowedPublicKeys - - -class PyJWS: - header_typ = "JWT" - - def __init__( - self, - algorithms: list[str] | None = None, - options: dict[str, Any] | None = None, - ) -> None: - self._algorithms = get_default_algorithms() - self._valid_algs = ( - set(algorithms) if algorithms is not None else set(self._algorithms) - ) - - # Remove algorithms that aren't on the whitelist - for key in list(self._algorithms.keys()): - if key not in self._valid_algs: - del self._algorithms[key] - - if options is None: - options = {} - self.options = {**self._get_default_options(), **options} - - @staticmethod - def _get_default_options() -> dict[str, bool]: - return {"verify_signature": True} - - def register_algorithm(self, alg_id: str, alg_obj: Algorithm) -> None: - """ - Registers a new Algorithm for use when creating and verifying tokens. - """ - if alg_id in self._algorithms: - raise ValueError("Algorithm already has a handler.") - - if not isinstance(alg_obj, Algorithm): - raise TypeError("Object is not of type `Algorithm`") - - self._algorithms[alg_id] = alg_obj - self._valid_algs.add(alg_id) - - def unregister_algorithm(self, alg_id: str) -> None: - """ - Unregisters an Algorithm for use when creating and verifying tokens - Throws KeyError if algorithm is not registered. - """ - if alg_id not in self._algorithms: - raise KeyError( - "The specified algorithm could not be removed" - " because it is not registered." - ) - - del self._algorithms[alg_id] - self._valid_algs.remove(alg_id) - - def get_algorithms(self) -> list[str]: - """ - Returns a list of supported values for the 'alg' parameter. - """ - return list(self._valid_algs) - - def get_algorithm_by_name(self, alg_name: str) -> Algorithm: - """ - For a given string name, return the matching Algorithm object. - - Example usage: - - >>> jws_obj.get_algorithm_by_name("RS256") - """ - try: - return self._algorithms[alg_name] - except KeyError as e: - if not has_crypto and alg_name in requires_cryptography: - raise NotImplementedError( - f"Algorithm '{alg_name}' could not be found. Do you have cryptography installed?" - ) from e - raise NotImplementedError("Algorithm not supported") from e - - def encode( - self, - payload: bytes, - key: AllowedPrivateKeys | str | bytes, - algorithm: str | None = "HS256", - headers: dict[str, Any] | None = None, - json_encoder: type[json.JSONEncoder] | None = None, - is_payload_detached: bool = False, - sort_headers: bool = True, - ) -> str: - segments = [] - - # declare a new var to narrow the type for type checkers - algorithm_: str = algorithm if algorithm is not None else "none" - - # Prefer headers values if present to function parameters. - if headers: - headers_alg = headers.get("alg") - if headers_alg: - algorithm_ = headers["alg"] - - headers_b64 = headers.get("b64") - if headers_b64 is False: - is_payload_detached = True - - # Header - header: dict[str, Any] = {"typ": self.header_typ, "alg": algorithm_} - - if headers: - self._validate_headers(headers) - header.update(headers) - - if not header["typ"]: - del header["typ"] - - if is_payload_detached: - header["b64"] = False - elif "b64" in header: - # True is the standard value for b64, so no need for it - del header["b64"] - - json_header = json.dumps( - header, separators=(",", ":"), cls=json_encoder, sort_keys=sort_headers - ).encode() - - segments.append(base64url_encode(json_header)) - - if is_payload_detached: - msg_payload = payload - else: - msg_payload = base64url_encode(payload) - segments.append(msg_payload) - - # Segments - signing_input = b".".join(segments) - - alg_obj = self.get_algorithm_by_name(algorithm_) - key = alg_obj.prepare_key(key) - signature = alg_obj.sign(signing_input, key) - - segments.append(base64url_encode(signature)) - - # Don't put the payload content inside the encoded token when detached - if is_payload_detached: - segments[1] = b"" - encoded_string = b".".join(segments) - - return encoded_string.decode("utf-8") - - def decode_complete( - self, - jwt: str | bytes, - key: AllowedPublicKeys | str | bytes = "", - algorithms: list[str] | None = None, - options: dict[str, Any] | None = None, - detached_payload: bytes | None = None, - **kwargs, - ) -> dict[str, Any]: - if kwargs: - warnings.warn( - "passing additional kwargs to decode_complete() is deprecated " - "and will be removed in pyjwt version 3. " - f"Unsupported kwargs: {tuple(kwargs.keys())}", - RemovedInPyjwt3Warning, - ) - if options is None: - options = {} - merged_options = {**self.options, **options} - verify_signature = merged_options["verify_signature"] - - if verify_signature and not algorithms: - raise DecodeError( - 'It is required that you pass in a value for the "algorithms" argument when calling decode().' - ) - - payload, signing_input, header, signature = self._load(jwt) - - if header.get("b64", True) is False: - if detached_payload is None: - raise DecodeError( - 'It is required that you pass in a value for the "detached_payload" argument to decode a message having the b64 header set to false.' - ) - payload = detached_payload - signing_input = b".".join([signing_input.rsplit(b".", 1)[0], payload]) - - if verify_signature: - self._verify_signature(signing_input, header, signature, key, algorithms) - - return { - "payload": payload, - "header": header, - "signature": signature, - } - - def decode( - self, - jwt: str | bytes, - key: AllowedPublicKeys | str | bytes = "", - algorithms: list[str] | None = None, - options: dict[str, Any] | None = None, - detached_payload: bytes | None = None, - **kwargs, - ) -> Any: - if kwargs: - warnings.warn( - "passing additional kwargs to decode() is deprecated " - "and will be removed in pyjwt version 3. " - f"Unsupported kwargs: {tuple(kwargs.keys())}", - RemovedInPyjwt3Warning, - ) - decoded = self.decode_complete( - jwt, key, algorithms, options, detached_payload=detached_payload - ) - return decoded["payload"] - - def get_unverified_header(self, jwt: str | bytes) -> dict[str, Any]: - """Returns back the JWT header parameters as a dict() - - Note: The signature is not verified so the header parameters - should not be fully trusted until signature verification is complete - """ - headers = self._load(jwt)[2] - self._validate_headers(headers) - - return headers - - def _load(self, jwt: str | bytes) -> tuple[bytes, bytes, dict[str, Any], bytes]: - if isinstance(jwt, str): - jwt = jwt.encode("utf-8") - - if not isinstance(jwt, bytes): - raise DecodeError(f"Invalid token type. Token must be a {bytes}") - - try: - signing_input, crypto_segment = jwt.rsplit(b".", 1) - header_segment, payload_segment = signing_input.split(b".", 1) - except ValueError as err: - raise DecodeError("Not enough segments") from err - - try: - header_data = base64url_decode(header_segment) - except (TypeError, binascii.Error) as err: - raise DecodeError("Invalid header padding") from err - - try: - header = json.loads(header_data) - except ValueError as e: - raise DecodeError(f"Invalid header string: {e}") from e - - if not isinstance(header, dict): - raise DecodeError("Invalid header string: must be a json object") - - try: - payload = base64url_decode(payload_segment) - except (TypeError, binascii.Error) as err: - raise DecodeError("Invalid payload padding") from err - - try: - signature = base64url_decode(crypto_segment) - except (TypeError, binascii.Error) as err: - raise DecodeError("Invalid crypto padding") from err - - return (payload, signing_input, header, signature) - - def _verify_signature( - self, - signing_input: bytes, - header: dict[str, Any], - signature: bytes, - key: AllowedPublicKeys | str | bytes = "", - algorithms: list[str] | None = None, - ) -> None: - try: - alg = header["alg"] - except KeyError: - raise InvalidAlgorithmError("Algorithm not specified") - - if not alg or (algorithms is not None and alg not in algorithms): - raise InvalidAlgorithmError("The specified alg value is not allowed") - - try: - alg_obj = self.get_algorithm_by_name(alg) - except NotImplementedError as e: - raise InvalidAlgorithmError("Algorithm not supported") from e - prepared_key = alg_obj.prepare_key(key) - - if not alg_obj.verify(signing_input, prepared_key, signature): - raise InvalidSignatureError("Signature verification failed") - - def _validate_headers(self, headers: dict[str, Any]) -> None: - if "kid" in headers: - self._validate_kid(headers["kid"]) - - def _validate_kid(self, kid: Any) -> None: - if not isinstance(kid, str): - raise InvalidTokenError("Key ID header parameter must be a string") - - -_jws_global_obj = PyJWS() -encode = _jws_global_obj.encode -decode_complete = _jws_global_obj.decode_complete -decode = _jws_global_obj.decode -register_algorithm = _jws_global_obj.register_algorithm -unregister_algorithm = _jws_global_obj.unregister_algorithm -get_algorithm_by_name = _jws_global_obj.get_algorithm_by_name -get_unverified_header = _jws_global_obj.get_unverified_header diff --git a/apigw-dynamodb-python-cdk/src/jwt/api_jwt.py b/apigw-dynamodb-python-cdk/src/jwt/api_jwt.py deleted file mode 100644 index 48d739ad6..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/api_jwt.py +++ /dev/null @@ -1,372 +0,0 @@ -from __future__ import annotations - -import json -import warnings -from calendar import timegm -from collections.abc import Iterable -from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, Any - -from . import api_jws -from .exceptions import ( - DecodeError, - ExpiredSignatureError, - ImmatureSignatureError, - InvalidAudienceError, - InvalidIssuedAtError, - InvalidIssuerError, - MissingRequiredClaimError, -) -from .warnings import RemovedInPyjwt3Warning - -if TYPE_CHECKING: - from .algorithms import AllowedPrivateKeys, AllowedPublicKeys - - -class PyJWT: - def __init__(self, options: dict[str, Any] | None = None) -> None: - if options is None: - options = {} - self.options: dict[str, Any] = {**self._get_default_options(), **options} - - @staticmethod - def _get_default_options() -> dict[str, bool | list[str]]: - return { - "verify_signature": True, - "verify_exp": True, - "verify_nbf": True, - "verify_iat": True, - "verify_aud": True, - "verify_iss": True, - "require": [], - } - - def encode( - self, - payload: dict[str, Any], - key: AllowedPrivateKeys | str | bytes, - algorithm: str | None = "HS256", - headers: dict[str, Any] | None = None, - json_encoder: type[json.JSONEncoder] | None = None, - sort_headers: bool = True, - ) -> str: - # Check that we get a dict - if not isinstance(payload, dict): - raise TypeError( - "Expecting a dict object, as JWT only supports " - "JSON objects as payloads." - ) - - # Payload - payload = payload.copy() - for time_claim in ["exp", "iat", "nbf"]: - # Convert datetime to a intDate value in known time-format claims - if isinstance(payload.get(time_claim), datetime): - payload[time_claim] = timegm(payload[time_claim].utctimetuple()) - - json_payload = self._encode_payload( - payload, - headers=headers, - json_encoder=json_encoder, - ) - - return api_jws.encode( - json_payload, - key, - algorithm, - headers, - json_encoder, - sort_headers=sort_headers, - ) - - def _encode_payload( - self, - payload: dict[str, Any], - headers: dict[str, Any] | None = None, - json_encoder: type[json.JSONEncoder] | None = None, - ) -> bytes: - """ - Encode a given payload to the bytes to be signed. - - This method is intended to be overridden by subclasses that need to - encode the payload in a different way, e.g. compress the payload. - """ - return json.dumps( - payload, - separators=(",", ":"), - cls=json_encoder, - ).encode("utf-8") - - def decode_complete( - self, - jwt: str | bytes, - key: AllowedPublicKeys | str | bytes = "", - algorithms: list[str] | None = None, - options: dict[str, Any] | None = None, - # deprecated arg, remove in pyjwt3 - verify: bool | None = None, - # could be used as passthrough to api_jws, consider removal in pyjwt3 - detached_payload: bytes | None = None, - # passthrough arguments to _validate_claims - # consider putting in options - audience: str | Iterable[str] | None = None, - issuer: str | None = None, - leeway: float | timedelta = 0, - # kwargs - **kwargs: Any, - ) -> dict[str, Any]: - if kwargs: - warnings.warn( - "passing additional kwargs to decode_complete() is deprecated " - "and will be removed in pyjwt version 3. " - f"Unsupported kwargs: {tuple(kwargs.keys())}", - RemovedInPyjwt3Warning, - ) - options = dict(options or {}) # shallow-copy or initialize an empty dict - options.setdefault("verify_signature", True) - - # If the user has set the legacy `verify` argument, and it doesn't match - # what the relevant `options` entry for the argument is, inform the user - # that they're likely making a mistake. - if verify is not None and verify != options["verify_signature"]: - warnings.warn( - "The `verify` argument to `decode` does nothing in PyJWT 2.0 and newer. " - "The equivalent is setting `verify_signature` to False in the `options` dictionary. " - "This invocation has a mismatch between the kwarg and the option entry.", - category=DeprecationWarning, - ) - - if not options["verify_signature"]: - options.setdefault("verify_exp", False) - options.setdefault("verify_nbf", False) - options.setdefault("verify_iat", False) - options.setdefault("verify_aud", False) - options.setdefault("verify_iss", False) - - if options["verify_signature"] and not algorithms: - raise DecodeError( - 'It is required that you pass in a value for the "algorithms" argument when calling decode().' - ) - - decoded = api_jws.decode_complete( - jwt, - key=key, - algorithms=algorithms, - options=options, - detached_payload=detached_payload, - ) - - payload = self._decode_payload(decoded) - - merged_options = {**self.options, **options} - self._validate_claims( - payload, merged_options, audience=audience, issuer=issuer, leeway=leeway - ) - - decoded["payload"] = payload - return decoded - - def _decode_payload(self, decoded: dict[str, Any]) -> Any: - """ - Decode the payload from a JWS dictionary (payload, signature, header). - - This method is intended to be overridden by subclasses that need to - decode the payload in a different way, e.g. decompress compressed - payloads. - """ - try: - payload = json.loads(decoded["payload"]) - except ValueError as e: - raise DecodeError(f"Invalid payload string: {e}") - if not isinstance(payload, dict): - raise DecodeError("Invalid payload string: must be a json object") - return payload - - def decode( - self, - jwt: str | bytes, - key: AllowedPublicKeys | str | bytes = "", - algorithms: list[str] | None = None, - options: dict[str, Any] | None = None, - # deprecated arg, remove in pyjwt3 - verify: bool | None = None, - # could be used as passthrough to api_jws, consider removal in pyjwt3 - detached_payload: bytes | None = None, - # passthrough arguments to _validate_claims - # consider putting in options - audience: str | Iterable[str] | None = None, - issuer: str | None = None, - leeway: float | timedelta = 0, - # kwargs - **kwargs: Any, - ) -> Any: - if kwargs: - warnings.warn( - "passing additional kwargs to decode() is deprecated " - "and will be removed in pyjwt version 3. " - f"Unsupported kwargs: {tuple(kwargs.keys())}", - RemovedInPyjwt3Warning, - ) - decoded = self.decode_complete( - jwt, - key, - algorithms, - options, - verify=verify, - detached_payload=detached_payload, - audience=audience, - issuer=issuer, - leeway=leeway, - ) - return decoded["payload"] - - def _validate_claims( - self, - payload: dict[str, Any], - options: dict[str, Any], - audience=None, - issuer=None, - leeway: float | timedelta = 0, - ) -> None: - if isinstance(leeway, timedelta): - leeway = leeway.total_seconds() - - if audience is not None and not isinstance(audience, (str, Iterable)): - raise TypeError("audience must be a string, iterable or None") - - self._validate_required_claims(payload, options) - - now = datetime.now(tz=timezone.utc).timestamp() - - if "iat" in payload and options["verify_iat"]: - self._validate_iat(payload, now, leeway) - - if "nbf" in payload and options["verify_nbf"]: - self._validate_nbf(payload, now, leeway) - - if "exp" in payload and options["verify_exp"]: - self._validate_exp(payload, now, leeway) - - if options["verify_iss"]: - self._validate_iss(payload, issuer) - - if options["verify_aud"]: - self._validate_aud( - payload, audience, strict=options.get("strict_aud", False) - ) - - def _validate_required_claims( - self, - payload: dict[str, Any], - options: dict[str, Any], - ) -> None: - for claim in options["require"]: - if payload.get(claim) is None: - raise MissingRequiredClaimError(claim) - - def _validate_iat( - self, - payload: dict[str, Any], - now: float, - leeway: float, - ) -> None: - try: - iat = int(payload["iat"]) - except ValueError: - raise InvalidIssuedAtError("Issued At claim (iat) must be an integer.") - if iat > (now + leeway): - raise ImmatureSignatureError("The token is not yet valid (iat)") - - def _validate_nbf( - self, - payload: dict[str, Any], - now: float, - leeway: float, - ) -> None: - try: - nbf = int(payload["nbf"]) - except ValueError: - raise DecodeError("Not Before claim (nbf) must be an integer.") - - if nbf > (now + leeway): - raise ImmatureSignatureError("The token is not yet valid (nbf)") - - def _validate_exp( - self, - payload: dict[str, Any], - now: float, - leeway: float, - ) -> None: - try: - exp = int(payload["exp"]) - except ValueError: - raise DecodeError("Expiration Time claim (exp) must be an" " integer.") - - if exp <= (now - leeway): - raise ExpiredSignatureError("Signature has expired") - - def _validate_aud( - self, - payload: dict[str, Any], - audience: str | Iterable[str] | None, - *, - strict: bool = False, - ) -> None: - if audience is None: - if "aud" not in payload or not payload["aud"]: - return - # Application did not specify an audience, but - # the token has the 'aud' claim - raise InvalidAudienceError("Invalid audience") - - if "aud" not in payload or not payload["aud"]: - # Application specified an audience, but it could not be - # verified since the token does not contain a claim. - raise MissingRequiredClaimError("aud") - - audience_claims = payload["aud"] - - # In strict mode, we forbid list matching: the supplied audience - # must be a string, and it must exactly match the audience claim. - if strict: - # Only a single audience is allowed in strict mode. - if not isinstance(audience, str): - raise InvalidAudienceError("Invalid audience (strict)") - - # Only a single audience claim is allowed in strict mode. - if not isinstance(audience_claims, str): - raise InvalidAudienceError("Invalid claim format in token (strict)") - - if audience != audience_claims: - raise InvalidAudienceError("Audience doesn't match (strict)") - - return - - if isinstance(audience_claims, str): - audience_claims = [audience_claims] - if not isinstance(audience_claims, list): - raise InvalidAudienceError("Invalid claim format in token") - if any(not isinstance(c, str) for c in audience_claims): - raise InvalidAudienceError("Invalid claim format in token") - - if isinstance(audience, str): - audience = [audience] - - if all(aud not in audience_claims for aud in audience): - raise InvalidAudienceError("Audience doesn't match") - - def _validate_iss(self, payload: dict[str, Any], issuer: Any) -> None: - if issuer is None: - return - - if "iss" not in payload: - raise MissingRequiredClaimError("iss") - - if payload["iss"] != issuer: - raise InvalidIssuerError("Invalid issuer") - - -_jwt_global_obj = PyJWT() -encode = _jwt_global_obj.encode -decode_complete = _jwt_global_obj.decode_complete -decode = _jwt_global_obj.decode diff --git a/apigw-dynamodb-python-cdk/src/jwt/exceptions.py b/apigw-dynamodb-python-cdk/src/jwt/exceptions.py deleted file mode 100644 index 8ac6ecf74..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/exceptions.py +++ /dev/null @@ -1,70 +0,0 @@ -class PyJWTError(Exception): - """ - Base class for all exceptions - """ - - pass - - -class InvalidTokenError(PyJWTError): - pass - - -class DecodeError(InvalidTokenError): - pass - - -class InvalidSignatureError(DecodeError): - pass - - -class ExpiredSignatureError(InvalidTokenError): - pass - - -class InvalidAudienceError(InvalidTokenError): - pass - - -class InvalidIssuerError(InvalidTokenError): - pass - - -class InvalidIssuedAtError(InvalidTokenError): - pass - - -class ImmatureSignatureError(InvalidTokenError): - pass - - -class InvalidKeyError(PyJWTError): - pass - - -class InvalidAlgorithmError(InvalidTokenError): - pass - - -class MissingRequiredClaimError(InvalidTokenError): - def __init__(self, claim: str) -> None: - self.claim = claim - - def __str__(self) -> str: - return f'Token is missing the "{self.claim}" claim' - - -class PyJWKError(PyJWTError): - pass - - -class PyJWKSetError(PyJWTError): - pass - - -class PyJWKClientError(PyJWTError): - pass - - -class PyJWKClientConnectionError(PyJWKClientError): - pass diff --git a/apigw-dynamodb-python-cdk/src/jwt/help.py b/apigw-dynamodb-python-cdk/src/jwt/help.py deleted file mode 100644 index 80b0ca56e..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/help.py +++ /dev/null @@ -1,64 +0,0 @@ -import json -import platform -import sys -from typing import Dict - -from . import __version__ as pyjwt_version - -try: - import cryptography - - cryptography_version = cryptography.__version__ -except ModuleNotFoundError: - cryptography_version = "" - - -def info() -> Dict[str, Dict[str, str]]: - """ - Generate information for a bug report. - Based on the requests package help utility module. - """ - try: - platform_info = { - "system": platform.system(), - "release": platform.release(), - } - except OSError: - platform_info = {"system": "Unknown", "release": "Unknown"} - - implementation = platform.python_implementation() - - if implementation == "CPython": - implementation_version = platform.python_version() - elif implementation == "PyPy": - pypy_version_info = sys.pypy_version_info # type: ignore[attr-defined] - implementation_version = ( - f"{pypy_version_info.major}." - f"{pypy_version_info.minor}." - f"{pypy_version_info.micro}" - ) - if pypy_version_info.releaselevel != "final": - implementation_version = "".join( - [implementation_version, pypy_version_info.releaselevel] - ) - else: - implementation_version = "Unknown" - - return { - "platform": platform_info, - "implementation": { - "name": implementation, - "version": implementation_version, - }, - "cryptography": {"version": cryptography_version}, - "pyjwt": {"version": pyjwt_version}, - } - - -def main() -> None: - """Pretty-print the bug information as JSON.""" - print(json.dumps(info(), sort_keys=True, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/apigw-dynamodb-python-cdk/src/jwt/jwk_set_cache.py b/apigw-dynamodb-python-cdk/src/jwt/jwk_set_cache.py deleted file mode 100644 index 243256305..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/jwk_set_cache.py +++ /dev/null @@ -1,31 +0,0 @@ -import time -from typing import Optional - -from .api_jwk import PyJWKSet, PyJWTSetWithTimestamp - - -class JWKSetCache: - def __init__(self, lifespan: int) -> None: - self.jwk_set_with_timestamp: Optional[PyJWTSetWithTimestamp] = None - self.lifespan = lifespan - - def put(self, jwk_set: PyJWKSet) -> None: - if jwk_set is not None: - self.jwk_set_with_timestamp = PyJWTSetWithTimestamp(jwk_set) - else: - # clear cache - self.jwk_set_with_timestamp = None - - def get(self) -> Optional[PyJWKSet]: - if self.jwk_set_with_timestamp is None or self.is_expired(): - return None - - return self.jwk_set_with_timestamp.get_jwk_set() - - def is_expired(self) -> bool: - return ( - self.jwk_set_with_timestamp is not None - and self.lifespan > -1 - and time.monotonic() - > self.jwk_set_with_timestamp.get_timestamp() + self.lifespan - ) diff --git a/apigw-dynamodb-python-cdk/src/jwt/jwks_client.py b/apigw-dynamodb-python-cdk/src/jwt/jwks_client.py deleted file mode 100644 index f19b10acb..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/jwks_client.py +++ /dev/null @@ -1,124 +0,0 @@ -import json -import urllib.request -from functools import lru_cache -from ssl import SSLContext -from typing import Any, Dict, List, Optional -from urllib.error import URLError - -from .api_jwk import PyJWK, PyJWKSet -from .api_jwt import decode_complete as decode_token -from .exceptions import PyJWKClientConnectionError, PyJWKClientError -from .jwk_set_cache import JWKSetCache - - -class PyJWKClient: - def __init__( - self, - uri: str, - cache_keys: bool = False, - max_cached_keys: int = 16, - cache_jwk_set: bool = True, - lifespan: int = 300, - headers: Optional[Dict[str, Any]] = None, - timeout: int = 30, - ssl_context: Optional[SSLContext] = None, - ): - if headers is None: - headers = {} - self.uri = uri - self.jwk_set_cache: Optional[JWKSetCache] = None - self.headers = headers - self.timeout = timeout - self.ssl_context = ssl_context - - if cache_jwk_set: - # Init jwt set cache with default or given lifespan. - # Default lifespan is 300 seconds (5 minutes). - if lifespan <= 0: - raise PyJWKClientError( - f'Lifespan must be greater than 0, the input is "{lifespan}"' - ) - self.jwk_set_cache = JWKSetCache(lifespan) - else: - self.jwk_set_cache = None - - if cache_keys: - # Cache signing keys - # Ignore mypy (https://github.com/python/mypy/issues/2427) - self.get_signing_key = lru_cache(maxsize=max_cached_keys)(self.get_signing_key) # type: ignore - - def fetch_data(self) -> Any: - jwk_set: Any = None - try: - r = urllib.request.Request(url=self.uri, headers=self.headers) - with urllib.request.urlopen( - r, timeout=self.timeout, context=self.ssl_context - ) as response: - jwk_set = json.load(response) - except (URLError, TimeoutError) as e: - raise PyJWKClientConnectionError( - f'Fail to fetch data from the url, err: "{e}"' - ) - else: - return jwk_set - finally: - if self.jwk_set_cache is not None: - self.jwk_set_cache.put(jwk_set) - - def get_jwk_set(self, refresh: bool = False) -> PyJWKSet: - data = None - if self.jwk_set_cache is not None and not refresh: - data = self.jwk_set_cache.get() - - if data is None: - data = self.fetch_data() - - if not isinstance(data, dict): - raise PyJWKClientError("The JWKS endpoint did not return a JSON object") - - return PyJWKSet.from_dict(data) - - def get_signing_keys(self, refresh: bool = False) -> List[PyJWK]: - jwk_set = self.get_jwk_set(refresh) - signing_keys = [ - jwk_set_key - for jwk_set_key in jwk_set.keys - if jwk_set_key.public_key_use in ["sig", None] and jwk_set_key.key_id - ] - - if not signing_keys: - raise PyJWKClientError("The JWKS endpoint did not contain any signing keys") - - return signing_keys - - def get_signing_key(self, kid: str) -> PyJWK: - signing_keys = self.get_signing_keys() - signing_key = self.match_kid(signing_keys, kid) - - if not signing_key: - # If no matching signing key from the jwk set, refresh the jwk set and try again. - signing_keys = self.get_signing_keys(refresh=True) - signing_key = self.match_kid(signing_keys, kid) - - if not signing_key: - raise PyJWKClientError( - f'Unable to find a signing key that matches: "{kid}"' - ) - - return signing_key - - def get_signing_key_from_jwt(self, token: str) -> PyJWK: - unverified = decode_token(token, options={"verify_signature": False}) - header = unverified["header"] - return self.get_signing_key(header.get("kid")) - - @staticmethod - def match_kid(signing_keys: List[PyJWK], kid: str) -> Optional[PyJWK]: - signing_key = None - - for key in signing_keys: - if key.key_id == kid: - signing_key = key - break - - return signing_key diff --git a/apigw-dynamodb-python-cdk/src/jwt/py.typed b/apigw-dynamodb-python-cdk/src/jwt/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/apigw-dynamodb-python-cdk/src/jwt/types.py b/apigw-dynamodb-python-cdk/src/jwt/types.py deleted file mode 100644 index 7d9935205..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/types.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Any, Callable, Dict - -JWKDict = Dict[str, Any] - -HashlibHash = Callable[..., Any] diff --git a/apigw-dynamodb-python-cdk/src/jwt/utils.py b/apigw-dynamodb-python-cdk/src/jwt/utils.py deleted file mode 100644 index 81c5ee41a..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/utils.py +++ /dev/null @@ -1,156 +0,0 @@ -import base64 -import binascii -import re -from typing import Union - -try: - from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve - from cryptography.hazmat.primitives.asymmetric.utils import ( - decode_dss_signature, - encode_dss_signature, - ) -except ModuleNotFoundError: - pass - - -def force_bytes(value: Union[bytes, str]) -> bytes: - if isinstance(value, str): - return value.encode("utf-8") - elif isinstance(value, bytes): - return value - else: - raise TypeError("Expected a string value") - - -def base64url_decode(input: Union[bytes, str]) -> bytes: - input_bytes = force_bytes(input) - - rem = len(input_bytes) % 4 - - if rem > 0: - input_bytes += b"=" * (4 - rem) - - return base64.urlsafe_b64decode(input_bytes) - - -def base64url_encode(input: bytes) -> bytes: - return base64.urlsafe_b64encode(input).replace(b"=", b"") - - -def to_base64url_uint(val: int) -> bytes: - if val < 0: - raise ValueError("Must be a positive integer") - - int_bytes = bytes_from_int(val) - - if len(int_bytes) == 0: - int_bytes = b"\x00" - - return base64url_encode(int_bytes) - - -def from_base64url_uint(val: Union[bytes, str]) -> int: - data = base64url_decode(force_bytes(val)) - return int.from_bytes(data, byteorder="big") - - -def number_to_bytes(num: int, num_bytes: int) -> bytes: - padded_hex = "%0*x" % (2 * num_bytes, num) - return binascii.a2b_hex(padded_hex.encode("ascii")) - - -def bytes_to_number(string: bytes) -> int: - return int(binascii.b2a_hex(string), 16) - - -def bytes_from_int(val: int) -> bytes: - remaining = val - byte_length = 0 - - while remaining != 0: - remaining >>= 8 - byte_length += 1 - - return val.to_bytes(byte_length, "big", signed=False) - - -def der_to_raw_signature(der_sig: bytes, curve: "EllipticCurve") -> bytes: - num_bits = curve.key_size - num_bytes = (num_bits + 7) // 8 - - r, s = decode_dss_signature(der_sig) - - return number_to_bytes(r, num_bytes) + number_to_bytes(s, num_bytes) - - -def raw_to_der_signature(raw_sig: bytes, curve: "EllipticCurve") -> bytes: - num_bits = curve.key_size - num_bytes = (num_bits + 7) // 8 - - if len(raw_sig) != 2 * num_bytes: - raise ValueError("Invalid signature") - - r = bytes_to_number(raw_sig[:num_bytes]) - s = bytes_to_number(raw_sig[num_bytes:]) - - return bytes(encode_dss_signature(r, s)) - - -# Based on https://github.com/hynek/pem/blob/7ad94db26b0bc21d10953f5dbad3acfdfacf57aa/src/pem/_core.py#L224-L252 -_PEMS = { - b"CERTIFICATE", - b"TRUSTED CERTIFICATE", - b"PRIVATE KEY", - b"PUBLIC KEY", - b"ENCRYPTED PRIVATE KEY", - b"OPENSSH PRIVATE KEY", - b"DSA PRIVATE KEY", - b"RSA PRIVATE KEY", - b"RSA PUBLIC KEY", - b"EC PRIVATE KEY", - b"DH PARAMETERS", - b"NEW CERTIFICATE REQUEST", - b"CERTIFICATE REQUEST", - b"SSH2 PUBLIC KEY", - b"SSH2 ENCRYPTED PRIVATE KEY", - b"X509 CRL", -} - -_PEM_RE = re.compile( - b"----[- ]BEGIN (" - + b"|".join(_PEMS) - + b""")[- ]----\r? -.+?\r? -----[- ]END \\1[- ]----\r?\n?""", - re.DOTALL, -) - - -def is_pem_format(key: bytes) -> bool: - return bool(_PEM_RE.search(key)) - - -# Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46 -_CERT_SUFFIX = b"-cert-v01@openssh.com" -_SSH_PUBKEY_RC = re.compile(rb"\A(\S+)[ \t]+(\S+)") -_SSH_KEY_FORMATS = [ - b"ssh-ed25519", - b"ssh-rsa", - b"ssh-dss", - b"ecdsa-sha2-nistp256", - b"ecdsa-sha2-nistp384", - b"ecdsa-sha2-nistp521", -] - - -def is_ssh_key(key: bytes) -> bool: - if any(string_value in key for string_value in _SSH_KEY_FORMATS): - return True - - ssh_pubkey_match = _SSH_PUBKEY_RC.match(key) - if ssh_pubkey_match: - key_type = ssh_pubkey_match.group(1) - if _CERT_SUFFIX == key_type[-len(_CERT_SUFFIX) :]: - return True - - return False diff --git a/apigw-dynamodb-python-cdk/src/jwt/warnings.py b/apigw-dynamodb-python-cdk/src/jwt/warnings.py deleted file mode 100644 index 8762a8cbb..000000000 --- a/apigw-dynamodb-python-cdk/src/jwt/warnings.py +++ /dev/null @@ -1,2 +0,0 @@ -class RemovedInPyjwt3Warning(DeprecationWarning): - pass diff --git a/apigw-dynamodb-python-cdk/src/lambda.zip b/apigw-dynamodb-python-cdk/src/lambda.zip deleted file mode 100644 index 57cc47d8dbf0fc9d20f7738a1d4b7a7d407bede7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70748 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