From 5069bf84c2705b916802049182cd08bcb521ed29 Mon Sep 17 00:00:00 2001 From: Noah Paige <69586985+noah-paige@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:52:08 -0500 Subject: [PATCH] Added Token Validations (#1682) ### Feature or Bugfix - Bugfix ### Detail - This PR does the following w.r.t Cognito IdP and Auth Flow - Changes Authorizer from built-in Cognito Authorizer to Custom Authorizer to validate token signature, issuer, and expiry time, etc. - Adds aditional step to execute GET API on Cognito's `/oauth/userInfo/` endpoint to ensure access Token validity Allows data.all API request to execute if the above criteria are met ### Relates - ### Security Please answer the questions below briefly where applicable, or write `N/A`. Based on [OWASP 10](https://owasp.org/Top10/en/). - Does this PR introduce or modify any input fields or queries - this includes fetching data from storage outside the application (e.g. a database, an S3 bucket)? - Is the input sanitized? - What precautions are you taking before deserializing the data you consume? - Is injection prevented by parametrizing queries? - Have you ensured no `eval` or similar functions are used? - Does this PR introduce any functionality or component that requires authorization? - How have you ensured it respects the existing AuthN/AuthZ mechanisms? - Are you logging failed auth attempts? - Are you using or adding any cryptographic features? - Do you use a standard proven implementations? - Are the used keys controlled by the customer? Where are they stored? - Are you introducing any new policies/roles/users? - Have you used the least-privilege principle? How? By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .checkov.baseline | 13 ++ .../dataall/base/utils/api_handler_utils.py | 8 +- .../custom_authorizer/auth_services.py | 2 +- .../custom_authorizer_lambda.py | 26 ++- .../custom_authorizer/jwt_services.py | 116 +++++----- .../custom_authorizer/requirements.txt | 5 +- deploy/requirements.txt | 3 +- deploy/stacks/backend_stack.py | 1 + deploy/stacks/lambda_api.py | 204 +++++++++--------- deploy/stacks/pipeline.py | 2 + .../contexts/GenericAuthContext.js | 12 +- frontend/src/authentication/hooks/useToken.js | 4 +- frontend/src/modules/Catalog/views/Catalog.js | 2 +- frontend/src/services/hooks/useClient.js | 2 +- tests_new/integration_tests/README.md | 3 + tests_new/integration_tests/client.py | 65 ++++-- tests_new/integration_tests/requirements.txt | 3 +- 17 files changed, 269 insertions(+), 202 deletions(-) diff --git a/.checkov.baseline b/.checkov.baseline index f80fb4c43..e0bbfce12 100644 --- a/.checkov.baseline +++ b/.checkov.baseline @@ -192,6 +192,13 @@ "CKV_AWS_115" ] }, + { + "resource": "AWS::Lambda::Function.CustomAuthorizerFunctiondevB38B5CCB", + "check_ids": [ + "CKV_AWS_115", + "CKV_AWS_116" + ] + }, { "resource": "AWS::Lambda::Function.ElasticSearchProxyHandlerDBDE7574", "check_ids": [ @@ -210,6 +217,12 @@ "CKV_AWS_158" ] }, + { + "resource": "AWS::Logs::LogGroup.customauthorizerloggroup8F3B5B9D", + "check_ids": [ + "CKV_AWS_158" + ] + }, { "resource": "AWS::Logs::LogGroup.dataalldevapigateway2625FE76", "check_ids": [ diff --git a/backend/dataall/base/utils/api_handler_utils.py b/backend/dataall/base/utils/api_handler_utils.py index df1a998ce..a43c100b8 100644 --- a/backend/dataall/base/utils/api_handler_utils.py +++ b/backend/dataall/base/utils/api_handler_utils.py @@ -24,12 +24,14 @@ ] ENGINE = get_engine(envname=ENVNAME) ALLOWED_ORIGINS = os.getenv('ALLOWED_ORIGINS', '*') +AWS_REGION = os.getenv('AWS_REGION') def redact_creds(event): - if 'headers' in event and 'Authorization' in event['headers']: + if event.get('headers', {}).get('Authorization'): event['headers']['Authorization'] = 'XXXXXXXXXXXX' - if 'multiValueHeaders' in event and 'Authorization' in event['multiValueHeaders']: + + if event.get('multiValueHeaders', {}).get('Authorization'): event['multiValueHeaders']['Authorization'] = 'XXXXXXXXXXXX' return event @@ -115,7 +117,7 @@ def check_reauth(query, auth_time, username): # Determine if there are any Operations that Require ReAuth From SSM Parameter try: reauth_apis = ParameterStoreManager.get_parameter_value( - region=os.getenv('AWS_REGION', 'eu-west-1'), parameter_path=f'/dataall/{ENVNAME}/reauth/apis' + region=AWS_REGION, parameter_path=f'/dataall/{ENVNAME}/reauth/apis' ).split(',') except Exception: log.info('No ReAuth APIs Found in SSM') diff --git a/deploy/custom_resources/custom_authorizer/auth_services.py b/deploy/custom_resources/custom_authorizer/auth_services.py index f5991a22e..52efe14aa 100644 --- a/deploy/custom_resources/custom_authorizer/auth_services.py +++ b/deploy/custom_resources/custom_authorizer/auth_services.py @@ -28,7 +28,7 @@ def generate_policy(verified_claims: dict, effect, incoming_resource_str: str): for claim_name, claim_value in verified_claims.items(): if isinstance(claim_value, list): - verified_claims.update({claim_name: json.dumps(claim_value)}) + verified_claims.update({claim_name: ','.join(claim_value)}) context = {**verified_claims} diff --git a/deploy/custom_resources/custom_authorizer/custom_authorizer_lambda.py b/deploy/custom_resources/custom_authorizer/custom_authorizer_lambda.py index ab710b1ae..47b9223e7 100644 --- a/deploy/custom_resources/custom_authorizer/custom_authorizer_lambda.py +++ b/deploy/custom_resources/custom_authorizer/custom_authorizer_lambda.py @@ -1,5 +1,6 @@ import logging import os +import json from auth_services import AuthServices from jwt_services import JWTServices @@ -16,21 +17,33 @@ Custom Lambda Authorizer is attached to the API Gateway. Check the deploy/stacks/lambda_api.py for more details on deployment """ +OPENID_CONFIG_PATH = os.path.join(os.environ['custom_auth_url'], '.well-known', 'openid-configuration') +JWT_SERVICE = JWTServices(OPENID_CONFIG_PATH) + def lambda_handler(incoming_event, context): # Get the Token which is sent in the Authorization Header + logger.debug(incoming_event) auth_token = incoming_event['headers']['Authorization'] if not auth_token: - raise Exception('Unauthorized . Token not found') + raise Exception('Unauthorized. Missing JWT') - verified_claims = JWTServices.validate_jwt_token(auth_token) - logger.debug(verified_claims) + # Validate User is Active with Proper Access Token + user_info = JWT_SERVICE.validate_access_token(auth_token) + + # Validate JWT + # Note: Removing the 7 Prefix Chars for 'Bearer ' from JWT + verified_claims = JWT_SERVICE.validate_jwt_token(auth_token[7:]) if not verified_claims: raise Exception('Unauthorized. Token is not valid') + logger.debug(verified_claims) + # Generate Allow Policy w/ Context effect = 'Allow' + verified_claims.update(user_info) policy = AuthServices.generate_policy(verified_claims, effect, incoming_event['methodArn']) - logger.debug('Generated policy is ', policy) + logger.debug(f'Generated policy is {json.dumps(policy)}') + print(f'Generated policy is {json.dumps(policy)}') return policy @@ -39,12 +52,13 @@ def lambda_handler(incoming_event, context): # AWS Lambda and any other local environments if __name__ == '__main__': # for testing locally you can enter the JWT ID Token here - token = '' + # + access_token = '' account_id = '' api_gw_id = '' event = { + 'headers': {'Authorization': access_token}, 'type': 'TOKEN', - 'Authorization': token, 'methodArn': f'arn:aws:execute-api:us-east-1:{account_id}:{api_gw_id}/prod/POST/graphql/api', } lambda_handler(event, None) diff --git a/deploy/custom_resources/custom_authorizer/jwt_services.py b/deploy/custom_resources/custom_authorizer/jwt_services.py index 812a03f01..c1f2f6a5c 100644 --- a/deploy/custom_resources/custom_authorizer/jwt_services.py +++ b/deploy/custom_resources/custom_authorizer/jwt_services.py @@ -1,101 +1,81 @@ import os import requests -from jose import jwk -from jose.jwt import get_unverified_header, decode, ExpiredSignatureError, JWTError +import jwt + import logging logger = logging.getLogger() logger.setLevel(os.environ.get('LOG_LEVEL', 'INFO')) -# Configs required to fetch public keys from JWKS -ISSUER_CONFIGS = { - f'{os.environ.get("custom_auth_url")}': { - 'jwks_uri': f'{os.environ.get("custom_auth_jwks_url")}', - 'allowed_audiences': f'{os.environ.get("custom_auth_client")}', - }, -} - -issuer_keys = {} - - -# instead of re-downloading the public keys every time -# we download them only on cold start -# https://aws.amazon.com/blogs/compute/container-reuse-in-lambda/ -def fetch_public_keys(): - try: - for issuer, issuer_config in ISSUER_CONFIGS.items(): - jwks_response = requests.get(issuer_config['jwks_uri']) - jwks_response.raise_for_status() - jwks: dict = jwks_response.json() - for key in jwks['keys']: - value = { - 'issuer': issuer, - 'audience': issuer_config['allowed_audiences'], - 'jwk': jwk.construct(key), - 'public_key': jwk.construct(key).public_key(), - } - issuer_keys.update({key['kid']: value}) - except Exception as e: - raise Exception(f'Unable to fetch public keys due to {str(e)}') - - -fetch_public_keys() # Options to validate the JWT token -# Only modification from default is to turn off verify_at_hash as we don't provide the access token for this validation +# Only modification from default is to turn off verify_aud as Cognito Access Token does not provide this claim jwt_options = { 'verify_signature': True, - 'verify_aud': True, + 'verify_aud': False, 'verify_iat': True, 'verify_exp': True, 'verify_nbf': True, 'verify_iss': True, 'verify_sub': True, 'verify_jti': True, - 'verify_at_hash': False, - 'require_aud': True, - 'require_iat': True, - 'require_exp': True, - 'require_nbf': False, - 'require_iss': True, - 'require_sub': True, - 'require_jti': True, - 'require_at_hash': False, - 'leeway': 0, + 'require': ['iat', 'exp', 'iss', 'sub', 'jti'], } class JWTServices: - @staticmethod - def validate_jwt_token(jwt_token): + def __init__(self, openid_config_path): + # Get OpenID Config JSON + self.openid_config = self._fetch_openid_config(openid_config_path) + + # Init pyJWT.JWKClient with JWK URI + self.jwks_client = jwt.PyJWKClient(self.openid_config.get('jwks_uri')) + + def _fetch_openid_config(self, openid_config_path): + response = requests.get(openid_config_path) + response.raise_for_status() + return response.json() + + def validate_jwt_token(self, jwt_token) -> dict: try: - # Decode and verify the JWT token - header = get_unverified_header(jwt_token) - kid = header['kid'] - if kid not in issuer_keys: - logger.info('Public key not found in provided set of keys') - # Retry Fetching the public certificates again in case rotation occurs and lambda has cached the publicKeys - fetch_public_keys() - if kid not in issuer_keys: - raise Exception('Unauthorized') - public_key = issuer_keys.get(kid) - payload = decode( + # get signing_key from JWT + signing_key = self.jwks_client.get_signing_key_from_jwt(jwt_token) + + # Decode and Verify JWT + payload = jwt.decode( jwt_token, - public_key.get('jwk'), + signing_key.key, algorithms=['RS256', 'HS256'], - issuer=public_key.get('issuer'), - audience=public_key.get('audience'), + issuer=os.environ['custom_auth_url'], + audience=os.environ.get('custom_auth_client'), + leeway=0, options=jwt_options, ) + # verify client_id if Cognito JWT + if 'client_id' in payload and payload['client_id'] != os.environ.get('custom_auth_client'): + raise Exception('Invalid Client ID in JWT Token') + + # verify cid for other IdPs + if 'cid' in payload and payload['cid'] != os.environ.get('custom_auth_client'): + raise Exception('Invalid Client ID in JWT Token') + return payload - except ExpiredSignatureError: + except jwt.exceptions.ExpiredSignatureError as e: logger.error('JWT token has expired.') - return None - except JWTError as e: + raise e + except jwt.exceptions.PyJWTError as e: logger.error(f'JWT token validation failed: {str(e)}') - return None + raise e except Exception as e: logger.error(f'Failed to validate token - {str(e)}') - return None + raise e + + def validate_access_token(self, access_token) -> dict: + # get UserInfo URI from OpenId Configuration + user_info_url = self.openid_config.get('userinfo_endpoint') + r = requests.get(user_info_url, headers={'Authorization': access_token}) + r.raise_for_status() + logger.debug(r.json()) + return r.json() diff --git a/deploy/custom_resources/custom_authorizer/requirements.txt b/deploy/custom_resources/custom_authorizer/requirements.txt index 14e5c340e..db3720bed 100644 --- a/deploy/custom_resources/custom_authorizer/requirements.txt +++ b/deploy/custom_resources/custom_authorizer/requirements.txt @@ -1,10 +1,9 @@ certifi==2024.7.4 charset-normalizer==3.1.0 -ecdsa==0.18.0 idna==3.7 pyasn1==0.5.0 -python-jose==3.3.0 requests==2.32.2 rsa==4.9 six==1.16.0 -urllib3==1.26.19 \ No newline at end of file +urllib3==1.26.19 +pyjwt==2.9.0 \ No newline at end of file diff --git a/deploy/requirements.txt b/deploy/requirements.txt index bf1d93556..157e91b1a 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -2,4 +2,5 @@ aws-cdk-lib==2.160.0 boto3==1.35.26 boto3-stubs==1.35.26 cdk-nag==2.7.2 -typeguard==4.2.1 \ No newline at end of file +typeguard==4.2.1 +cdk-klayers==0.3.0 \ No newline at end of file diff --git a/deploy/stacks/backend_stack.py b/deploy/stacks/backend_stack.py index b2531cd69..20beb33ea 100644 --- a/deploy/stacks/backend_stack.py +++ b/deploy/stacks/backend_stack.py @@ -196,6 +196,7 @@ def __init__( apig_vpce=apig_vpce, prod_sizing=prod_sizing, user_pool=cognito_stack.user_pool if custom_auth is None else None, + user_pool_client=cognito_stack.client if custom_auth is None else None, pivot_role_name=self.pivot_role_name, reauth_ttl=reauth_config.get('ttl', 5) if reauth_config else 5, email_notification_sender_email_id=email_sender, diff --git a/deploy/stacks/lambda_api.py b/deploy/stacks/lambda_api.py index 9825af74d..1151dd994 100644 --- a/deploy/stacks/lambda_api.py +++ b/deploy/stacks/lambda_api.py @@ -22,6 +22,7 @@ RemovalPolicy, BundlingOptions, ) +from cdk_klayers import Klayers from aws_cdk.aws_apigateway import DomainNameOptions, EndpointType, SecurityPolicy from aws_cdk.aws_certificatemanager import Certificate from aws_cdk.aws_ec2 import ( @@ -54,6 +55,7 @@ def __init__( apig_vpce=None, prod_sizing=False, user_pool=None, + user_pool_client=None, pivot_role_name=None, reauth_ttl=5, email_notification_sender_email_id=None, @@ -228,73 +230,95 @@ def __init__( ) ) - if custom_auth is not None: - # Create the custom authorizer lambda - custom_authorizer_assets = os.path.realpath( - os.path.join( - os.path.dirname(__file__), - '..', - 'custom_resources', - 'custom_authorizer', - ) + # Create the custom authorizer lambda + custom_authorizer_assets = os.path.realpath( + os.path.join( + os.path.dirname(__file__), + '..', + 'custom_resources', + 'custom_authorizer', ) + ) + ## GET CONGITO USERL POOL ID and APP CLIENT ID - if not os.path.isdir(custom_authorizer_assets): - raise Exception(f'Custom Authorizer Folder not found at {custom_authorizer_assets}') + if not os.path.isdir(custom_authorizer_assets): + raise Exception(f'Custom Authorizer Folder not found at {custom_authorizer_assets}') - custom_lambda_env = { - 'envname': envname, - 'LOG_LEVEL': 'DEBUG', - 'custom_auth_provider': custom_auth.get('provider'), - 'custom_auth_url': custom_auth.get('url'), - 'custom_auth_client': custom_auth.get('client_id'), - 'custom_auth_jwks_url': custom_auth.get('jwks_url'), - } + custom_lambda_env = { + 'envname': envname, + 'LOG_LEVEL': log_level, + } + if custom_auth: + custom_lambda_env.update( + { + 'custom_auth_provider': custom_auth.get('provider'), + 'custom_auth_url': custom_auth.get('url'), + 'custom_auth_client': custom_auth.get('client_id'), + } + ) for claims_map in custom_auth.get('claims_mapping', {}): custom_lambda_env[claims_map] = custom_auth.get('claims_mapping', '').get(claims_map, '') + else: + custom_lambda_env.update( + { + 'custom_auth_provider': 'Cognito', + 'custom_auth_url': f'https://cognito-idp.{self.region}.amazonaws.com/{user_pool.user_pool_id}', + 'custom_auth_client': user_pool_client.user_pool_client_id, + 'email': 'email', + 'user_id': 'email', + } + ) + + # Initialize Klayers + runtime = _lambda.Runtime.PYTHON_3_9 + klayers = Klayers(self, python_version=runtime, region=self.region) - authorizer_fn_sg = self.create_lambda_sgs(envname, 'customauthorizer', resource_prefix, vpc) - self.authorizer_fn = _lambda.Function( + # get the latest layer version for the cryptography package + cryptography_layer = klayers.layer_version(self, 'cryptography') + + authorizer_fn_sg = self.create_lambda_sgs(envname, 'customauthorizer', resource_prefix, vpc) + self.authorizer_fn = _lambda.Function( + self, + f'CustomAuthorizerFunction-{envname}', + function_name=f'{resource_prefix}-{envname}-custom-authorizer', + log_group=logs.LogGroup( self, - f'CustomAuthorizerFunction-{envname}', - function_name=f'{resource_prefix}-{envname}-custom-authorizer', - log_group=logs.LogGroup( - self, - 'customauthorizerloggroup', - log_group_name=f'/aws/lambda/{resource_prefix}-{envname}-custom-authorizer', - retention=getattr(logs.RetentionDays, self.log_retention_duration), - ), - handler='custom_authorizer_lambda.lambda_handler', - code=_lambda.Code.from_asset( - path=custom_authorizer_assets, - bundling=BundlingOptions( - image=_lambda.Runtime.PYTHON_3_9.bundling_image, - local=SolutionBundling(source_path=custom_authorizer_assets), - ), + 'customauthorizerloggroup', + log_group_name=f'/aws/lambda/{resource_prefix}-{envname}-custom-authorizer', + retention=getattr(logs.RetentionDays, self.log_retention_duration), + ), + handler='custom_authorizer_lambda.lambda_handler', + code=_lambda.Code.from_asset( + path=custom_authorizer_assets, + bundling=BundlingOptions( + image=_lambda.Runtime.PYTHON_3_9.bundling_image, + local=SolutionBundling(source_path=custom_authorizer_assets), ), - memory_size=512 if prod_sizing else 256, - description='dataall Custom authorizer replacing cognito authorizer', - timeout=Duration.seconds(20), - environment=custom_lambda_env, - environment_encryption=lambda_env_key, - vpc=vpc, - security_groups=[authorizer_fn_sg], - runtime=_lambda.Runtime.PYTHON_3_9, - ) + ), + memory_size=512 if prod_sizing else 256, + description='dataall Custom authorizer replacing cognito authorizer', + timeout=Duration.seconds(20), + environment=custom_lambda_env, + environment_encryption=lambda_env_key, + vpc=vpc, + security_groups=[authorizer_fn_sg], + runtime=runtime, + layers=[cryptography_layer], + ) - # Add NAT Connectivity For Custom Authorizer Lambda - self.authorizer_fn.connections.allow_to( - ec2.Peer.any_ipv4(), ec2.Port.tcp(443), 'Allow NAT Internet Access SG Egress' - ) + # Add NAT Connectivity For Custom Authorizer Lambda + self.authorizer_fn.connections.allow_to( + ec2.Peer.any_ipv4(), ec2.Port.tcp(443), 'Allow NAT Internet Access SG Egress' + ) - # Store custom authorizer's ARN in ssm - ssm.StringParameter( - self, - f'{resource_prefix}-{envname}-custom-authorizer-arn', - parameter_name=f'/dataall/{envname}/customauth/customauthorizerarn', - string_value=self.authorizer_fn.function_arn, - ) + # Store custom authorizer's ARN in ssm + ssm.StringParameter( + self, + f'{resource_prefix}-{envname}-custom-authorizer-arn', + parameter_name=f'/dataall/{envname}/customauth/customauthorizerarn', + string_value=self.authorizer_fn.function_arn, + ) # Add VPC Endpoint Connectivity if vpce_connection: @@ -585,41 +609,31 @@ def set_up_graphql_api_gateway( user_pool, custom_auth, ): - if custom_auth is None: - cognito_authorizer = apigw.CognitoUserPoolsAuthorizer( - self, - 'CognitoAuthorizer', - cognito_user_pools=[user_pool], - authorizer_name=f'{resource_prefix}-{envname}-cognito-authorizer', - identity_source='method.request.header.Authorization', - results_cache_ttl=Duration.minutes(60), - ) - else: - # Create a custom Authorizer - custom_authorizer_role = iam.Role( - self, - f'{resource_prefix}-{envname}-custom-authorizer-role', - role_name=f'{resource_prefix}-{envname}-custom-authorizer-role', - assumed_by=iam.ServicePrincipal('apigateway.amazonaws.com'), - description='Allow Custom Authorizer to call custom auth lambda', - ) - custom_authorizer_role.add_to_policy( - iam.PolicyStatement( - effect=iam.Effect.ALLOW, - actions=['lambda:InvokeFunction'], - resources=[self.authorizer_fn.function_arn], - ) + # Create a custom Authorizer + custom_authorizer_role = iam.Role( + self, + f'{resource_prefix}-{envname}-custom-authorizer-role', + role_name=f'{resource_prefix}-{envname}-custom-authorizer-role', + assumed_by=iam.ServicePrincipal('apigateway.amazonaws.com'), + description='Allow Custom Authorizer to call custom auth lambda', + ) + custom_authorizer_role.add_to_policy( + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=['lambda:InvokeFunction'], + resources=[self.authorizer_fn.function_arn], ) + ) - custom_authorizer = apigw.RequestAuthorizer( - self, - 'CustomAuthorizer', - handler=self.authorizer_fn, - identity_sources=[apigw.IdentitySource.header('Authorization')], - authorizer_name=f'{resource_prefix}-{envname}-custom-authorizer', - assume_role=custom_authorizer_role, - results_cache_ttl=Duration.minutes(60), - ) + custom_authorizer = apigw.RequestAuthorizer( + self, + 'CustomAuthorizer', + handler=self.authorizer_fn, + identity_sources=[apigw.IdentitySource.header('Authorization')], + authorizer_name=f'{resource_prefix}-{envname}-custom-authorizer', + assume_role=custom_authorizer_role, + results_cache_ttl=Duration.minutes(1), + ) if not internet_facing: if apig_vpce: api_vpc_endpoint = InterfaceVpcEndpoint.from_interface_vpc_endpoint_attributes( @@ -754,10 +768,8 @@ def set_up_graphql_api_gateway( ) graphql_proxy.add_method( 'POST', - authorizer=cognito_authorizer if custom_auth is None else custom_authorizer, - authorization_type=apigw.AuthorizationType.COGNITO - if custom_auth is None - else apigw.AuthorizationType.CUSTOM, + authorizer=custom_authorizer, + authorization_type=apigw.AuthorizationType.CUSTOM, request_validator=request_validator, request_models={'application/json': graphql_validation_model}, ) @@ -796,10 +808,8 @@ def set_up_graphql_api_gateway( ) search_proxy.add_method( 'POST', - authorizer=cognito_authorizer if custom_auth is None else custom_authorizer, - authorization_type=apigw.AuthorizationType.COGNITO - if custom_auth is None - else apigw.AuthorizationType.CUSTOM, + authorizer=custom_authorizer, + authorization_type=apigw.AuthorizationType.CUSTOM, request_validator=request_validator, request_models={'application/json': search_validation_model}, ) diff --git a/deploy/stacks/pipeline.py b/deploy/stacks/pipeline.py index a7ba892f6..a676f07d5 100644 --- a/deploy/stacks/pipeline.py +++ b/deploy/stacks/pipeline.py @@ -716,6 +716,8 @@ def set_approval_tests_stage( 'aws sts get-caller-identity --profile buildprofile', f'export COGNITO_CLIENT=$(aws ssm get-parameter --name /dataall/{target_env["envname"]}/cognito/appclient --profile buildprofile --output text --query "Parameter.Value")', f'export API_ENDPOINT=$(aws ssm get-parameter --name /dataall/{target_env["envname"]}/apiGateway/backendUrl --profile buildprofile --output text --query "Parameter.Value")', + f'export IDP_DOMAIN_URL=https://$(aws ssm get-parameter --name /dataall/{target_env["envname"]}/cognito/domain --profile buildprofile --output text --query "Parameter.Value").auth.{target_env["region"]}.amazoncognito.com', + f'export DATAALL_DOMAIN_URL=https://$(aws ssm get-parameter --name /dataall/{target_env["envname"]}/CloudfrontDistributionDomainName --profile buildprofile --output text --query "Parameter.Value")', f'export TESTDATA=$(aws ssm get-parameter --name /dataall/{target_env["envname"]}/testdata --profile buildprofile --output text --query "Parameter.Value")', f'export ENVNAME={target_env["envname"]}', f'export AWS_REGION={target_env["region"]}', diff --git a/frontend/src/authentication/contexts/GenericAuthContext.js b/frontend/src/authentication/contexts/GenericAuthContext.js index 07fbcc4df..d1fec575b 100644 --- a/frontend/src/authentication/contexts/GenericAuthContext.js +++ b/frontend/src/authentication/contexts/GenericAuthContext.js @@ -84,7 +84,8 @@ export const GenericAuthProvider = (props) => { email: user.email, name: user.email, id_token: user.id_token, - short_id: user.short_id + short_id: user.short_id, + access_token: user.access_token } } }); @@ -129,7 +130,8 @@ export const GenericAuthProvider = (props) => { email: user.email, name: user.email, id_token: user.id_token, - short_id: user.short_id + short_id: user.short_id, + access_token: user.access_token } } }); @@ -178,6 +180,7 @@ export const GenericAuthProvider = (props) => { process.env.REACT_APP_CUSTOM_AUTH_EMAIL_CLAIM_MAPPING ], id_token: auth.user.id_token, + access_token: auth.user.access_token, short_id: auth.user.profile[ process.env.REACT_APP_CUSTOM_AUTH_USERID_CLAIM_MAPPING @@ -188,6 +191,7 @@ export const GenericAuthProvider = (props) => { return { email: user.attributes.email, id_token: user.signInUserSession.idToken.jwtToken, + access_token: user.signInUserSession.accessToken.jwtToken, short_id: 'none' }; } @@ -240,7 +244,7 @@ export const GenericAuthProvider = (props) => { } }); } else { - await Auth.signOut(); + await Auth.signOut({ global: true }); dispatch({ type: 'LOGOUT', payload: { @@ -271,7 +275,7 @@ export const GenericAuthProvider = (props) => { console.error('Failed to ReAuth', error); } } else { - await Auth.signOut(); + await Auth.signOut({ global: true }); dispatch({ type: 'REAUTH', payload: { diff --git a/frontend/src/authentication/hooks/useToken.js b/frontend/src/authentication/hooks/useToken.js index 08cfd5668..8b17536db 100644 --- a/frontend/src/authentication/hooks/useToken.js +++ b/frontend/src/authentication/hooks/useToken.js @@ -20,14 +20,14 @@ export const useToken = () => { if (!auth.user) { await auth.signinSilent(); } - const t = auth.user.id_token; + const t = auth.user.access_token; setToken(t); } catch (error) { if (!auth) throw Error('User Token Not Found !'); } } else { const session = await Auth.currentSession(); - const t = await session.getIdToken().getJwtToken(); + const t = await session.getAccessToken().getJwtToken(); setToken(t); } } catch (error) { diff --git a/frontend/src/modules/Catalog/views/Catalog.js b/frontend/src/modules/Catalog/views/Catalog.js index c351c5da7..d8d21b2a5 100644 --- a/frontend/src/modules/Catalog/views/Catalog.js +++ b/frontend/src/modules/Catalog/views/Catalog.js @@ -234,7 +234,7 @@ const Catalog = () => { url: transformedRequest.url, credentials: { token }, headers: { - Authorization: token, + Authorization: token ? `Bearer ${token}` : '', AccessKeyId: 'None', SecretKey: 'None' } diff --git a/frontend/src/services/hooks/useClient.js b/frontend/src/services/hooks/useClient.js index bb2762ad4..9e20d4619 100644 --- a/frontend/src/services/hooks/useClient.js +++ b/frontend/src/services/hooks/useClient.js @@ -54,7 +54,7 @@ export const useClient = () => { const authLink = new ApolloLink((operation, forward) => { operation.setContext({ headers: { - Authorization: t ? `${t}` : '', + Authorization: t ? `Bearer ${t}` : '', AccessKeyId: 'none', SecretKey: 'none' } diff --git a/tests_new/integration_tests/README.md b/tests_new/integration_tests/README.md index 63493bc16..054b8fb31 100644 --- a/tests_new/integration_tests/README.md +++ b/tests_new/integration_tests/README.md @@ -173,6 +173,9 @@ You can also run the tests locally by... export AWS_REGION = "Introduce backend region" export COGNITO_CLIENT = "Introduce Cognito client id" export API_ENDPOINT = "Introduce API endpoint url" + export IDP_DOMAIN_URL = "Introduce your Identity Provider domain url" + export DATAALL_DOMAIN_URL = "Introduce data.all frontend domain url" + echo "add your testdata here" > testdata.json make integration-tests ``` diff --git a/tests_new/integration_tests/client.py b/tests_new/integration_tests/client.py index 75fa0455e..bef02d39e 100644 --- a/tests_new/integration_tests/client.py +++ b/tests_new/integration_tests/client.py @@ -1,9 +1,12 @@ import requests -import boto3 import os +import uuid +from urllib.parse import parse_qs, urlparse from munch import DefaultMunch from retrying import retry from integration_tests.errors import GqlError +from oauthlib.oauth2 import WebApplicationClient +from requests_oauthlib import OAuth2Session ENVNAME = os.getenv('ENVNAME', 'dev') @@ -17,7 +20,7 @@ class Client: def __init__(self, username, password): self.username = username self.password = password - self.token = self._get_jwt_token() + self.access_token = self._get_jwt_tokens() @retry( retry_on_exception=_retry_if_connection_error, @@ -27,7 +30,7 @@ def __init__(self, username, password): ) def query(self, query: str): graphql_endpoint = os.path.join(os.environ['API_ENDPOINT'], 'graphql', 'api') - headers = {'AccessKeyId': 'none', 'SecretKey': 'none', 'authorization': self.token} + headers = {'accesskeyid': 'none', 'SecretKey': 'none', 'Authorization': f'Bearer {self.access_token}'} r = requests.post(graphql_endpoint, json=query, headers=headers) if errors := r.json().get('errors'): raise GqlError(errors) @@ -35,16 +38,50 @@ def query(self, query: str): return DefaultMunch.fromDict(r.json()) - def _get_jwt_token(self): - cognito_client = boto3.client('cognito-idp', region_name=os.getenv('AWS_REGION', 'eu-west-1')) - kwargs = { - 'ClientId': os.environ['COGNITO_CLIENT'], - 'AuthFlow': 'USER_PASSWORD_AUTH', - 'AuthParameters': { - 'USERNAME': self.username, - 'PASSWORD': self.password, - }, + def _get_jwt_tokens(self): + token = uuid.uuid4() + scope = 'aws.cognito.signin.user.admin openid' + + idp_domain_url = os.environ['IDP_DOMAIN_URL'] + + token_url = os.path.join(idp_domain_url, 'oauth2', 'token') + login_url = os.path.join(idp_domain_url, 'login') + + client_id = os.environ['COGNITO_CLIENT'] + redirect_uri = os.environ['DATAALL_DOMAIN_URL'] + + data = { + '_csrf': token, + 'username': self.username, + 'password': self.password, + } + params = { + 'client_id': client_id, + 'scope': scope, + 'redirect_uri': redirect_uri, + 'response_type': 'code', } - resp = cognito_client.initiate_auth(**kwargs) - return resp['AuthenticationResult']['IdToken'] + headers = {'cookie': f'XSRF-TOKEN={token}; csrf-state=""; csrf-state-legacy=""'} + r = requests.post( + login_url, + params=params, + data=data, + headers=headers, + allow_redirects=False, + ) + + r.raise_for_status() + + code = parse_qs(urlparse(r.headers['location']).query)['code'][0] + + client = WebApplicationClient(client_id=client_id) + oauth = OAuth2Session(client=client, redirect_uri=redirect_uri) + token = oauth.fetch_token( + token_url=token_url, + client_id=client_id, + code=code, + include_client_id=True, + ) + + return token.get('access_token') diff --git a/tests_new/integration_tests/requirements.txt b/tests_new/integration_tests/requirements.txt index 9c274748e..06deb022a 100644 --- a/tests_new/integration_tests/requirements.txt +++ b/tests_new/integration_tests/requirements.txt @@ -8,4 +8,5 @@ pytest-dependency==0.5.1 requests==2.32.2 dataclasses-json==0.6.6 werkzeug==3.0.6 -retrying==1.3.4 \ No newline at end of file +retrying==1.3.4 +requests-oauthlib==2.0.0 \ No newline at end of file