From 6e0765b18f954b83f4b9d064fecfffa32cf61a31 Mon Sep 17 00:00:00 2001 From: kdraai Date: Tue, 17 Dec 2024 18:39:26 +0200 Subject: [PATCH 1/7] apigw certificate bound access token example added --- .../.gitignore | 244 ++++++++++++++++++ .../MakeFile | 11 + .../README.md | 89 +++++++ .../__init__.py | 0 .../example-pattern.json | 58 +++++ .../handlers/__init__.py | 0 .../handlers/authorizer.py | 187 ++++++++++++++ .../handlers/pre_token_gen_lambda.py | 63 +++++ .../template.yaml | 188 ++++++++++++++ 9 files changed, 840 insertions(+) create mode 100644 apigw-cognito-certificate-bound-access-token/.gitignore create mode 100644 apigw-cognito-certificate-bound-access-token/MakeFile create mode 100644 apigw-cognito-certificate-bound-access-token/README.md create mode 100644 apigw-cognito-certificate-bound-access-token/__init__.py create mode 100644 apigw-cognito-certificate-bound-access-token/example-pattern.json create mode 100644 apigw-cognito-certificate-bound-access-token/handlers/__init__.py create mode 100644 apigw-cognito-certificate-bound-access-token/handlers/authorizer.py create mode 100644 apigw-cognito-certificate-bound-access-token/handlers/pre_token_gen_lambda.py create mode 100644 apigw-cognito-certificate-bound-access-token/template.yaml diff --git a/apigw-cognito-certificate-bound-access-token/.gitignore b/apigw-cognito-certificate-bound-access-token/.gitignore new file mode 100644 index 000000000..4808264db --- /dev/null +++ b/apigw-cognito-certificate-bound-access-token/.gitignore @@ -0,0 +1,244 @@ + +# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Ruby plugin and RubyMine +/.rakeTasks + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Build folder + +*/build/* + +# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode \ No newline at end of file diff --git a/apigw-cognito-certificate-bound-access-token/MakeFile b/apigw-cognito-certificate-bound-access-token/MakeFile new file mode 100644 index 000000000..031b88e42 --- /dev/null +++ b/apigw-cognito-certificate-bound-access-token/MakeFile @@ -0,0 +1,11 @@ +.PHONY: build-CombinedLayer clean + +# Default target +build-CombinedLayer: + mkdir -p "$(ARTIFACTS_DIR)/python" + pip install requests -t "$(ARTIFACTS_DIR)/python" + pip install python-jose -t "$(ARTIFACTS_DIR)/python" + pip install cryptography -t "$(ARTIFACTS_DIR)/python" + +clean: + rm -rf "$(ARTIFACTS_DIR)" \ No newline at end of file diff --git a/apigw-cognito-certificate-bound-access-token/README.md b/apigw-cognito-certificate-bound-access-token/README.md new file mode 100644 index 000000000..035ccfefb --- /dev/null +++ b/apigw-cognito-certificate-bound-access-token/README.md @@ -0,0 +1,89 @@ +# Certificate-Bound Access Tokens using API Gateway and Cognito + +This pattern makes use of API Gateway and Cognito to implement certificate-bound access tokens. For more on certificate-boud access tokens, review the [RFC](https://datatracker.ietf.org/doc/html/rfc8705). This solution has some manual steps which will be discussed later. + +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. + +## 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 Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)~ (AWS SAM) installed +* [Docker installed](https://docs.docker.com/engine/install/). +* A domain that you own. +* A certificate issued under the domain you own. +* Create client certificate as per the [mTLS blogpost](https://aws.amazon.com/blogs/compute/introducing-mutual-tls-authentication-for-amazon-api-gateway/). + +## 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 to the pattern directory: +``` +cd apigw-certificate-bound-access-tokens +``` + +3. Ensure that you add the relevant parameters to `samconfig.toml`. +* stack_name +* s3_bucket +* s3_prefix +* region +* parameter_overrides. + * BucketName - to store client certificate and trust store. + * CACertKey - trust store. + * ClientCertKey - client certificate Amazon S3 object key. + * CustomDomainCertArn - custom domain AWS Certificate Manager certficate ARN. + * CustomDomainName - custom domain name for API Gateway. Must match CustomDomainCertArn. + +4. Build the solution: +``` +sam build +``` + +5. Deploy the solution: +``` +sam deploy +``` + +## How it works + +This pattern creates an Amazon API Gateway REST API as well as a custom domain name and enables mTLS. Further, it creates a Cognito User Pool. The Cognito User Pool is used to issue certificate-bound access tokens. The REST API makes use of an authorizer to compare the "cnf" claim in the access token to the fingerprint of the client certificate sent as part of the mutual authentication TLS handshake. + +## Testing + +1. Determine the fingerprint of the client certificate and base64 encode it: +``` +openssl x509 -in client-cert.crt -noout -fingerprint -sha256 | cut -d'=' -f2 | tr -d ':' | xxd -r -p | base64 | tr -d '=' +``` + +2. Navigate to the Cognito User Pool created in the solution. [Then create and verify](https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html) a user. + +3. Add the certificate fingerprint to the `custom:cert_fingerprint` custom attribute of the user. + +4. [Create a DNS record](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-api-gateway.html) for the custom domain. + +5. [Get an access token](https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-public-server-side.html) from Cognito. + +6. Test the solution: + +``` +curl -v https:///example --cert client-cert.crt --key client-cert.key -H "Authorization: " +``` + +## Cleanup + +1. Delete the stack + ```bash + aws cloudformation delete-stack --stack-name STACK_NAME + ``` +1. Confirm the stack has been deleted + ```bash + aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'STACK_NAME')].StackStatus" + ``` +---- +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-cognito-certificate-bound-access-token/__init__.py b/apigw-cognito-certificate-bound-access-token/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apigw-cognito-certificate-bound-access-token/example-pattern.json b/apigw-cognito-certificate-bound-access-token/example-pattern.json new file mode 100644 index 000000000..370714a77 --- /dev/null +++ b/apigw-cognito-certificate-bound-access-token/example-pattern.json @@ -0,0 +1,58 @@ +{ + "title": "Certificate-Bound Access Tokens using API Gateway and Cognito", + "description": "Creating an API Gateway and Cognito to implement certificate-bound access tokens", + "language": "Python", + "level": "300", + "framework": "SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern creates an Amazon API Gateway REST API as well as a custom domain name and enables mTLS.", + "Further, it creates a Cognito User Pool. The Cognito User Pool is used to issue certificate-bound access tokens.", + "The REST API makes use of an authorizer to compare the 'cnf' claim in the access token to the fingerprint of the client certificate sent as part of the mutual authentication TLS handshake" + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-cognito-certificate-bound-access-token", + "templateURL": "serverless-patterns/apigw-cognito-certificate-bound-access-token", + "projectFolder": "apigw-cognito-certificate-bound-access-token", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "API Gateway mTLS", + "link": "https://aws.amazon.com/blogs/compute/introducing-mutual-tls-authentication-for-amazon-api-gateway/" + }, + { + "text": "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens", + "link": "https://datatracker.ietf.org/doc/html/rfc8705" + } + ] + }, + "deploy": { + "text": [ + "sam deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk delete." + ] + }, + "authors": [ + { + "name": "Kevin Draai", + "image": "https://media.licdn.com/dms/image/v2/C4D03AQFaayZVs_v_bA/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1556110763035?e=1740009600&v=beta&t=fJLsUYmOsXYgvp0m7oE_eUkPQ_V-WNrQsSJggGdf3d0", + "bio": "Senior Cloud Support Engineer", + "linkedin": "https://www.linkedin.com/in/kevin-draai-30587780?lipi=urn%3Ali%3Apage%3Ad_flagship3_profile_view_base_contact_details%3BiRU2NMnHQpSHbj64llXe3A%3D%3D" + } + ] +} diff --git a/apigw-cognito-certificate-bound-access-token/handlers/__init__.py b/apigw-cognito-certificate-bound-access-token/handlers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apigw-cognito-certificate-bound-access-token/handlers/authorizer.py b/apigw-cognito-certificate-bound-access-token/handlers/authorizer.py new file mode 100644 index 000000000..c7a67e02b --- /dev/null +++ b/apigw-cognito-certificate-bound-access-token/handlers/authorizer.py @@ -0,0 +1,187 @@ +import json +import boto3 +import logging +from jose import jwt +from jose.utils import base64url_decode +from botocore.exceptions import ClientError +from cryptography import x509 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric import rsa + +import requests +from datetime import datetime, timezone +import os +import traceback + +# Set up logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +s3_client = boto3.client('s3') + +def lambda_handler(event, context): + logger.info(f"Received event: {json.dumps(event)}") + bucket_name = os.environ['BUCKET_NAME'] + object_key = os.environ['CACERT_KEY'] + + # Extract the Authorization header + token = event['headers']['authorization'] + token = token.split(" ")[1] if token.startswith("Bearer ") else token + + try: + # Verify the JWT token + verified_claims = verify_jwt(token) + + # Extract the client certificate from the request + client_cert_pem = event['requestContext']['identity']['clientCert']['clientCertPem'] + + # Verify the certificate + if not verify_certificate(client_cert_pem, bucket_name, object_key): + logger.error('Invalid client certificate') + return generate_policy('user', 'Deny', event['methodArn']) + + # Check if the certificate is bound to the token + if not is_cert_bound_to_token(verified_claims, client_cert_pem): + logger.error("Certificate is not bound to the token") + return generate_policy('user', 'Deny', event['methodArn']) + + return generate_policy('user', 'Allow', event['methodArn']) + + except Exception as e: + # Log the full stack trace + logger.error("An error occurred:") + logger.error(traceback.format_exc()) + + # You can also log the specific error message + logger.error(f"Error message: {str(e)}") + + # Return a Deny policy in case of any error + return generate_policy('user', 'Deny', event['methodArn']) + +def verify_jwt(token): + # Get the kid (Key ID) from the token header + headers = jwt.get_unverified_headers(token) + kid = headers['kid'] + + # Get the Cognito User Pool ID from the token + payload = jwt.get_unverified_claims(token) + user_pool_id = payload['iss'].split('/')[-1] + + # Get the public keys + keys_url = f'https://cognito-idp.{boto3.Session().region_name}.amazonaws.com/{user_pool_id}/.well-known/jwks.json' + response = requests.get(keys_url) + keys = response.json()['keys'] + + # Find the correct public key + public_key = next((key for key in keys if key['kid'] == kid), None) + if not public_key: + raise Exception('Public key not found') + + # Verify the token + verified_claims = jwt.decode( + token, + public_key, + algorithms=['RS256'], + audience=payload.get('aud') if payload.get('aud') else payload.get('client_id'), + issuer=f'https://cognito-idp.{boto3.Session().region_name}.amazonaws.com/{user_pool_id}' + ) + return verified_claims + +from datetime import datetime, timezone +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend + +def verify_certificate(cert_pem, bucket_name, object_key): + try: + # Load the client certificate + cert = x509.load_pem_x509_certificate(cert_pem.encode(), default_backend()) + + # Load the CA certificate + ca_cert_pem = get_ca_cert(bucket_name, object_key) + if not ca_cert_pem: + return False + ca_cert = x509.load_pem_x509_certificate(ca_cert_pem.encode(), default_backend()) + + # Verify the certificate signature + ca_public_key = ca_cert.public_key() + if isinstance(ca_public_key, rsa.RSAPublicKey): + try: + ca_public_key.verify( + cert.signature, + cert.tbs_certificate_bytes, + padding.PKCS1v15(), + cert.signature_hash_algorithm + ) + except InvalidSignature: + logger.error("Invalid certificate signature") + return False + else: + logger.error("Unsupported public key type") + return False + + # Check if the certificate has expired + now = datetime.now(timezone.utc) + + if now < cert.not_valid_before_utc or now > cert.not_valid_after_utc: + logger.error("Certificate has expired or is not yet valid") + return False + + # Check revocation status + # implement this + + return True + + except Exception as e: + logger.error(f"Certificate verification error: {str(e)}") + return False + +def get_ca_cert(bucket_name, object_key): + try: + response = s3_client.get_object(Bucket=bucket_name, Key=object_key) + return response['Body'].read().decode('utf-8') + except ClientError as e: + logger.error(f"Error retrieving CA cert: {str(e)}") + return None + +def check_revocation_status(cert): + # implement this + pass + +def is_cert_bound_to_token(claims, cert_pem): + try: + cert = x509.load_pem_x509_certificate(cert_pem.encode(), default_backend()) + fingerprint = cert.fingerprint(hashes.SHA256()).hex() + x5t_claim = claims.get('cnf', {}).get('x5t#S256') + + if not x5t_claim: + logger.error("x5t#S256 claim not found in token") + return False + + # Convert the x5t_claim to bytes before decoding + x5t_hex = base64url_decode(x5t_claim.encode('ascii')).hex() + + return fingerprint == x5t_hex + except Exception as e: + logger.error(f"Error in is_cert_bound_to_token: {str(e)}") + logger.error(traceback.format_exc()) + return False + +def generate_policy(principal_id, effect, resource): + return { + 'principalId': principal_id, + 'policyDocument': { + 'Version': '2012-10-17', + 'Statement': [ + { + 'Action': 'execute-api:Invoke', + 'Effect': effect, + 'Resource': resource + } + ] + } + } \ No newline at end of file diff --git a/apigw-cognito-certificate-bound-access-token/handlers/pre_token_gen_lambda.py b/apigw-cognito-certificate-bound-access-token/handlers/pre_token_gen_lambda.py new file mode 100644 index 000000000..1fa3d413c --- /dev/null +++ b/apigw-cognito-certificate-bound-access-token/handlers/pre_token_gen_lambda.py @@ -0,0 +1,63 @@ +import boto3 +import json +import logging + +# Set up logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +# Initialize the Cognito client +cognito = boto3.client('cognito-idp') + +def lambda_handler(event, context): + logger.info(f"Received event: {json.dumps(event)}") + + # Extract relevant information from the event + user_pool_id = event['userPoolId'] + username = event['userName'] + + try: + # Retrieve the user's attributes from Cognito + user_response = cognito.admin_get_user( + UserPoolId=user_pool_id, + Username=username + ) + + # Find the certificate fingerprint in the user's attributes + cert_fingerprint = next( + (attr['Value'] for attr in user_response['UserAttributes'] + if attr['Name'] == 'custom:cert_fingerprint'), + None + ) + + if not cert_fingerprint: + logger.warning(f"No certificate fingerprint found for user {username}") + return event + + logger.info(f"Found certificate fingerprint for user {username}: {cert_fingerprint}") + + # Prepare the response structure + response = { + 'claimsAndScopeOverrideDetails': { + 'accessTokenGeneration': { + 'claimsToAddOrOverride': { + 'cnf': { + 'x5t#S256': cert_fingerprint + } + } + } + } + } + + # Add the response to the event + event['response'] = response + + logger.info(f"Added certificate fingerprint and version to access token claims for user {username}") + + except Exception as e: + logger.error(f"Error processing pre-token generation for user {username}: {str(e)}") + # In case of an error, we don't modify the event + # This allows the token generation to proceed without the certificate binding + + logger.info(f"Returning event: {json.dumps(event)}") + return event \ No newline at end of file diff --git a/apigw-cognito-certificate-bound-access-token/template.yaml b/apigw-cognito-certificate-bound-access-token/template.yaml new file mode 100644 index 000000000..1d3443e7b --- /dev/null +++ b/apigw-cognito-certificate-bound-access-token/template.yaml @@ -0,0 +1,188 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + Certificate-Bound Access Tokens using Amazon API Gateway and Amazon Cognito + + This SAM template deploys a secure API infrastructure with the following components: + - An API Gateway with a custom domain and mutual TLS (mTLS) authentication + - A Lambda-based custom authorizer for API requests + - A Cognito User Pool with a pre-token generation Lambda trigger to bind the certificate to the access token + + The infrastructure implements Certificate-Bound Access Tokens within AWS. + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 10 + +Parameters: + BucketName: + Type: String + Description: Name of the S3 bucket containing the certificates + + CACertKey: + Type: String + Description: Object key for the certificates in the S3 bucket + + ClientCertKey: + Type: String + Description: Object key for the client certificate fingerprint + + CustomDomainCertArn: + Type: String + Description: The ARN of the issued certificate for API Gateway custom domain + + CustomDomainName: + Type: String + Description: Custom domain name for the API + +Resources: + CombinedLayer: + Type: AWS::Serverless::LayerVersion + Properties: + LayerName: combined-dependencies-layer + ContentUri: ./ + CompatibleRuntimes: + - python3.12 + Metadata: + BuildMethod: makefile + BuildArchitecture: x86_64 + Makefile: Makefile + BuildTarget: build-CombinedLayer + + Authorizer: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./ + Handler: handlers.authorizer.lambda_handler + Runtime: python3.12 + Layers: + - !Ref CombinedLayer + + Policies: + - AWSLambdaBasicExecutionRole + - Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - s3:GetObject + Resource: !Sub 'arn:aws:s3:::${BucketName}/${CACertKey}' + - Effect: Allow + Action: + - cognito-idp:GetSigningCertificate + Resource: '*' + Environment: + Variables: + BUCKET_NAME: !Ref BucketName + CACERT_KEY: !Ref CACertKey + + PreTokenGenerationFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./ + Handler: handlers.pre_token_gen_lambda.lambda_handler + Runtime: python3.12 + Policies: + - AWSLambdaBasicExecutionRole + - Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - cognito-idp:AdminGetUser + Resource: !Sub 'arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/*' + + CognitoUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: !Sub ${AWS::StackName}-user-pool + AutoVerifiedAttributes: + - email + Policies: + PasswordPolicy: + MinimumLength: 8 + Schema: + - Name: cert_fingerprint + AttributeDataType: String + Mutable: true + LambdaConfig: + PreTokenGenerationConfig: + LambdaArn: !GetAtt PreTokenGenerationFunction.Arn + LambdaVersion: V2_0 + + CognitoUserPoolClient: + Type: AWS::Cognito::UserPoolClient + Properties: + ClientName: !Sub ${AWS::StackName}-user-pool-client + UserPoolId: !Ref CognitoUserPool + GenerateSecret: false + ExplicitAuthFlows: + - ALLOW_USER_PASSWORD_AUTH + - ALLOW_REFRESH_TOKEN_AUTH + + Api: + Type: AWS::Serverless::Api + Properties: + StageName: prod + Auth: + DefaultAuthorizer: CustomAuthorizer + Authorizers: + CustomAuthorizer: + FunctionArn: !GetAtt Authorizer.Arn + FunctionPayloadType: REQUEST + Identity: + Headers: + - Authorization + Domain: + DomainName: !Ref CustomDomainName + CertificateArn: !Ref CustomDomainCertArn + EndpointConfiguration: REGIONAL + SecurityPolicy: TLS_1_2 + MutualTlsAuthentication: + TruststoreUri: !Sub 's3://${BucketName}/${CACertKey}' + BasePath: ["/"] + + ExampleFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./ + Handler: index.lambda_handler + Runtime: python3.12 + InlineCode: | + import json + + def lambda_handler(event, context): + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': 'Hello from the example function!', + 'event': event + }) + } + Events: + GetExample: + Type: Api + Properties: + Path: /example + Method: get + RestApiId: !Ref Api + + LambdaInvokePermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref PreTokenGenerationFunction + Principal: cognito-idp.amazonaws.com + SourceArn: !GetAtt CognitoUserPool.Arn + +Outputs: + CustomDomainUrl: + Description: "Custom domain URL for the API" + Value: !Sub "https://${CustomDomainName}/" + + UserPoolId: + Description: "ID of the Cognito User Pool" + Value: !Ref CognitoUserPool + + UserPoolClientId: + Description: "ID of the Cognito User Pool Client" + Value: !Ref CognitoUserPoolClient \ No newline at end of file From 94438794258df401557b022d77391e1b0a96c54a Mon Sep 17 00:00:00 2001 From: kdraai Date: Fri, 24 Jan 2025 10:28:40 +0200 Subject: [PATCH 2/7] updated README.md and Makefile file name --- .../README.md | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/apigw-cognito-certificate-bound-access-token/README.md b/apigw-cognito-certificate-bound-access-token/README.md index 035ccfefb..90b0ae287 100644 --- a/apigw-cognito-certificate-bound-access-token/README.md +++ b/apigw-cognito-certificate-bound-access-token/README.md @@ -1,4 +1,4 @@ -# Certificate-Bound Access Tokens using API Gateway and Cognito +# Certificate-Bound Access Tokens using Amazon API Gateway and Amazon Cognito This pattern makes use of API Gateway and Cognito to implement certificate-bound access tokens. For more on certificate-boud access tokens, review the [RFC](https://datatracker.ietf.org/doc/html/rfc8705). This solution has some manual steps which will be discussed later. @@ -6,9 +6,9 @@ Important: this application uses various AWS services and there are costs associ ## 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 Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)~ (AWS SAM) installed +* [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 Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)~ (AWS SAM) installed * [Docker installed](https://docs.docker.com/engine/install/). * A domain that you own. * A certificate issued under the domain you own. @@ -23,10 +23,10 @@ git clone https://github.com/aws-samples/serverless-patterns 2. Change directory to the pattern directory: ``` -cd apigw-certificate-bound-access-tokens +cd apigw-cognito-certificate-bound-access-token ``` -3. Ensure that you add the relevant parameters to `samconfig.toml`. +3. Ensure that you add the relevant parameters to `samconfig.toml`: * stack_name * s3_bucket * s3_prefix @@ -40,7 +40,7 @@ cd apigw-certificate-bound-access-tokens 4. Build the solution: ``` -sam build +sam build --use-container ``` 5. Deploy the solution: @@ -50,7 +50,7 @@ sam deploy ## How it works -This pattern creates an Amazon API Gateway REST API as well as a custom domain name and enables mTLS. Further, it creates a Cognito User Pool. The Cognito User Pool is used to issue certificate-bound access tokens. The REST API makes use of an authorizer to compare the "cnf" claim in the access token to the fingerprint of the client certificate sent as part of the mutual authentication TLS handshake. +This pattern creates an Amazon API Gateway REST API with a custom domain name and enables mTLS. Further, it creates a Cognito User Pool. The Cognito User Pool is used to issue certificate-bound access tokens. The REST API makes use of an authorizer to compare the "cnf" claim in the access token to the fingerprint of the client certificate sent as part of the mutual authentication TLS handshake. ## Testing @@ -67,6 +67,41 @@ openssl x509 -in client-cert.crt -noout -fingerprint -sha256 | cut -d'=' -f2 | t 5. [Get an access token](https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-public-server-side.html) from Cognito. +Example of getting an access token using boto3: +``` +class CognitoAuth(AuthBase): + def __init__(self, user_pool_id, client_id, username, password): + self.user_pool_id = user_pool_id + self.client_id = client_id + self.username = username + self.password = password + self.token = self.authenticate_user() + + def authenticate_user(self): + client = boto3.client('cognito-idp', region_name='us-east-1') + try: + response = client.admin_initiate_auth( + UserPoolId=self.user_pool_id, + ClientId=self.client_id, + AuthFlow='ADMIN_USER_PASSWORD_AUTH', + AuthParameters={ + 'USERNAME': self.username, + 'PASSWORD': self.password, + } + ) + return response['AuthenticationResult']['AccessToken'] + except client.exceptions.NotAuthorizedException: + raise Exception("The username or password is incorrect") + except Exception as e: + raise Exception(f"An error occurred: {str(e)}") + + def __call__(self, r): + r.headers['Authorization'] = f'Bearer {self.token}' + return r +``` + +Notes that this requires the `ADMIN_USER_PASSWORD_AUTH` auth flow which is not enabled by default by this solution. You will need to do it on the console. + 6. Test the solution: ``` @@ -77,7 +112,7 @@ curl -v https:///example --cert client-cert.crt --key client 1. Delete the stack ```bash - aws cloudformation delete-stack --stack-name STACK_NAME + sam delete ``` 1. Confirm the stack has been deleted ```bash From a915b61d39b40052d1aa9f5244aeba0edfc8daa4 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 24 Jan 2025 10:40:28 +0200 Subject: [PATCH 3/7] Rename MakeFile to Makefile --- .../{MakeFile => Makefile} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename apigw-cognito-certificate-bound-access-token/{MakeFile => Makefile} (91%) diff --git a/apigw-cognito-certificate-bound-access-token/MakeFile b/apigw-cognito-certificate-bound-access-token/Makefile similarity index 91% rename from apigw-cognito-certificate-bound-access-token/MakeFile rename to apigw-cognito-certificate-bound-access-token/Makefile index 031b88e42..26d3f09c6 100644 --- a/apigw-cognito-certificate-bound-access-token/MakeFile +++ b/apigw-cognito-certificate-bound-access-token/Makefile @@ -8,4 +8,4 @@ build-CombinedLayer: pip install cryptography -t "$(ARTIFACTS_DIR)/python" clean: - rm -rf "$(ARTIFACTS_DIR)" \ No newline at end of file + rm -rf "$(ARTIFACTS_DIR)" From 3f0f79bcc1e4756ea9a240b5fc2f2bf6390be3f4 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 27 Jan 2025 12:56:09 +0200 Subject: [PATCH 4/7] added AWSCLI command for getting access token --- .../README.md | 68 ++++++------------- 1 file changed, 22 insertions(+), 46 deletions(-) diff --git a/apigw-cognito-certificate-bound-access-token/README.md b/apigw-cognito-certificate-bound-access-token/README.md index 90b0ae287..177d7cb73 100644 --- a/apigw-cognito-certificate-bound-access-token/README.md +++ b/apigw-cognito-certificate-bound-access-token/README.md @@ -14,6 +14,8 @@ Important: this application uses various AWS services and there are costs associ * A certificate issued under the domain you own. * Create client certificate as per the [mTLS blogpost](https://aws.amazon.com/blogs/compute/introducing-mutual-tls-authentication-for-amazon-api-gateway/). +Place the trust store in the S3 bucket that you will specfiy when deploying in the solution. + ## Deployment Instructions 1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: @@ -26,28 +28,23 @@ git clone https://github.com/aws-samples/serverless-patterns cd apigw-cognito-certificate-bound-access-token ``` -3. Ensure that you add the relevant parameters to `samconfig.toml`: -* stack_name -* s3_bucket -* s3_prefix -* region -* parameter_overrides. - * BucketName - to store client certificate and trust store. - * CACertKey - trust store. - * ClientCertKey - client certificate Amazon S3 object key. - * CustomDomainCertArn - custom domain AWS Certificate Manager certficate ARN. - * CustomDomainName - custom domain name for API Gateway. Must match CustomDomainCertArn. - -4. Build the solution: +3. Build the solution: ``` sam build --use-container ``` -5. Deploy the solution: +3. Deploy the solution: ``` -sam deploy +sam deploy --guided ``` +Parameter_overrides: + * BucketName - to store client certificate and trust store. + * CACertKey - trust store. + * ClientCertKey - client certificate Amazon S3 object key. + * CustomDomainCertArn - custom domain AWS Certificate Manager certficate ARN. + * CustomDomainName - custom domain name for API Gateway. Must match CustomDomainCertArn. + ## How it works This pattern creates an Amazon API Gateway REST API with a custom domain name and enables mTLS. Further, it creates a Cognito User Pool. The Cognito User Pool is used to issue certificate-bound access tokens. The REST API makes use of an authorizer to compare the "cnf" claim in the access token to the fingerprint of the client certificate sent as part of the mutual authentication TLS handshake. @@ -67,40 +64,19 @@ openssl x509 -in client-cert.crt -noout -fingerprint -sha256 | cut -d'=' -f2 | t 5. [Get an access token](https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flows-public-server-side.html) from Cognito. -Example of getting an access token using boto3: +Example of getting an access token using the AWSCLI: ``` -class CognitoAuth(AuthBase): - def __init__(self, user_pool_id, client_id, username, password): - self.user_pool_id = user_pool_id - self.client_id = client_id - self.username = username - self.password = password - self.token = self.authenticate_user() - - def authenticate_user(self): - client = boto3.client('cognito-idp', region_name='us-east-1') - try: - response = client.admin_initiate_auth( - UserPoolId=self.user_pool_id, - ClientId=self.client_id, - AuthFlow='ADMIN_USER_PASSWORD_AUTH', - AuthParameters={ - 'USERNAME': self.username, - 'PASSWORD': self.password, - } - ) - return response['AuthenticationResult']['AccessToken'] - except client.exceptions.NotAuthorizedException: - raise Exception("The username or password is incorrect") - except Exception as e: - raise Exception(f"An error occurred: {str(e)}") - - def __call__(self, r): - r.headers['Authorization'] = f'Bearer {self.token}' - return r +aws cognito-idp admin-initiate-auth \ + --user-pool-id YOUR_USER_POOL_ID \ + --client-id YOUR_CLIENT_ID \ + --auth-flow ADMIN_USER_PASSWORD_AUTH \ + --auth-parameters 'USERNAME=YOUR_USERNAME,PASSWORD=YOUR_PASSWORD' \ + --region us-east-1 \ + --query 'AuthenticationResult.AccessToken' \ + --output text ``` -Notes that this requires the `ADMIN_USER_PASSWORD_AUTH` auth flow which is not enabled by default by this solution. You will need to do it on the console. +Notes that this requires the `ADMIN_USER_PASSWORD_AUTH` auth flow which is not enabled by default by this solution. You will need to do it on the console. This is only for testing purposes. 6. Test the solution: From 323d0c49e7ce95a45b781a60ef5741c7cd616980 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 27 Jan 2025 12:58:11 +0200 Subject: [PATCH 5/7] updated output information in README.md --- apigw-cognito-certificate-bound-access-token/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apigw-cognito-certificate-bound-access-token/README.md b/apigw-cognito-certificate-bound-access-token/README.md index 177d7cb73..8f4e3b078 100644 --- a/apigw-cognito-certificate-bound-access-token/README.md +++ b/apigw-cognito-certificate-bound-access-token/README.md @@ -84,6 +84,11 @@ Notes that this requires the `ADMIN_USER_PASSWORD_AUTH` auth flow which is not e curl -v https:///example --cert client-cert.crt --key client-cert.key -H "Authorization: " ``` +You should see output as follows: +``` +{"message": "Hello from the example function!", "event": {"resource": "/example", "path": "/example"... +``` + ## Cleanup 1. Delete the stack From e0a2cec20122dd499101bdb29f8ac1f86c4b4c57 Mon Sep 17 00:00:00 2001 From: ellisms <114107920+ellisms@users.noreply.github.com> Date: Mon, 27 Jan 2025 08:40:38 -0500 Subject: [PATCH 6/7] publishing file --- ...apigw-cognito-cert-bound-access-token.json | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 apigw-cognito-certificate-bound-access-token/apigw-cognito-cert-bound-access-token.json diff --git a/apigw-cognito-certificate-bound-access-token/apigw-cognito-cert-bound-access-token.json b/apigw-cognito-certificate-bound-access-token/apigw-cognito-cert-bound-access-token.json new file mode 100644 index 000000000..a2006e60b --- /dev/null +++ b/apigw-cognito-certificate-bound-access-token/apigw-cognito-cert-bound-access-token.json @@ -0,0 +1,87 @@ +{ + "title": "Certificate-Bound Access Tokens using Amazon API Gateway and Amazon Cognito", + "description": "Implement certificate-bound access tokens for custom domain with API Gateway and Cognito user pools", + "language": "Python", + "level": "300", + "framework": "SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern creates an Amazon API Gateway REST API and enables mTLS for a custom domain.", + "Further, it creates a Cognito User Pool, which issues the certificate-bound access tokens.", + "The REST API makes use of an authorizer to compare the 'cnf' claim in the access token to the fingerprint of the client certificate sent as part of the mutual authentication TLS handshake" + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-cognito-certificate-bound-access-token", + "templateURL": "serverless-patterns/apigw-cognito-certificate-bound-access-token", + "projectFolder": "apigw-cognito-certificate-bound-access-token", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "API Gateway mTLS", + "link": "https://aws.amazon.com/blogs/compute/introducing-mutual-tls-authentication-for-amazon-api-gateway/" + }, + { + "text": "OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens", + "link": "https://datatracker.ietf.org/doc/html/rfc8705" + } + ] + }, + "deploy": { + "text": [ + "sam deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: sam delete." + ] + }, + "authors": [ + { + "name": "Kevin Draai", + "image": "https://media.licdn.com/dms/image/v2/C4D03AQFaayZVs_v_bA/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1556110763035?e=1740009600&v=beta&t=fJLsUYmOsXYgvp0m7oE_eUkPQ_V-WNrQsSJggGdf3d0", + "bio": "Senior Cloud Support Engineer", + "linkedin": "kevin-draai-30587780" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "route53", + "label": "Route53 Custom Domain" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "apigw", + "label": "API Gateway REST API" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "line1": { + "from": "icon2", + "to": "icon3", + "label": "mTLS" + }, + "line2": { + "from": "icon1", + "to": "icon2" + } + } +} From 41fcf7d8a728415bf5a94b937cdb21290586e8f8 Mon Sep 17 00:00:00 2001 From: ellisms <114107920+ellisms@users.noreply.github.com> Date: Mon, 27 Jan 2025 08:41:54 -0500 Subject: [PATCH 7/7] tagging --- apigw-cognito-certificate-bound-access-token/template.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apigw-cognito-certificate-bound-access-token/template.yaml b/apigw-cognito-certificate-bound-access-token/template.yaml index 1d3443e7b..82232b3c9 100644 --- a/apigw-cognito-certificate-bound-access-token/template.yaml +++ b/apigw-cognito-certificate-bound-access-token/template.yaml @@ -8,7 +8,7 @@ Description: > - A Lambda-based custom authorizer for API requests - A Cognito User Pool with a pre-token generation Lambda trigger to bind the certificate to the access token - The infrastructure implements Certificate-Bound Access Tokens within AWS. + The infrastructure implements Certificate-Bound Access Tokens within AWS. (uksb-1tthgi812) (tag:apigw-cognito-certificate-bound-access-token) # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: @@ -185,4 +185,4 @@ Outputs: UserPoolClientId: Description: "ID of the Cognito User Pool Client" - Value: !Ref CognitoUserPoolClient \ No newline at end of file + Value: !Ref CognitoUserPoolClient