├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── custom-auth ├── lambda.py └── requirements.txt ├── docs └── architecture_reference.png ├── helper.sh ├── infrastructure ├── stack-no-auth.template └── stack-with-auth.template └── pets-clinic-api ├── lambda.py └── requirements.txt /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unify Identity Providers with Amazon Verified Permissions and an Amazon API Gateway Lambda authorizer 2 | 3 | In many cases, enterprises need to build a unified access control layer to their APIs that allow access from multiple sources (for example multiple identity providers or different user groups and categories). One common use case would be an API exposed to different tenants through API Gateway, which can be supported by an Amazon Cognito multi-tenancy solution. For example, two Amazon Cognito User-pools could be created for an application, one for patients and another for healthcare providers with a common API that is used to provide access. 4 | 5 | In addition to Amazon Cognito, developers can use Amazon API Gateway to manage, and expose APIs to provide access to back end resources. API Gateway is a fully managed service that allows developers to build APIs that acts as an entry point for applications and allows access to data, or functionality running on EC2, ECS, Lambda or any web application. To integrate with multiple IDPs, an AWS Lambda authorizer is used to control access to the API with Amazon Cognito as an identity provider. The Lambda authorizer enables customization of API authorization, and in this case allows multiple IDPs to be leveraged. 6 | 7 | [Amazon Verified Permissions](https://aws.amazon.com/verified-permissions/)is a standalone service used to implement fine-grained authorization on resources in an application. Amazon Cognito is used to provide the authentication function for users prior to being authorized by Amazon Verified Permissions. All four services are leveraged together to combine multi IDPs into a centralized application access solution. The solution isn't limited to the Amazon Cognito IDP, any third party identity provider that generates JWTs can be used. 8 | 9 | ## Solution Overview 10 | 11 | In this blog, we describe an architectural design that showcases how to use multiple Amazon Cognito User Pools, Amazon Verified Permissions, and a Lambda authorizer for API Gateway backed APIs. The solution is based on user-pool multi-tenancy for user authentication. It leverages Amazon Cognito user pools to assign authenticated users a set of temporary and least privilege credentials for application access. Once users are authenticated, they are authorized to access APIs through a lambda function. This function interfaces with Amazon Verified Permissions to apply the appropriate access policy based on user attributes. 12 | 13 | ### Architecture reference (Request Flow): 14 | 15 | The following figure shows the basic architecture and information flow for user requests. 16 | 17 | ![arch](./docs/architecture_reference.png) 18 | 19 | **Figure 1 – User request flow** 20 | 21 | Let’s go through the request flow to understand what happens at each step, as shown in Figure 1: 22 | 23 | 1. There two groups of users – external (Clients) and Internal (Veterinarians). 24 | These user groups authenticate against an IDP (Cognito in this case). 25 | 2. The groups attempt to access an API through API gateway along with their JWT token and client ID. 26 | 3. The Lambda authorizer will validate the claims. 27 | **Note:** If Amazon Cognito is the IDP then IsAuthorizedWithToken can be used to simplify the step. 28 | 4. Once the token is validated, the Lambda Authorizer will make a query to Amazon Verified Permissions with associated policy information to check the request. 29 | 5. Based on the group info API Gateway will evaluate the policy to the appropriate API Resource. 30 | 31 | `Note`: To further optimize Lambda authorizer, the authorization decision can be cached or disabled, depending on your needs. By enabling cache, you could improve the performance as the authorization policy will be returned from the cache whenever there is a cache key match. To learn more, see [Configure a Lambda authorizer using the API Gateway console](https://docs.aws.amazon.com/apigateway/latest/developerguide/configure-api-gateway-lambda-authorization-with-console.html). 32 | 33 | ## Getting Started 34 | 35 | ### Prerequisites 36 | 37 | For this solution, you need the following prerequisites: 38 | 39 | * The [AWS Command Line Interface (CLI)](https://aws.amazon.com/cli/) installed and [configured for use](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). 40 | * Python 3.9 or later, to package Python code for Lambda 41 | `Note`: We recommend that you use a [virtual environment](https://docs.python.org/3.9/library/venv.html) or [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/) to isolate the solution from the rest of your Python environment. 42 | * An IAM role or user with enough permissions to create Amazon Cognito User Pool, IAM Role, Lambda, IAM Policy, and API Gateway. 43 | * jq for JSON processing in bash script. 44 | Install on Ubuntu/Debian: 45 | 46 | ```bash 47 | sudo apt-get install jq 48 | ``` 49 | 50 | Install on Mac with Homebrew: 51 | 52 | ```bash 53 | brew install jq 54 | ``` 55 | 56 | * The GitHub repository for the solution. You can [download it](https://github.com/aws-samples/amazon-cognito-avp-apigateway/archive/refs/heads/main.zip), or you can use the following [Git](https://git-scm.com/) command to download it from your terminal. 57 | `Note`: This sample code should be used to test out the solution and is not intended to be used in production account. 58 | 59 | ```bash 60 | $ git clone https://github.com/aws-samples/amazon-cognito-avp-apigateway.git 61 | $ cd amazon-cognito-avp-apigateway 62 | ``` 63 | 64 | To implement this reference architecture, you will be utilizing the following services: 65 | 66 | * [Amazon Verified Permissions](https://aws.amazon.com/verified-permissions/) is a new service that helps you implement and enforce fine-grained authorization on resources within the applications that you build and deploy, such as HR systems and banking applications. 67 | * [Amazon API Gateway](https://aws.amazon.com/api-gateway/) is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. APIs act as the "front door" for applications to access data, business logic, or functionality from your backend services. Using API Gateway, you can create RESTful APIs and WebSocket APIs that enable real-time two-way communication applications. API Gateway supports containerized and serverless workloads, as well as web applications. 68 | * [AWS Lambda](https://aws.amazon.com/lambda/) is a serverless compute service that lets you run code without provisioning or managing servers, creating workload-aware cluster scaling logic, maintaining event integrations, or managing runtimes. With Lambda, you can run code for virtually any type of application or backend service - all with zero administration 69 | * [Amazon Cognito](https://aws.amazon.com/cognito/) provides an identity store that scales to millions of users, supports social and enterprise identity federation, and offers advanced security features to protect your consumers and business. Built on open identity standards, Amazon Cognito supports various compliance regulations and integrates with frontend and backend development resources. 70 | 71 | 72 | `Note`: This solution was tested in the us-east-1 region. Before selecting a region, verify that the necessary [services—Amazon Verified Permissions, Amazon Cognito, API Gateway, and Lambda—are available in those Regions](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/). 73 | 74 | ### Deploy the sample solution 75 | 76 | From within the directory where you downloaded the sample code from GitHub, first run the following command to package the lambda functions. Then run the next command to generate a random Amazon Cognito user password and create the resources described in the previous section. 77 | 78 | `Note`: In this case you are generating a random user password for demo purposes. Follow best practices for user passwords in production implementations. 79 | 80 | ```bash 81 | $ bash ./helper.sh package-lambda-functions 82 | … 83 | Successfully completed packaging files. 84 | ``` 85 | 86 | ```bash 87 | $ bash ./helper.sh cf-create-stack-gen-password 88 | … 89 | Successfully created CloudFormation stack. 90 | ``` 91 | 92 | #### Validate Amazon Cognito user creation 93 | 94 | To validate that an Amazon Cognito user has been created successfully, run the following command to open the Amazon Cognito UI in your browser and then log in with your credentials. 95 | 96 | `Note`: When you run this command, it returns the user name and password that you should use to log in. 97 | 98 | For Internal User Pool Domain Users 99 | 100 | ```bash 101 | $ bash ./helper.sh open-cognito-internal-domain-ui 102 | Opening Cognito UI... 103 | URL: xxxxxxxxx 104 | Please use following credentials to login: 105 | Username: cognitouser 106 | Password: xxxxxxxx 107 | ``` 108 | 109 | For External User Pool Domain Users 110 | 111 | ```bash 112 | $ bash ./helper.sh open-cognito-external-domain-ui 113 | Opening Cognito UI... 114 | URL: xxxxxxxxx 115 | Please use following credentials to login: 116 | Username: cognitouser 117 | Password: xxxxxxxx 118 | ``` 119 | 120 | #### Validate Amazon Cognito JWT upon login 121 | 122 | Since we haven’t installed a web application that would respond to the redirect request, Amazon Cognito will redirect to localhost, which might look like an error. The key aspect is that after a successful log in, there is a URL similar to the following in the navigation bar of your browser: 123 | 124 | ```bash 125 | http://localhost/#id_token=eyJraWQiOiJicVhMYWFlaTl4aUhzTnY3W... 126 | ``` 127 | 128 | ### Test the API configuration 129 | 130 | Before you protect the API with Amazon Cognito so that only authorized users can access it, let’s verify that the configuration is correct and the API is served by API Gateway. The following command makes a curl request to API Gateway to retrieve data from the API service. 131 | 132 | ```bash 133 | $ bash ./helper.sh curl-api 134 | API to check his appointment details of PI-T123 135 | URL: https://epgst74zff.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T123 136 | Response: 137 | {"appointment": {"id": "PI-T123", "name": "Dave", "Pet": "Onyx - Dog. 2y 3m", "Phone Number": "+1234567", "Visit History": "Patient History from last visit with primary vet", "Assigned Veterinarian": "Jane"}} 138 | 139 | API to check his appointment details of PI-T124 140 | URL: https://epgst74zff.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T124 141 | Response: 142 | {"appointment": {"id": "PI-T124", "name": "Joy", "Pet": "Jelly - Dog. 6y 2m", "Phone Number": "+1368728", "Visit History": "None", "Assigned Veterinarian": "Jane"}} 143 | 144 | API to check his appointment details of PI-T125 145 | URL: https://epgst74zff.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T125 146 | Response: 147 | {"appointment": {"id": "PI-T125", "name": "Dave", "Pet": "Sassy - Cat. 1y", "Phone Number": "+1398777", "Visit History": "Patient History from last visit with primary vet", "Assigned Veterinarian": "Adam"}} 148 | ``` 149 | 150 | ### Protect the API 151 | 152 | To protect your API, the following is required: 153 | 154 | 1. Amazon Verified Permissions to store the policy for user authorization. 155 | 2. A Lambda function to verify the user’s access token and authorize the user via Amazon Verified Permissions. 156 | 157 | #### Update and create resources 158 | 159 | Run the following command to update existing resources and create a Lambda authorizer and Amazon Verified Permissions policy store. 160 | 161 | ```bash 162 | $ bash ./helper.sh cf-update-stack 163 | Successfully updated CloudFormation stack. 164 | ``` 165 | 166 | ### Test the custom authorizer setup 167 | 168 | Begin your testing with the following request, which doesn’t include an access token. 169 | 170 | `Note`: Wait for a few minutes to allow API Gateway to deploy prior to running the commands below. 171 | 172 | ```bash 173 | $ bash ./helper.sh curl-api 174 | API to check his appointment details of PI-T123 175 | URL: https://epgst74zff.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T123 176 | Response: 177 | {"message":"Unauthorized"} 178 | 179 | API to check his appointment details of PI-T124 180 | URL: https://epgst74zff.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T124 181 | Response: 182 | {"message":"Unauthorized"} 183 | 184 | API to check his appointment details of PI-T125 185 | URL: https://epgst74zff.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T125 186 | Response: 187 | {"message":"Unauthorized"} 188 | ``` 189 | 190 | The request is denied with the message **Unauthorized**. At this point, the Amazon API Gateway expects a header named Authorization (case sensitive) in the request. If there’s no authorization header, the request is denied before it reaches the lambda authorizer. This is a way to filter out requests that don’t include required information. 191 | 192 | Use the following command for the next test. In this test, you pass the required header but the token is invalid because it wasn’t issued by Amazon Cognito but is a simple JWT-format token stored in ./helper.sh. To learn more about how to decode and validate a JWT, see [decode and verify an Amazon Cognito JSON token](https://aws.amazon.com/premiumsupport/knowledge-center/decode-verify-cognito-json-token/). 193 | 194 | ```bash 195 | $ bash ./helper.sh curl-api-invalid-token 196 | {"Message":"User is not authorized to access this resource"} 197 | ``` 198 | 199 | This time the message is different. The Lambda authorizer received the request and identified the token as invalid and responded with the message **User is not authorized to access this resource**. 200 | 201 | To make a successful request to the protected API, your code will need to perform the following steps: 202 | 203 | 1. Use a user name and password to authenticate against your Amazon Cognito user pool. 204 | 2. Acquire the tokens (id token, access token, and refresh token). 205 | 3. Make an HTTPS (TLS) request to API Gateway and pass the access token in the headers. 206 | 207 | Finally, let’s programmatically log in to Amazon Cognito UI, acquire a valid access token, and make a request to API Gateway. Run the following command to call the protected API. 208 | 209 | ```bash 210 | $ ./helper.sh curl-protected-internal-user-api 211 | 212 | Getting API URL, Cognito Usernames, Cognito Users Password and Cognito ClientId... 213 | User: Jane 214 | Password: Pa%%word-2023-04-17-17-11-32 215 | Resource: PI-T123 216 | URL: https://16qyz501mg.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T123 217 | 218 | Authenticating to get access_token... 219 | Access Token: eyJraWQiOiJIaVRvckxxxxxxxxxx6BfCBKASA 220 | 221 | Response: 222 | {"appointment": {"id": "PI-T123", "name": "Dave", "Pet": "Onyx - Dog. 2y 3m", "Phone Number": "+1234567", "Visit History": "Patient History from last visit with primary vet", "Assigned Veterinarian": "Jane"}} 223 | 224 | User: Adam 225 | Password: Pa%%word-2023-04-17-17-11-32 226 | Resource: PI-T123 227 | URL: https://16qyz501mg.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T123 228 | 229 | Authenticating to get access_token... 230 | Access Token: eyJraWQiOiJIaVRvckxxxxxxxxxx6BfCBKASA 231 | 232 | Response: 233 | {"Message":"User is not authorized to access this resource"} 234 | 235 | User: Adam 236 | Password: Pa%%word-2023-04-17-17-11-32 237 | Resource: PI-T125 238 | URL: https://16qyz501mg.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T125 239 | 240 | Authenticating to get access_token... 241 | Access Token: eyJraWQiOiJIaVRvckxxxxxxxxxx6BfCBKASA 242 | 243 | Response: 244 | {"appointment": {"id": "PI-T125", "name": "Dave", "Pet": "Sassy - Cat. 1y", "Phone Number": "+1398777", "Visit History": "Patient History from last visit with primary vet", "Assigned Veterinarian": "Adam"}} 245 | ``` 246 | 247 | ```bash 248 | $ ./helper.sh curl-protected-external-user-api 249 | 250 | Now calling external userpool users for accessing request 251 | User: Dave 252 | Password: Pa%%word-2023-04-17-17-11-32 253 | Resource: PI-T123 254 | URL: https://16qyz501mg.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T123 255 | 256 | Authenticating to get access_token... 257 | Access Token: eyJraWQiOiJIaVRvckxxxxxxxxxx6BfCBKASA 258 | 259 | Response: 260 | {"appointment": {"id": "PI-T123", "name": "Dave", "Pet": "Onyx - Dog. 2y 3m", "Phone Number": "+1234567", "Visit History": "Patient History from last visit with primary vet", "Assigned Veterinarian": "Jane"}} 261 | 262 | User: Joy 263 | Password Pa%%word-2023-04-17-17-11-32 264 | Resource: PI-T123 265 | URL: https://16qyz501mg.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T123 266 | 267 | Authenticating to get access_token... 268 | Access Token: eyJraWQiOiJIaVRvckxxxxxxxxxx6BfCBKASA 269 | 270 | Response: 271 | {"Message":"User is not authorized to access this resource"} 272 | 273 | User: Joy 274 | Password Pa%%word-2023-04-17-17-11-32 275 | Resource: PI-T124 276 | URL: https://16qyz501mg.execute-api.us-east-1.amazonaws.com/dev/appointment/PI-T124 277 | 278 | Authenticating to get access_token... 279 | Access Token: eyJraWQiOiJIaVRvckxxxxxxxxxx6BfCBKASA 280 | 281 | Response: 282 | {"appointment": {"id": "PI-T124", "name": "Joy", "Pet": "Jelly - Dog. 6y 2m", "Phone Number": "+1368728", "Visit History": "None", "Assigned Veterinarian": "Jane"}} 283 | ``` 284 | 285 | This time, you receive a response with data from the API service. Let’s examine the steps that the example code performed: 286 | 287 | 1. Lambda authorizer validates the access token. 288 | 2. Lambda authorizer leverages Amazon Verified Permissions to evaluate the user's requested actions against the policy store. 289 | 3. Lambda authorizer passes the IAM policy back to API Gateway. 290 | 4. API Gateway evaluates the IAM policy and the final effect is an allow. 291 | 5. API Gateway forwards the request to Lambda. 292 | 6. Lambda returns the response. 293 | 294 | In each of the tests, internal and external, a request was denied because the user was not authorized. This is due to the Amazon Verified Permissions policies. In the internal user pool, Veterinarians are only allowed to see their own patients data. Similarly, in the external user pool, clients are only allowed to see their own data. See the policies statements below. 295 | 296 | Internal Policy 297 | ``` 298 | permit (principal in UserGroup::"AllVeterinarians", 299 | action == Action::"GET/appointment", 300 | resource in UserGroup::"AllVeterinarians") 301 | when { principal == resource.Veterinarian }; 302 | ``` 303 | 304 | External Policy 305 | ``` 306 | permit (principal in UserGroup::"AllClients", 307 | action == Action::"GET/appointment", 308 | resource in UserGroup::"AllClients") 309 | when { principal == resource.owner}; 310 | ``` 311 | 312 | Run the following command to delete the deployed resources and clean up. 313 | 314 | ```bash 315 | $ bash ./helper.sh cf-delete-stack 316 | ``` 317 | 318 | ### Next Steps 319 | 320 | In this blog we demonstrated how multiple Amazon Cognito User Pools can be leveraged alongside Amazon Verified Permissions to build a single access layer to APIs. Amazon Cognito was used in this example, but another third party IDP could be leveraged. As a next step, explore the [Cedar playground](https://www.cedarpolicy.com/en) to test policies that can be used with Amazon Verified Permissions, or expand this solution by integrating a third party IDP. 321 | 322 | Please validate that this solution meets your company's security requirements prior to use. 323 | 324 | --- 325 | 326 | ## Security 327 | 328 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 329 | 330 | ## License 331 | 332 | This library is licensed under the MIT-0 License. See the LICENSE file. 333 | -------------------------------------------------------------------------------- /custom-auth/lambda.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import sys 4 | import time 5 | import urllib.request 6 | 7 | envLambdaTaskRoot = os.environ["LAMBDA_TASK_ROOT"] 8 | sys.path.insert(0,os.environ["LAMBDA_TASK_ROOT"]+"/dependencies") 9 | 10 | import boto3 11 | from jose import jwk, jwt 12 | from jose.utils import base64url_decode 13 | 14 | # envs 15 | AWS_REGION = os.environ['AWS_REGION'] 16 | COGNITO_INTERNAL_USER_POOL_ID = os.environ['COGNITO_INTERNAL_USER_POOL_ID'] 17 | COGNITO_INTERNAL_APP_CLIENT_ID = os.environ['COGNITO_INTERNAL_APP_CLIENT_ID'] 18 | COGNITO_EXTERNAL_USER_POOL_ID = os.environ['COGNITO_EXTERNAL_USER_POOL_ID'] 19 | COGNITO_EXTERNAL_APP_CLIENT_ID = os.environ['COGNITO_EXTERNAL_APP_CLIENT_ID'] 20 | VERIFIED_PERMISSION_POLICY_STORE_ID = os.environ['VERIFIED_PERMISSION_POLICY_STORE_ID'] 21 | 22 | keys_url1 = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(AWS_REGION, COGNITO_INTERNAL_USER_POOL_ID) 23 | keys_url2 = 'https://cognito-idp.{}.amazonaws.com/{}/.well-known/jwks.json'.format(AWS_REGION, COGNITO_EXTERNAL_USER_POOL_ID) 24 | # instead of re-downloading the public keys every time 25 | # we download them only on cold start 26 | # https://aws.amazon.com/blogs/compute/container-reuse-in-lambda/ 27 | 28 | # Excluding [B310] Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected. 29 | # URLs are verified above 30 | keys = {} 31 | with urllib.request.urlopen(keys_url1) as f: # nosec B310 32 | response = f.read() 33 | keys['internal'] = json.loads(response.decode('utf-8'))['keys'] 34 | with urllib.request.urlopen(keys_url2) as f: # nosec B310 35 | response = f.read() 36 | keys['external'] = json.loads(response.decode('utf-8'))['keys'] 37 | 38 | 39 | def handler(event, context): 40 | print('Event: ', event) 41 | 42 | token_data = parse_token_data(event) 43 | client_type_data = parse_client_type(event) 44 | 45 | if token_data['valid'] is False or client_type_data['valid'] is False: 46 | return get_deny_policy() 47 | 48 | try: 49 | client_keys = keys.get(client_type_data['client_type']) 50 | claims = verify_token(token_data['token'], client_keys) 51 | 52 | role = claims['custom:Role'] 53 | username = claims['cognito:username'] 54 | method_arn = event['methodArn'] 55 | return is_authorized(role, username, method_arn) 56 | 57 | except Exception as e: 58 | print(e) 59 | 60 | return get_deny_policy() 61 | 62 | def is_authorized(role: str, username:str, method_arn): 63 | authorization_service = boto3.client('verifiedpermissions') 64 | """authorization decision 65 | 1. build authorization query 66 | 2. call isAuthorized and get decision 67 | 3. based on decision, return an explicit allow or deny policy 68 | """ 69 | items = method_arn.rsplit("/") 70 | print('items ', items) 71 | 72 | action = "{}/{}".format(items[2], items[3]) 73 | if len(items) > 4: 74 | appointmentId = items[4] 75 | else: 76 | print('Appointment id is missing in URI') 77 | return get_deny_policy() 78 | 79 | authorizationQuery = { 80 | "policyStoreId": VERIFIED_PERMISSION_POLICY_STORE_ID, 81 | "principal":{ 82 | "entityType": "username", 83 | "entityId": username 84 | }, 85 | "action":{ 86 | "actionType":"Action", 87 | "actionId": action 88 | }, 89 | "resource": { 90 | "entityType": "appointmentId", 91 | "entityId": items[4] 92 | }, 93 | "entities": get_entities() 94 | } 95 | # print('authorizationQuery: ', authorizationQuery) 96 | authZResult = authorization_service.is_authorized( **authorizationQuery ) 97 | print("Authorization Decision:" + authZResult.get("decision")) 98 | 99 | policy = { 100 | 'Version': "2012-10-17", 101 | 'Statement': {} 102 | } 103 | if authZResult.get("decision") == "ALLOW": 104 | policy['Statement']['Action'] = 'execute-api:Invoke' 105 | policy['Statement']['Effect'] = authZResult.get("decision") 106 | policy['Statement']['Resource'] = method_arn 107 | return get_response_object(policy) 108 | return get_deny_policy() 109 | 110 | 111 | """----------------build entities structure---------------------""" 112 | def get_entities(): 113 | entities = { 114 | "entityList": [ 115 | { 116 | "identifier": { 117 | "entityType": "username", 118 | "entityId": "Dave" 119 | }, 120 | "parents": [ 121 | { 122 | "entityType": "UserGroup", 123 | "entityId": "AllClients" 124 | } 125 | ] 126 | }, 127 | { 128 | "identifier": { 129 | "entityType": "username", 130 | "entityId": "Joy" 131 | }, 132 | "parents": [ 133 | { 134 | "entityType": "UserGroup", 135 | "entityId": "AllClients" 136 | } 137 | ] 138 | }, 139 | { 140 | "identifier": { 141 | "entityType": "UserGroup", 142 | "entityId": "AllClients" 143 | } 144 | }, 145 | { 146 | "identifier": { 147 | "entityType": "username", 148 | "entityId": "Adam" 149 | }, 150 | "parents": [ 151 | { 152 | "entityType": "UserGroup", 153 | "entityId": "AllVeterinarians" 154 | } 155 | ] 156 | }, 157 | { 158 | "identifier": { 159 | "entityType": "username", 160 | "entityId": "Jane" 161 | }, 162 | "parents": [ 163 | { 164 | "entityType": "UserGroup", 165 | "entityId": "AllVeterinarians" 166 | } 167 | ] 168 | }, 169 | { 170 | "identifier": { 171 | "entityType": "UserGroup", 172 | "entityId": "AllClients" 173 | } 174 | }, 175 | { 176 | "identifier": { 177 | "entityType": "UserGroup", 178 | "entityId": "AllVeterinarians" 179 | } 180 | }, 181 | { 182 | "identifier": { 183 | "entityType": "appointmentId", 184 | "entityId": "PI-T123" 185 | }, 186 | "parents": [ 187 | { 188 | "entityType": "UserGroup", 189 | "entityId": "AllClients" 190 | }, 191 | { 192 | "entityType": "UserGroup", 193 | "entityId": "AllVeterinarians" 194 | } 195 | ], 196 | "attributes": { 197 | "owner": { 198 | "entityIdentifier": { 199 | "entityType": "username", 200 | "entityId": "Dave" 201 | } 202 | }, 203 | "Veterinarian": { 204 | "entityIdentifier": { 205 | "entityType": "username", 206 | "entityId": "Jane" 207 | } 208 | } 209 | } 210 | }, 211 | { 212 | "identifier": { 213 | "entityType": "appointmentId", 214 | "entityId": "PI-T124" 215 | }, 216 | "parents": [ 217 | { 218 | "entityType": "UserGroup", 219 | "entityId": "AllClients" 220 | }, 221 | { 222 | "entityType": "UserGroup", 223 | "entityId": "AllVeterinarians" 224 | } 225 | ], 226 | "attributes": { 227 | "owner": { 228 | "entityIdentifier": { 229 | "entityType": "username", 230 | "entityId": "Joy" 231 | } 232 | }, 233 | "Veterinarian": { 234 | "entityIdentifier": { 235 | "entityType": "username", 236 | "entityId": "Jane" 237 | } 238 | } 239 | } 240 | }, 241 | { 242 | "identifier": { 243 | "entityType": "appointmentId", 244 | "entityId": "PI-T125" 245 | }, 246 | "parents": [ 247 | { 248 | "entityType": "UserGroup", 249 | "entityId": "AllClients" 250 | }, 251 | { 252 | "entityType": "UserGroup", 253 | "entityId": "AllVeterinarians" 254 | } 255 | ], 256 | "attributes": { 257 | "owner": { 258 | "entityIdentifier": { 259 | "entityType": "username", 260 | "entityId": "Dave" 261 | } 262 | }, 263 | "Veterinarian": { 264 | "entityIdentifier": { 265 | "entityType": "username", 266 | "entityId": "Adam" 267 | } 268 | } 269 | } 270 | } 271 | ] 272 | } 273 | 274 | return entities 275 | 276 | 277 | def get_response_object(policyDocument, principalId='yyyyyyyy', context={}): 278 | response = { 279 | "principalId": principalId, 280 | "policyDocument": policyDocument, 281 | "context": context, 282 | "usageIdentifierKey": "{api-key}" 283 | } 284 | print('response: ', response) 285 | return response 286 | 287 | 288 | def get_deny_policy(): 289 | return { 290 | "principalId": "yyyyyyyy", 291 | "policyDocument": { 292 | "Version": "2012-10-17", 293 | "Statement": [ 294 | { 295 | "Action": "execute-api:Invoke", 296 | "Effect": "Deny", 297 | "Resource": "arn:aws:execute-api:*:*:*/ANY/*" 298 | } 299 | ] 300 | }, 301 | "context": {}, 302 | "usageIdentifierKey": "{api-key}" 303 | } 304 | 305 | def parse_client_type(event): 306 | response = {'valid': False} 307 | 308 | if 'clienttype' not in event['headers']: 309 | print('ClientType not present') 310 | return response 311 | 312 | client_type = event['headers']['clienttype'] 313 | print('client_type' , client_type) 314 | 315 | # deny request of header isn't made out of two strings, or 316 | # first string isn't equal to "Bearer" (enforcing following standards, 317 | # but technically could be anything or could be left out completely) 318 | if not keys.get(client_type): 319 | return response 320 | 321 | print('client_type: ', client_type) 322 | return { 323 | 'valid': True, 324 | 'client_type': client_type 325 | } 326 | 327 | 328 | def parse_token_data(event): 329 | response = {'valid': False} 330 | 331 | if 'Authorization' not in event['headers']: 332 | print('Authorization not present') 333 | return response 334 | 335 | auth_header = event['headers']['Authorization'] 336 | print('auth_header' , auth_header) 337 | auth_header_list = auth_header.split(' ') 338 | 339 | # deny request of header isn't made out of two strings, or 340 | # first string isn't equal to "Bearer" (enforcing following standards, 341 | # but technically could be anything or could be left out completely) 342 | if len(auth_header_list) != 2 or auth_header_list[0] != 'Bearer': 343 | return response 344 | 345 | access_token = auth_header_list[1] 346 | print('access_token: ', access_token) 347 | return { 348 | 'valid': True, 349 | 'token': access_token 350 | } 351 | 352 | 353 | def verify_token(token, client_keys): 354 | # get the kid from the headers prior to verification 355 | print('token', token) 356 | headers = jwt.get_unverified_headers(token) 357 | print('headers: ', headers) 358 | print('client_keys: ', client_keys) 359 | kid = headers['kid'] 360 | 361 | print('kid: ', kid) 362 | 363 | # search for the kid in the downloaded public keys 364 | key_index = -1 365 | for i in range(len(client_keys)): 366 | if kid == client_keys[i]['kid']: 367 | key_index = i 368 | break 369 | 370 | 371 | if key_index == -1: 372 | print('Public key not found in jwks.json') 373 | return False 374 | 375 | # construct the public key 376 | public_key = jwk.construct(client_keys[key_index]) 377 | 378 | # get the last two sections of the token, 379 | # message and signature (encoded in base64) 380 | message, encoded_signature = str(token).rsplit('.', 1) 381 | 382 | # decode the signature 383 | decoded_signature = base64url_decode(encoded_signature.encode('utf-8')) 384 | 385 | # verify the signature 386 | if not public_key.verify(message.encode("utf8"), decoded_signature): 387 | print('Signature verification failed') 388 | return False 389 | 390 | print('Signature successfully verified') 391 | 392 | # since we passed the verification, we can now safely 393 | # use the unverified claims 394 | claims = jwt.get_unverified_claims(token) 395 | 396 | # additionally we can verify the token expiration 397 | if time.time() > claims['exp']: 398 | print('Token is expired') 399 | return False 400 | 401 | # now we can use the claims 402 | print('claims: ', claims) 403 | return claims -------------------------------------------------------------------------------- /custom-auth/requirements.txt: -------------------------------------------------------------------------------- 1 | python-jose==3.2.0 2 | boto3==1.28.23 3 | botocore==1.31.23 -------------------------------------------------------------------------------- /docs/architecture_reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/multiple-identity-providers-amazon-api-gateway/4d00e56ac0773db5a2f10afd31b57f7ef3b4060c/docs/architecture_reference.png -------------------------------------------------------------------------------- /helper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CF_STACK_NAME="avp-stack" 4 | 5 | get_account_id() { 6 | ACCOUNT_ID=$(aws sts get-caller-identity \ 7 | --query 'Account' --output text) 8 | } 9 | 10 | get_stack_region() { 11 | STACK_REGION=$(aws configure get region) 12 | } 13 | 14 | get_cognito_users_password() { 15 | local command_output 16 | 17 | command_output=$(aws cloudformation describe-stacks \ 18 | --stack-name ${CF_STACK_NAME} \ 19 | --no-paginate | jq -r '.') 20 | 21 | COGNITO_USERS_PASSWORD=$(echo "$command_output" \ 22 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="UserPassword").ParameterValue') 23 | echo "Password: $COGNITO_USERS_PASSWORD" 24 | } 25 | 26 | get_cognito_username_and_password() { 27 | local command_output 28 | 29 | command_output=$(aws cloudformation describe-stacks \ 30 | --stack-name ${CF_STACK_NAME} \ 31 | --no-paginate | jq -r '.') 32 | 33 | COGNITO_USERS_PASSWORD=$(echo "$command_output" \ 34 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="UserPassword").ParameterValue') 35 | EXTERNAL_USER_NAME1=$(echo "$command_output" \ 36 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="ExternalUser1Name").ParameterValue') 37 | EXTERNAL_USER_NAME2=$(echo "$command_output" \ 38 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="ExternalUser2Name").ParameterValue') 39 | INTERNAL_USER_NAME1=$(echo "$command_output" \ 40 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="InternalUser1Name").ParameterValue') 41 | INTERNAL_USER_NAME2=$(echo "$command_output" \ 42 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="InternalUser2Name").ParameterValue') 43 | } 44 | 45 | get_api_url_cognitouser_cognitouserpass_cognitoclientid() { 46 | local command_output 47 | 48 | command_output=$(aws cloudformation describe-stacks \ 49 | --stack-name ${CF_STACK_NAME} \ 50 | --no-paginate | jq -r '.') 51 | 52 | COGNITO_USERS_PASSWORD=$(echo "$command_output" \ 53 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="UserPassword").ParameterValue') 54 | EXTERNAL_USER_NAME1=$(echo "$command_output" \ 55 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="ExternalUser1Name").ParameterValue') 56 | EXTERNAL_USER_NAME2=$(echo "$command_output" \ 57 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="ExternalUser2Name").ParameterValue') 58 | INTERNAL_USER_NAME1=$(echo "$command_output" \ 59 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="InternalUser1Name").ParameterValue') 60 | INTERNAL_USER_NAME2=$(echo "$command_output" \ 61 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="InternalUser2Name").ParameterValue') 62 | 63 | API_URL=$(echo "$command_output" \ 64 | | jq -r '.Stacks[0].Outputs[] | select(.OutputKey=="ApiGatewayDeploymentUrlApiEndpoint").OutputValue') 65 | INTERNAL_COGNITO_CLIENT_ID=$(echo "$command_output" \ 66 | | jq -r '.Stacks[0].Outputs[] | select(.OutputKey=="CognitoUserPoolClientInternal").OutputValue') 67 | EXTERNAL_COGNITO_CLIENT_ID=$(echo "$command_output" \ 68 | | jq -r '.Stacks[0].Outputs[] | select(.OutputKey=="CognitoUserPoolClientExternal").OutputValue') 69 | } 70 | 71 | get_api_url_v2_cognitouser_cognitouserpass_cognitoclientid() { 72 | local command_output 73 | 74 | command_output=$(aws cloudformation describe-stacks \ 75 | --stack-name ${CF_STACK_NAME} \ 76 | --no-paginate | jq -r '.') 77 | 78 | COGNITO_USERS_PASSWORD=$(echo "$command_output" \ 79 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="UserPassword").ParameterValue') 80 | EXTERNAL_USER_NAME1=$(echo "$command_output" \ 81 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="ExternalUser1Name").ParameterValue') 82 | EXTERNAL_USER_NAME2=$(echo "$command_output" \ 83 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="ExternalUser2Name").ParameterValue') 84 | INTERNAL_USER_NAME1=$(echo "$command_output" \ 85 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="InternalUser1Name").ParameterValue') 86 | INTERNAL_USER_NAME2=$(echo "$command_output" \ 87 | | jq -r '.Stacks[0].Parameters[] | select(.ParameterKey=="InternalUser2Name").ParameterValue') 88 | 89 | API_URL=$(echo "$command_output" \ 90 | | jq -r '.Stacks[0].Outputs[] | select(.OutputKey=="ApiGatewayDeploymentUrlApiEndpoint").OutputValue') 91 | INTERNAL_COGNITO_CLIENT_ID=$(echo "$command_output" \ 92 | | jq -r '.Stacks[0].Outputs[] | select(.OutputKey=="CognitoUserPoolClientInternal").OutputValue') 93 | EXTERNAL_COGNITO_CLIENT_ID=$(echo "$command_output" \ 94 | | jq -r '.Stacks[0].Outputs[] | select(.OutputKey=="CognitoUserPoolClientExternal").OutputValue') 95 | } 96 | 97 | get_api_url() { 98 | API_URL=$(aws cloudformation describe-stacks \ 99 | --stack-name ${CF_STACK_NAME} \ 100 | --query 'Stacks[0].Outputs[?OutputKey==`ApiGatewayDeploymentUrlApiEndpoint`].OutputValue' --output text) 101 | } 102 | 103 | get_login_payload_data() { 104 | 105 | DATA=$(cat< /dev/null 139 | else 140 | aws s3api create-bucket \ 141 | --bucket "${S3_BUCKET_NAME}" \ 142 | --region "${STACK_REGION}" \ 143 | --create-bucket-configuration LocationConstraint="${STACK_REGION}" > /dev/null 144 | fi 145 | 146 | aws s3 cp ./cf-lambdas/custom-auth.zip s3://"${S3_BUCKET_NAME}" 147 | aws s3 cp ./cf-lambdas/pets-clinic-api.zip s3://"${S3_BUCKET_NAME}" 148 | } 149 | 150 | delete_s3_bucket_for_lambdas() { 151 | get_account_id 152 | get_stack_region 153 | 154 | S3_BUCKET_NAME="${CF_STACK_NAME}-${ACCOUNT_ID}-${STACK_REGION}-lambdas" 155 | 156 | aws s3 rm s3://"${S3_BUCKET_NAME}/custom-auth.zip" 157 | aws s3 rm s3://"${S3_BUCKET_NAME}/pets-clinic-api.zip" 158 | 159 | aws s3api delete-bucket \ 160 | --bucket "${S3_BUCKET_NAME}" \ 161 | --region "${STACK_REGION}" > /dev/null 162 | } 163 | 164 | check_for_function_exit_code() { 165 | EXIT_CODE="$1" 166 | MSG="$2" 167 | 168 | if [[ "$?" == "${EXIT_CODE}" ]] 169 | then 170 | echo "${MSG}" 171 | else 172 | echo "Error occured. Please verify your configurations and try again." 173 | fi 174 | } 175 | 176 | for var in "$@" 177 | do 178 | case "$var" in 179 | cf-create-stack-gen-password) 180 | COGNITO_USER_PASS=Pa%%word-$(date +%F-%H-%M-%S) 181 | echo "Starting..." && echo "Generated password: ${COGNITO_USER_PASS}" 182 | COGNITO_USER_PASS="${COGNITO_USER_PASS}" bash ./helper.sh cf-create-stack 183 | ;; 184 | cf-create-stack-openssl-gen-password) 185 | COGNITO_USER_PASS=Pa%%word-$(openssl rand -hex 12) 186 | echo "" && echo "Generated password: ${COGNITO_USER_PASS}" 187 | COGNITO_USER_PASS="${COGNITO_USER_PASS}" bash ./helper.sh cf-create-stack 188 | ;; 189 | cf-create-stack) 190 | create_s3_bucket_for_lambdas 191 | 192 | echo "Creating CloudFormation Stack in region ${STACK_REGION}." 193 | STACK_ID=$(aws cloudformation create-stack \ 194 | --stack-name ${CF_STACK_NAME} \ 195 | --template-body file://infrastructure/stack-no-auth.template \ 196 | --parameters ParameterKey=UserPassword,ParameterValue=${COGNITO_USER_PASS} \ 197 | --capabilities CAPABILITY_NAMED_IAM \ 198 | --query 'StackId' --output text) 199 | 200 | aws cloudformation wait stack-create-complete \ 201 | --stack-name ${STACK_ID} 202 | 203 | check_for_function_exit_code "$?" "Successfully created CloudFormation stack." 204 | ;; 205 | cf-update-stack) 206 | get_cognito_users_password 207 | 208 | STACK_ID=$(aws cloudformation update-stack \ 209 | --stack-name ${CF_STACK_NAME} \ 210 | --template-body file://infrastructure/stack-with-auth.template \ 211 | --parameters ParameterKey=UserPassword,ParameterValue=${COGNITO_USERS_PASSWORD} \ 212 | --capabilities CAPABILITY_NAMED_IAM \ 213 | --query 'StackId' --output text) 214 | 215 | aws cloudformation wait stack-update-complete \ 216 | --stack-name ${STACK_ID} 217 | 218 | check_for_function_exit_code "$?" "Successfully updated CloudFormation stack." 219 | ;; 220 | cf-delete-stack) 221 | delete_s3_bucket_for_lambdas 222 | 223 | aws cloudformation delete-stack \ 224 | --stack-name ${CF_STACK_NAME} >> /dev/null 225 | 226 | echo "Deleting CloudFormation stack. If you want to wait for delete complition please run command below." 227 | echo "bash ./helper.sh cf-delete-stack-completed" 228 | ;; 229 | cf-delete-stack-completed) 230 | aws cloudformation wait stack-delete-complete \ 231 | --stack-name ${CF_STACK_NAME} 232 | 233 | check_for_function_exit_code "$?" "Successfully deleted CloudFormation stack." 234 | ;; 235 | open-cognito-internal-domain-ui) 236 | COGNITO_UI_URL=$(aws cloudformation describe-stacks \ 237 | --stack-name ${CF_STACK_NAME} \ 238 | --query 'Stacks[0].Outputs[?OutputKey==`CognitoHostedUiInternalDomainUrl`].OutputValue' --output text) 239 | 240 | get_cognito_username_and_password 241 | 242 | echo "Opening Cognito UI..." 243 | echo "URL: ${COGNITO_UI_URL}" 244 | echo "" 245 | echo "Please use following credentials to login and validate for any internal users:" 246 | echo "" 247 | echo "Username: ${INTERNAL_USER_NAME1}" 248 | echo "Password: ${COGNITO_USERS_PASSWORD}" 249 | echo "Username: ${INTERNAL_USER_NAME2}" 250 | echo "Password: ${COGNITO_USERS_PASSWORD}" 251 | 252 | # for visual effect for user to recognize msg above 253 | ./helper.sh visual 6 254 | 255 | open "${COGNITO_UI_URL}" 256 | ;; 257 | open-cognito-external-domain-ui) 258 | COGNITO_UI_URL=$(aws cloudformation describe-stacks \ 259 | --stack-name ${CF_STACK_NAME} \ 260 | --query 'Stacks[0].Outputs[?OutputKey==`CognitoHostedUiExternalDomainUrl`].OutputValue' --output text) 261 | 262 | get_cognito_username_and_password 263 | 264 | echo "Opening Cognito UI..." 265 | echo "URL: ${COGNITO_UI_URL}" 266 | echo "" 267 | echo "Please use following credentials to login and validate for any external users:" 268 | echo "" 269 | echo "Username: ${EXTERNAL_USER_NAME1}" 270 | echo "Password: ${COGNITO_USERS_PASSWORD}" 271 | echo "" 272 | echo "Username: ${EXTERNAL_USER_NAME2}" 273 | echo "Password: ${COGNITO_USERS_PASSWORD}" 274 | 275 | # for visual effect for user to recognize msg above 276 | ./helper.sh visual 6 277 | 278 | open "${COGNITO_UI_URL}" 279 | ;; 280 | curl-api) 281 | get_api_url 282 | APPOITMENT_API=$API_URL"/PI-T123" 283 | echo "" 284 | echo "API to check the appointment details of PI-T123" 285 | echo "URL: $APPOITMENT_API" 286 | echo "Response: " 287 | curl "${APPOITMENT_API}" 288 | echo "" 289 | 290 | APPOITMENT_API=$API_URL"/PI-T124" 291 | echo "" 292 | echo "API to check the appointment details of PI-T124" 293 | echo "URL: $APPOITMENT_API" 294 | echo "Response: " 295 | curl "${APPOITMENT_API}" 296 | echo "" 297 | 298 | APPOITMENT_API=$API_URL"/PI-T125" 299 | echo "" 300 | echo "API to check the appointment details of PI-T125" 301 | echo "URL: $APPOITMENT_API" 302 | echo "Response: " 303 | curl "${APPOITMENT_API}" 304 | echo "" 305 | ;; 306 | curl-api-invalid-token) 307 | get_api_url 308 | APPOITMENT_API=$API_URL"/PI-T123" 309 | echo "" 310 | echo "API to check the appointment details of PI-T123 with invalid token" 311 | curl -s -H "Authorization: Bearer aGVhZGVy.Y2xhaW1z.c2lnbmF0dXJl" "${APPOITMENT_API}" 312 | echo "" 313 | ;; 314 | curl-protected-external-user-api) 315 | echo "" 316 | echo "Now calling external userpool users for accessing request" 317 | get_api_url_cognitouser_cognitouserpass_cognitoclientid 318 | APPOITMENT_API=$API_URL"/PI-T123" 319 | echo "User: $EXTERNAL_USER_NAME1" 320 | echo "Password: $COGNITO_USERS_PASSWORD" 321 | echo "Resource: PI-T123" 322 | echo "URL: $APPOITMENT_API" 323 | get_login_payload_data $EXTERNAL_USER_NAME1 $COGNITO_USERS_PASSWORD $EXTERNAL_COGNITO_CLIENT_ID 324 | echo "" 325 | echo "Authenticating to get access_token..." 326 | get_access_token 327 | echo "" 328 | echo "Response: " 329 | curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "ClientType: external" "${APPOITMENT_API}" 330 | echo "" 331 | 332 | APPOITMENT_API=$API_URL"/PI-T123" 333 | echo "" 334 | echo "User: $EXTERNAL_USER_NAME2" 335 | echo "Password $COGNITO_USERS_PASSWORD" 336 | echo "Resource: PI-T123" 337 | echo "URL: $APPOITMENT_API" 338 | get_login_payload_data $EXTERNAL_USER_NAME2 $COGNITO_USERS_PASSWORD $EXTERNAL_COGNITO_CLIENT_ID 339 | echo "" 340 | echo "Authenticating to get access_token..." 341 | get_access_token 342 | echo "" 343 | echo "Response: " 344 | curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "ClientType: external" "${APPOITMENT_API}" 345 | echo "" 346 | 347 | APPOITMENT_API=$API_URL"/PI-T124" 348 | echo "" 349 | echo "User: $EXTERNAL_USER_NAME2" 350 | echo "Password $COGNITO_USERS_PASSWORD" 351 | echo "Resource: PI-T124" 352 | echo "URL: $APPOITMENT_API" 353 | get_login_payload_data $EXTERNAL_USER_NAME2 $COGNITO_USERS_PASSWORD $EXTERNAL_COGNITO_CLIENT_ID 354 | echo "" 355 | echo "Authenticating to get access_token..." 356 | get_access_token 357 | echo "" 358 | echo "Response: " 359 | curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "ClientType: external" "${APPOITMENT_API}" 360 | echo "" 361 | ;; 362 | curl-protected-internal-user-api) 363 | echo "" 364 | echo "Getting API URL, Cognito Usernames, Cognito Users Password and Cognito ClientId..." 365 | get_api_url_cognitouser_cognitouserpass_cognitoclientid 366 | APPOITMENT_API=$API_URL"/PI-T123" 367 | echo "User: $INTERNAL_USER_NAME1" 368 | echo "Password: $COGNITO_USERS_PASSWORD" 369 | echo "Resource: PI-T123" 370 | echo "URL: $APPOITMENT_API" 371 | get_login_payload_data $INTERNAL_USER_NAME1 $COGNITO_USERS_PASSWORD $INTERNAL_COGNITO_CLIENT_ID 372 | echo "" 373 | echo "Authenticating to get access_token..." 374 | get_access_token 375 | echo "" 376 | echo "Response: " 377 | curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "ClientType: internal" "${APPOITMENT_API}" 378 | echo "" 379 | 380 | echo "" 381 | APPOITMENT_API=$API_URL"/PI-T123" 382 | echo "User: $INTERNAL_USER_NAME2" 383 | echo "Password: $COGNITO_USERS_PASSWORD" 384 | echo "Resource: PI-T123" 385 | echo "URL: $APPOITMENT_API" 386 | get_login_payload_data $INTERNAL_USER_NAME2 $COGNITO_USERS_PASSWORD $INTERNAL_COGNITO_CLIENT_ID 387 | echo "" 388 | echo "Authenticating to get access_token..." 389 | get_access_token 390 | echo "" 391 | echo "Response: " 392 | curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "ClientType: internal" "${APPOITMENT_API}" 393 | echo "" 394 | 395 | echo "" 396 | APPOITMENT_API=$API_URL"/PI-T125" 397 | echo "User: $INTERNAL_USER_NAME2" 398 | echo "Password: $COGNITO_USERS_PASSWORD" 399 | echo "Resource: PI-T125" 400 | echo "URL: $APPOITMENT_API" 401 | get_login_payload_data $INTERNAL_USER_NAME2 $COGNITO_USERS_PASSWORD $INTERNAL_COGNITO_CLIENT_ID 402 | echo "" 403 | echo "Authenticating to get access_token..." 404 | get_access_token 405 | echo "" 406 | echo "Response: " 407 | curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" -H "ClientType: internal" "${APPOITMENT_API}" 408 | echo "" 409 | ;; 410 | curl-protected-api-not-allowed-endpoint) 411 | echo "Getting API URL, Cognito Username, Cognito Users Password and Cognito ClientId..." 412 | get_api_url_v2_cognitouser_cognitouserpass_cognitoclientid 413 | 414 | get_login_payload_data 415 | echo "Authenticating to get access_token..." 416 | get_access_token 417 | 418 | echo "Making api call..." 419 | curl -s -H "Authorization: Bearer ${ACCESS_TOKEN}" "${API_URL_V2}" 420 | echo "" 421 | ;; 422 | create-s3-bucket) 423 | create_s3_bucket_for_lambdas 424 | ;; 425 | delete-s3-bucket) 426 | delete_s3_bucket_for_lambdas 427 | ;; 428 | package-custom-auth) 429 | cd ./custom-auth 430 | pip3 install -r requirements.txt --target ./package/dependencies 431 | cd ./package 432 | zip -r ../custom-auth.zip . > /dev/null 433 | cd .. && zip -g custom-auth.zip lambda.py 434 | mv ./custom-auth.zip ../cf-lambdas 435 | rm -r ./package 436 | echo "Successfully completed packaging custom-auth." 437 | ;; 438 | package-pets-clinic-api) 439 | cd ./pets-clinic-api 440 | zip pets-clinic-api.zip lambda.py && mv pets-clinic-api.zip ../cf-lambdas 441 | echo "Successfully completed packaging pets-clinic-api." 442 | ;; 443 | package-lambda-functions) 444 | mkdir -p cf-lambdas 445 | bash ./helper.sh package-custom-auth 446 | bash ./helper.sh package-pets-clinic-api 447 | 448 | echo "Successfully completed packaging files." 449 | ;; 450 | visual) 451 | for ((i=1;i<=${2};i++)); 452 | do 453 | sleep 0.5 && echo -n "." 454 | done 455 | ;; 456 | *) 457 | ;; 458 | esac 459 | done 460 | -------------------------------------------------------------------------------- /infrastructure/stack-no-auth.template: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Resources: 3 | CognitoUserPoolInternal: 4 | Type: 'AWS::Cognito::UserPool' 5 | Properties: 6 | UserPoolName: !Sub ${AWS::StackName}-internal 7 | Schema: 8 | - AttributeDataType: String 9 | Name: Role 10 | CognitoUserPoolExternal: 11 | Type: 'AWS::Cognito::UserPool' 12 | Properties: 13 | UserPoolName: !Sub ${AWS::StackName}-external 14 | Schema: 15 | - AttributeDataType: String 16 | Name: Role 17 | CognitoUserPoolClientInternal: 18 | Type: 'AWS::Cognito::UserPoolClient' 19 | Properties: 20 | UserPoolId: !Ref CognitoUserPoolInternal 21 | ClientName: !Sub ${AWS::StackName}-internal 22 | AllowedOAuthFlows: 23 | - implicit 24 | AllowedOAuthFlowsUserPoolClient: true 25 | AllowedOAuthScopes: 26 | - email 27 | - openid 28 | - profile 29 | CallbackURLs: 30 | - 'http://localhost' 31 | GenerateSecret: false 32 | ExplicitAuthFlows: 33 | - ALLOW_USER_PASSWORD_AUTH 34 | - ALLOW_USER_SRP_AUTH 35 | - ALLOW_REFRESH_TOKEN_AUTH 36 | SupportedIdentityProviders: 37 | - COGNITO 38 | CognitoUserPoolClientExternal: 39 | Type: 'AWS::Cognito::UserPoolClient' 40 | Properties: 41 | ClientName: !Sub ${AWS::StackName}-external 42 | UserPoolId: !Ref CognitoUserPoolExternal 43 | AllowedOAuthFlows: 44 | - implicit 45 | AllowedOAuthFlowsUserPoolClient: true 46 | AllowedOAuthScopes: 47 | - email 48 | - openid 49 | - profile 50 | CallbackURLs: 51 | - 'http://localhost' 52 | GenerateSecret: false 53 | ExplicitAuthFlows: 54 | - ALLOW_USER_PASSWORD_AUTH 55 | - ALLOW_USER_SRP_AUTH 56 | - ALLOW_REFRESH_TOKEN_AUTH 57 | SupportedIdentityProviders: 58 | - COGNITO 59 | CognitoUserPoolDomainInternal: 60 | Type: 'AWS::Cognito::UserPoolDomain' 61 | Properties: 62 | # using client id will make the domain unique 63 | Domain: !Sub dns-name-${CognitoUserPoolClientInternal} 64 | UserPoolId: !Ref CognitoUserPoolInternal 65 | DependsOn: 66 | - CognitoUserPoolClientInternal 67 | CognitoUserPoolDomainExternal: 68 | Type: 'AWS::Cognito::UserPoolDomain' 69 | Properties: 70 | # using client id will make the domain unique 71 | Domain: !Sub dns-name-${CognitoUserPoolClientExternal} 72 | UserPoolId: !Ref CognitoUserPoolExternal 73 | DependsOn: 74 | - CognitoUserPoolClientExternal 75 | HelperInitCognitoFunction: 76 | Type: AWS::Lambda::Function 77 | Metadata: 78 | cfn_nag: 79 | rules_to_suppress: 80 | - id: W89 81 | reason: "This is sample function which doesn't require VPC access." 82 | Properties: 83 | Code: 84 | ZipFile: | 85 | import json 86 | import boto3 87 | import os 88 | import cfnresponse 89 | 90 | AWS_REGION = os.environ['AWS_REGION'] 91 | 92 | def handler(event, context): 93 | print('Event: ', event) 94 | resource_properties = event['ResourceProperties'] 95 | client = boto3.client('cognito-idp') 96 | user_pool_id = resource_properties['UserPoolId'] 97 | user_name = resource_properties['CognitoUserName'] 98 | role = resource_properties['CognitoUserRole'] 99 | user_password = resource_properties['CognitoUserPassword'] 100 | response = '' 101 | if event['RequestType'] == 'Create': 102 | try: 103 | response = client.admin_create_user( 104 | UserPoolId = user_pool_id, 105 | Username= user_name, 106 | UserAttributes = [{ 107 | 'Name':'custom:Role', 108 | 'Value': role 109 | }], 110 | MessageAction = 'SUPPRESS', 111 | TemporaryPassword=user_password 112 | ) 113 | response = client.admin_set_user_password( 114 | UserPoolId = user_pool_id, 115 | Username = user_name, 116 | Password = user_password, 117 | Permanent = True 118 | ) 119 | except Exception as x: 120 | print('response', x) 121 | cfnresponse.send(event, context, cfnresponse.FAILED, {}) 122 | else: 123 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 124 | elif event['RequestType'] == 'Delete': 125 | try: 126 | response = client.admin_delete_user( 127 | UserPoolId = user_pool_id, 128 | Username= user_name 129 | ) 130 | print('response', response) 131 | except Exception as x: 132 | print('response', x) 133 | cfnresponse.send(event, context, cfnresponse.FAILED, {}) 134 | else: 135 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 136 | Handler: index.handler 137 | Role: !GetAtt HelperCognitoLambdaRole.Arn 138 | Runtime: python3.9 139 | ReservedConcurrentExecutions: 1 140 | Timeout: 30 141 | HelperInitializeCognitoUserExternal1: 142 | Type: Custom::HelperInitCognitoFunction 143 | Properties: 144 | ServiceToken: !GetAtt HelperInitCognitoFunction.Arn 145 | UserPoolId: !Ref CognitoUserPoolExternal 146 | CognitoUserName: !Ref ExternalUser1Name 147 | CognitoUserPassword: !Ref UserPassword 148 | CognitoUserRole: !Ref ExternalUserRole 149 | HelperInitializeCognitoUserExternal2: 150 | Type: Custom::HelperInitCognitoFunction 151 | Properties: 152 | ServiceToken: !GetAtt HelperInitCognitoFunction.Arn 153 | UserPoolId: !Ref CognitoUserPoolExternal 154 | CognitoUserName: !Ref ExternalUser2Name 155 | CognitoUserPassword: !Ref UserPassword 156 | CognitoUserRole: !Ref ExternalUserRole 157 | HelperInitializeCognitoUserInternal1: 158 | Type: Custom::HelperInitCognitoFunction 159 | Properties: 160 | ServiceToken: !GetAtt HelperInitCognitoFunction.Arn 161 | UserPoolId: !Ref CognitoUserPoolInternal 162 | CognitoUserName: !Ref InternalUser1Name 163 | CognitoUserPassword: !Ref UserPassword 164 | CognitoUserRole: !Ref InternalUserRole 165 | HelperInitializeCognitoUserInternal2: 166 | Type: Custom::HelperInitCognitoFunction 167 | Properties: 168 | ServiceToken: !GetAtt HelperInitCognitoFunction.Arn 169 | UserPoolId: !Ref CognitoUserPoolInternal 170 | CognitoUserName: !Ref InternalUser2Name 171 | CognitoUserPassword: !Ref UserPassword 172 | CognitoUserRole: !Ref InternalUserRole 173 | HelperCognitoLambdaRole: 174 | Type: AWS::IAM::Role 175 | Properties: 176 | AssumeRolePolicyDocument: 177 | Version: '2012-10-17' 178 | Statement: 179 | - Effect: Allow 180 | Principal: 181 | Service: 182 | - lambda.amazonaws.com 183 | Action: 184 | - sts:AssumeRole 185 | Path: "/" 186 | Policies: 187 | - PolicyName: helperCognitoLambdaRole 188 | PolicyDocument: 189 | Version: '2012-10-17' 190 | Statement: 191 | - Effect: Allow 192 | Action: 193 | - cognito-idp:Admin* 194 | Resource: 195 | - !GetAtt CognitoUserPoolExternal.Arn 196 | - !GetAtt CognitoUserPoolInternal.Arn 197 | - Effect: Allow 198 | Action: 199 | - logs:CreateLogGroup 200 | - logs:CreateLogStream 201 | - logs:PutLogEvents 202 | Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-HelperInitCognitoFunction-*:* 203 | ApiServiceIAMRole: 204 | Type: AWS::IAM::Role 205 | Properties: 206 | AssumeRolePolicyDocument: 207 | Version: 2012-10-17 208 | Statement: 209 | - Effect: Allow 210 | Principal: 211 | Service: 212 | - lambda.amazonaws.com 213 | Action: 214 | - sts:AssumeRole 215 | Path: / 216 | Policies: 217 | - PolicyName: LambdaExecutionPolicy 218 | PolicyDocument: 219 | Version: 2012-10-17 220 | Statement: 221 | - Effect: Allow 222 | Action: 223 | - logs:CreateLogGroup 224 | - logs:CreateLogStream 225 | - logs:PutLogEvents 226 | Resource: arn:aws:logs:*:*:* 227 | - Effect: Allow 228 | Action: 229 | - cognito-idp:Admin* 230 | Resource: 231 | - !GetAtt CognitoUserPoolExternal.Arn 232 | - !GetAtt CognitoUserPoolInternal.Arn 233 | ApiServiceLambdaFunction: 234 | Type: 'AWS::Lambda::Function' 235 | Metadata: 236 | cfn_nag: 237 | rules_to_suppress: 238 | - id: W89 239 | reason: "Sample function doesn't required VPC access." 240 | Properties: 241 | FunctionName: !Sub ${AWS::StackName}-lambda 242 | Runtime: "python3.9" 243 | Handler: "lambda.handler" 244 | Role: !GetAtt ApiServiceIAMRole.Arn 245 | ReservedConcurrentExecutions: 1 246 | Code: 247 | S3Bucket: !Sub ${AWS::StackName}-${AWS::AccountId}-${AWS::Region}-lambdas 248 | S3Key: "pets-clinic-api.zip" 249 | ApiServiceLambdaFunctionPermission: 250 | Type: 'AWS::Lambda::Permission' 251 | Properties: 252 | Action: "lambda:InvokeFunction" 253 | FunctionName: !GetAtt ApiServiceLambdaFunction.Arn 254 | Principal: "apigateway.amazonaws.com" 255 | # <<< API Service 256 | # >>> Amazon API gateway 257 | ApiGatewayRestApi: 258 | Type: 'AWS::ApiGateway::RestApi' 259 | Properties: 260 | Name: !Sub ${AWS::StackName}-apigateway 261 | ApiGatewayResource: 262 | Type: 'AWS::ApiGateway::Resource' 263 | Properties: 264 | RestApiId: !Ref ApiGatewayRestApi 265 | ParentId: !GetAtt ApiGatewayRestApi.RootResourceId 266 | PathPart: "{api+}" 267 | ApiGatewayMethod: 268 | Type: 'AWS::ApiGateway::Method' 269 | Metadata: 270 | cfn_nag: 271 | rules_to_suppress: 272 | - id: W59 273 | reason: "This is sample function which doesn't require AuthorizationType." 274 | Properties: 275 | HttpMethod: "ANY" 276 | ResourceId: !Ref ApiGatewayResource 277 | RestApiId: !Ref ApiGatewayRestApi 278 | AuthorizationType: NONE 279 | Integration: 280 | Type: AWS_PROXY 281 | IntegrationHttpMethod: "POST" 282 | Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiServiceLambdaFunction.Arn}/invocations 283 | ApiGatewayDeploymentUnProtected: 284 | Type: AWS::ApiGateway::Deployment 285 | Metadata: 286 | cfn_nag: 287 | rules_to_suppress: 288 | - id: W68 289 | reason: "This is sample function for blog which doesn't require usageplan." 290 | - id: W45 291 | reason: "This is sample function for blog which doesn't require AccessLogSetting." 292 | Properties: 293 | RestApiId: !Ref ApiGatewayRestApi 294 | StageName: dev 295 | Description: unprotected api 296 | DependsOn: 297 | - ApiGatewayMethod 298 | # >>> Inputs 299 | Parameters: 300 | ExternalUserRole: 301 | Type: String 302 | Default: Client 303 | Description: Enter Cognito External Pool username role. 304 | ExternalUser1Name: 305 | Type: String 306 | Default: Dave 307 | Description: Enter Cognito External Pool username. 308 | ExternalUser2Name: 309 | Type: String 310 | Default: Joy 311 | Description: Enter Cognito External Pool username2. 312 | InternalUserRole: 313 | Type: String 314 | Default: Doctor 315 | Description: Enter Cognito Internal Pool username2 role. 316 | InternalUser1Name: 317 | Type: String 318 | Default: Jane 319 | Description: Enter Cognito Internal Pool username2. 320 | InternalUser2Name: 321 | Type: String 322 | Default: Adam 323 | Description: Enter Cognito Internal Pool username3. 324 | UserPassword: 325 | Type: String 326 | AllowedPattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\^$*.\[\]{}\(\)?\-“!@#%&/,><\’:;|_~`])\S{8,99}$' 327 | Description: |- 328 | Enter Cognito users password. Password must fulfill User Pool Password Requirements. 329 | See documentaton for more details https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-policies.html 330 | # >>> Outputs 331 | Outputs: 332 | CognitoUserPoolClientInternal: 333 | Value: !Ref CognitoUserPoolClientInternal 334 | CognitoUserPoolClientExternal: 335 | Value: !Ref CognitoUserPoolClientExternal 336 | CognitoHostedUiExternalDomainUrl: 337 | Value: !Sub https://${CognitoUserPoolDomainExternal}.auth.${AWS::Region}.amazoncognito.com/login?client_id=${CognitoUserPoolClientExternal}&response_type=token&scope=email+openid+profile&redirect_uri=http://localhost 338 | CognitoHostedUiInternalDomainUrl: 339 | Value: !Sub https://${CognitoUserPoolDomainInternal}.auth.${AWS::Region}.amazoncognito.com/login?client_id=${CognitoUserPoolClientInternal}&response_type=token&scope=email+openid+profile&redirect_uri=http://localhost 340 | ApiGatewayDeploymentUrlApiEndpoint: 341 | Value: !Sub https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/dev/appointment 342 | -------------------------------------------------------------------------------- /infrastructure/stack-with-auth.template: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Resources: 3 | CognitoUserPoolInternal: 4 | Type: 'AWS::Cognito::UserPool' 5 | Properties: 6 | UserPoolName: !Sub ${AWS::StackName}-internal 7 | Schema: 8 | - AttributeDataType: String 9 | Name: Role 10 | CognitoUserPoolExternal: 11 | Type: 'AWS::Cognito::UserPool' 12 | Properties: 13 | UserPoolName: !Sub ${AWS::StackName}-external 14 | Schema: 15 | - AttributeDataType: String 16 | Name: Role 17 | CognitoUserPoolClientInternal: 18 | Type: 'AWS::Cognito::UserPoolClient' 19 | Properties: 20 | UserPoolId: !Ref CognitoUserPoolInternal 21 | ClientName: !Sub ${AWS::StackName}-internal 22 | AllowedOAuthFlows: 23 | - implicit 24 | AllowedOAuthFlowsUserPoolClient: true 25 | AllowedOAuthScopes: 26 | - email 27 | - openid 28 | - profile 29 | CallbackURLs: 30 | - 'http://localhost' 31 | GenerateSecret: false 32 | ExplicitAuthFlows: 33 | - ALLOW_USER_PASSWORD_AUTH 34 | - ALLOW_USER_SRP_AUTH 35 | - ALLOW_REFRESH_TOKEN_AUTH 36 | SupportedIdentityProviders: 37 | - COGNITO 38 | CognitoUserPoolClientExternal: 39 | Type: 'AWS::Cognito::UserPoolClient' 40 | Properties: 41 | ClientName: !Sub ${AWS::StackName}-external 42 | UserPoolId: !Ref CognitoUserPoolExternal 43 | AllowedOAuthFlows: 44 | - implicit 45 | AllowedOAuthFlowsUserPoolClient: true 46 | AllowedOAuthScopes: 47 | - email 48 | - openid 49 | - profile 50 | CallbackURLs: 51 | - 'http://localhost' 52 | GenerateSecret: false 53 | ExplicitAuthFlows: 54 | - ALLOW_USER_PASSWORD_AUTH 55 | - ALLOW_USER_SRP_AUTH 56 | - ALLOW_REFRESH_TOKEN_AUTH 57 | SupportedIdentityProviders: 58 | - COGNITO 59 | CognitoUserPoolDomainInternal: 60 | Type: 'AWS::Cognito::UserPoolDomain' 61 | Properties: 62 | # using client id will make the domain unique 63 | Domain: !Sub dns-name-${CognitoUserPoolClientInternal} 64 | UserPoolId: !Ref CognitoUserPoolInternal 65 | DependsOn: 66 | - CognitoUserPoolClientInternal 67 | CognitoUserPoolDomainExternal: 68 | Type: 'AWS::Cognito::UserPoolDomain' 69 | Properties: 70 | # using client id will make the domain unique 71 | Domain: !Sub dns-name-${CognitoUserPoolClientExternal} 72 | UserPoolId: !Ref CognitoUserPoolExternal 73 | DependsOn: 74 | - CognitoUserPoolClientExternal 75 | HelperInitCognitoFunction: 76 | Type: AWS::Lambda::Function 77 | Metadata: 78 | cfn_nag: 79 | rules_to_suppress: 80 | - id: W89 81 | reason: "This is sample function which doesn't require VPC access." 82 | Properties: 83 | Code: 84 | ZipFile: | 85 | import json 86 | import boto3 87 | import os 88 | import cfnresponse 89 | 90 | AWS_REGION = os.environ['AWS_REGION'] 91 | 92 | def handler(event, context): 93 | print('Event: ', event) 94 | resource_properties = event['ResourceProperties'] 95 | client = boto3.client('cognito-idp') 96 | user_pool_id = resource_properties['UserPoolId'] 97 | user_name = resource_properties['CognitoUserName'] 98 | role = resource_properties['CognitoUserRole'] 99 | user_password = resource_properties['CognitoUserPassword'] 100 | response = '' 101 | if event['RequestType'] == 'Create': 102 | try: 103 | response = client.admin_create_user( 104 | UserPoolId = user_pool_id, 105 | Username= user_name, 106 | UserAttributes = [{ 107 | 'Name':'custom:Role', 108 | 'Value': role 109 | }], 110 | MessageAction = 'SUPPRESS', 111 | TemporaryPassword=user_password 112 | ) 113 | response = client.admin_set_user_password( 114 | UserPoolId = user_pool_id, 115 | Username = user_name, 116 | Password = user_password, 117 | Permanent = True 118 | ) 119 | except Exception as x: 120 | print('response', x) 121 | cfnresponse.send(event, context, cfnresponse.FAILED, {}) 122 | else: 123 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 124 | elif event['RequestType'] == 'Delete': 125 | try: 126 | response = client.admin_delete_user( 127 | UserPoolId = user_pool_id, 128 | Username= user_name 129 | ) 130 | print('response', response) 131 | except Exception as x: 132 | print('response', x) 133 | cfnresponse.send(event, context, cfnresponse.FAILED, {}) 134 | else: 135 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) 136 | Handler: index.handler 137 | Role: !GetAtt HelperCognitoLambdaRole.Arn 138 | Runtime: python3.9 139 | ReservedConcurrentExecutions: 1 140 | Timeout: 30 141 | HelperInitializeCognitoUserExternal1: 142 | Type: Custom::HelperInitCognitoFunction 143 | Properties: 144 | ServiceToken: !GetAtt HelperInitCognitoFunction.Arn 145 | UserPoolId: !Ref CognitoUserPoolExternal 146 | CognitoUserName: !Ref ExternalUser1Name 147 | CognitoUserPassword: !Ref UserPassword 148 | CognitoUserRole: !Ref ExternalUserRole 149 | HelperInitializeCognitoUserExternal2: 150 | Type: Custom::HelperInitCognitoFunction 151 | Properties: 152 | ServiceToken: !GetAtt HelperInitCognitoFunction.Arn 153 | UserPoolId: !Ref CognitoUserPoolExternal 154 | CognitoUserName: !Ref ExternalUser2Name 155 | CognitoUserPassword: !Ref UserPassword 156 | CognitoUserRole: !Ref ExternalUserRole 157 | HelperInitializeCognitoUserInternal1: 158 | Type: Custom::HelperInitCognitoFunction 159 | Properties: 160 | ServiceToken: !GetAtt HelperInitCognitoFunction.Arn 161 | UserPoolId: !Ref CognitoUserPoolInternal 162 | CognitoUserName: !Ref InternalUser1Name 163 | CognitoUserPassword: !Ref UserPassword 164 | CognitoUserRole: !Ref InternalUserRole 165 | HelperInitializeCognitoUserInternal2: 166 | Type: Custom::HelperInitCognitoFunction 167 | Properties: 168 | ServiceToken: !GetAtt HelperInitCognitoFunction.Arn 169 | UserPoolId: !Ref CognitoUserPoolInternal 170 | CognitoUserName: !Ref InternalUser2Name 171 | CognitoUserPassword: !Ref UserPassword 172 | CognitoUserRole: !Ref InternalUserRole 173 | HelperCognitoLambdaRole: 174 | Type: AWS::IAM::Role 175 | Properties: 176 | AssumeRolePolicyDocument: 177 | Version: '2012-10-17' 178 | Statement: 179 | - Effect: Allow 180 | Principal: 181 | Service: 182 | - lambda.amazonaws.com 183 | Action: 184 | - sts:AssumeRole 185 | Path: "/" 186 | Policies: 187 | - PolicyName: helperCognitoLambdaRole 188 | PolicyDocument: 189 | Version: '2012-10-17' 190 | Statement: 191 | - Effect: Allow 192 | Action: 193 | - cognito-idp:Admin* 194 | Resource: 195 | - !GetAtt CognitoUserPoolExternal.Arn 196 | - !GetAtt CognitoUserPoolInternal.Arn 197 | - Effect: Allow 198 | Action: 199 | - logs:CreateLogGroup 200 | - logs:CreateLogStream 201 | - logs:PutLogEvents 202 | Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-HelperInitCognitoFunction-*:* 203 | # >>> AVP Store 204 | AVPPolicyStore: 205 | Type: AWS::VerifiedPermissions::PolicyStore 206 | Properties: 207 | ValidationSettings: 208 | Mode: "OFF" 209 | AVPInternalPolicy: 210 | Type: AWS::VerifiedPermissions::Policy 211 | Properties: 212 | PolicyStoreId: !GetAtt AVPPolicyStore.PolicyStoreId 213 | Definition: 214 | Static: 215 | Description: "Policy defining access for internal users" 216 | Statement: | 217 | permit (principal in UserGroup::"AllVeterinarians", 218 | action == Action::"GET/appointment", 219 | resource in UserGroup::"AllVeterinarians") 220 | when { principal == resource.Veterinarian}; 221 | AVPExternalPolicy: 222 | Type: AWS::VerifiedPermissions::Policy 223 | Properties: 224 | PolicyStoreId: !GetAtt AVPPolicyStore.PolicyStoreId 225 | Definition: 226 | Static: 227 | Description: "Policy defining access for external client users" 228 | Statement: | 229 | permit (principal in UserGroup::"AllClients", 230 | action == Action::"GET/appointment", 231 | resource in UserGroup::"AllClients") 232 | when { principal == resource.owner}; 233 | # >>> API Service 234 | ApiServiceIAMRole: 235 | Type: AWS::IAM::Role 236 | Properties: 237 | AssumeRolePolicyDocument: 238 | Version: 2012-10-17 239 | Statement: 240 | - Effect: Allow 241 | Principal: 242 | Service: 243 | - lambda.amazonaws.com 244 | Action: 245 | - sts:AssumeRole 246 | Path: / 247 | Policies: 248 | - PolicyName: LambdaExecutionPolicy 249 | PolicyDocument: 250 | Version: 2012-10-17 251 | Statement: 252 | - Effect: Allow 253 | Action: 254 | - logs:CreateLogGroup 255 | - logs:CreateLogStream 256 | - logs:PutLogEvents 257 | Resource: arn:aws:logs:*:*:* 258 | - Effect: Allow 259 | Action: 260 | - cognito-idp:Admin* 261 | Resource: 262 | - !GetAtt CognitoUserPoolExternal.Arn 263 | - !GetAtt CognitoUserPoolInternal.Arn 264 | ApiServiceLambdaFunction: 265 | Type: 'AWS::Lambda::Function' 266 | Metadata: 267 | cfn_nag: 268 | rules_to_suppress: 269 | - id: W89 270 | reason: "Sample function doesn't required VPC access." 271 | Properties: 272 | FunctionName: !Sub ${AWS::StackName}-lambda 273 | Runtime: "python3.9" 274 | Handler: "lambda.handler" 275 | Role: !GetAtt ApiServiceIAMRole.Arn 276 | ReservedConcurrentExecutions: 1 277 | Code: 278 | S3Bucket: !Sub ${AWS::StackName}-${AWS::AccountId}-${AWS::Region}-lambdas 279 | S3Key: "pets-clinic-api.zip" 280 | ApiServiceLambdaFunctionPermission: 281 | Type: 'AWS::Lambda::Permission' 282 | Properties: 283 | Action: "lambda:InvokeFunction" 284 | FunctionName: !GetAtt ApiServiceLambdaFunction.Arn 285 | Principal: "apigateway.amazonaws.com" 286 | # <<< API Service 287 | # >>> Amazon API gateway 288 | ApiGatewayRestApi: 289 | Type: 'AWS::ApiGateway::RestApi' 290 | Properties: 291 | Name: !Sub ${AWS::StackName}-apigateway 292 | ApiGatewayResource: 293 | Type: 'AWS::ApiGateway::Resource' 294 | Properties: 295 | RestApiId: !Ref ApiGatewayRestApi 296 | ParentId: !GetAtt ApiGatewayRestApi.RootResourceId 297 | PathPart: "{api+}" 298 | ApiGatewayMethod: 299 | Type: 'AWS::ApiGateway::Method' 300 | Metadata: 301 | cfn_nag: 302 | rules_to_suppress: 303 | - id: W59 304 | reason: "This is sample function which doesn't require AuthorizationType." 305 | Properties: 306 | HttpMethod: "ANY" 307 | ResourceId: !Ref ApiGatewayResource 308 | RestApiId: !Ref ApiGatewayRestApi 309 | AuthorizationType: CUSTOM 310 | AuthorizerId: !Ref ApiGatewayAuthorizer 311 | Integration: 312 | Type: AWS_PROXY 313 | IntegrationHttpMethod: "POST" 314 | Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiServiceLambdaFunction.Arn}/invocations 315 | ApiGatewayDeploymentProtected: 316 | Type: AWS::ApiGateway::Deployment 317 | Metadata: 318 | cfn_nag: 319 | rules_to_suppress: 320 | - id: W68 321 | reason: "This is sample function for blog which doesn't require usageplan." 322 | - id: W45 323 | reason: "This is sample function for blog which doesn't require AccessLogSetting." 324 | Properties: 325 | RestApiId: !Ref ApiGatewayRestApi 326 | StageName: dev 327 | Description: protected api 328 | DependsOn: 329 | - ApiGatewayMethod 330 | - ApiGatewayAuthorizer 331 | CustomAuthIAMRole: 332 | Type: AWS::IAM::Role 333 | Properties: 334 | AssumeRolePolicyDocument: 335 | Version: 2012-10-17 336 | Statement: 337 | - Effect: Allow 338 | Principal: 339 | Service: 340 | - lambda.amazonaws.com 341 | Action: 342 | - sts:AssumeRole 343 | Path: / 344 | Policies: 345 | - PolicyName: LambdaExecutionPolicy 346 | PolicyDocument: 347 | Version: 2012-10-17 348 | Statement: 349 | - Effect: Allow 350 | Action: 351 | - logs:CreateLogGroup 352 | - logs:CreateLogStream 353 | - logs:PutLogEvents 354 | Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}-CustomAuthLambdaFunction-*:* 355 | - Effect: Allow 356 | Action: 357 | - verifiedpermissions:IsAuthorized 358 | Resource: '*' 359 | CustomAuthLambdaFunction: 360 | Type: 'AWS::Lambda::Function' 361 | Metadata: 362 | cfn_nag: 363 | rules_to_suppress: 364 | - id: W89 365 | reason: "This is sample function which doesn't require VPC access." 366 | Properties: 367 | Runtime: "python3.9" 368 | Handler: "lambda.handler" 369 | Role: !GetAtt CustomAuthIAMRole.Arn 370 | ReservedConcurrentExecutions: 1 371 | Code: 372 | S3Bucket: !Sub ${AWS::StackName}-${AWS::AccountId}-${AWS::Region}-lambdas 373 | S3Key: "custom-auth.zip" 374 | Environment: 375 | Variables: 376 | AWS_DATA_PATH: "./models" 377 | COGNITO_INTERNAL_USER_POOL_ID: !Ref CognitoUserPoolInternal 378 | COGNITO_INTERNAL_APP_CLIENT_ID: !Ref CognitoUserPoolClientInternal 379 | COGNITO_EXTERNAL_USER_POOL_ID: !Ref CognitoUserPoolExternal 380 | COGNITO_EXTERNAL_APP_CLIENT_ID: !Ref CognitoUserPoolClientExternal 381 | VERIFIED_PERMISSION_POLICY_STORE_ID: !GetAtt AVPPolicyStore.PolicyStoreId 382 | ApiGatewayCustomAuthIAMPolicy: 383 | Type: 'AWS::IAM::Policy' 384 | Properties: 385 | PolicyName: !Sub ${AWS::StackName}-ApiGatewayCustomAuthIAMPolicy 386 | Roles: 387 | - !Ref ApiGatewayCustomAuthIAMRole 388 | PolicyDocument: 389 | Version: 2012-10-17 390 | Statement: 391 | - Effect: Allow 392 | Action: 393 | - 'lambda:InvokeFunction' 394 | Resource: !GetAtt CustomAuthLambdaFunction.Arn 395 | ApiGatewayCustomAuthIAMRole: 396 | Type: 'AWS::IAM::Role' 397 | Properties: 398 | AssumeRolePolicyDocument: |- 399 | { 400 | "Version": "2012-10-17", 401 | "Statement": [ 402 | { 403 | "Action": "sts:AssumeRole", 404 | "Principal": { 405 | "Service": "apigateway.amazonaws.com" 406 | }, 407 | "Effect": "Allow", 408 | "Sid": "" 409 | } 410 | ] 411 | } 412 | ApiGatewayAuthorizer: 413 | Type: 'AWS::ApiGateway::Authorizer' 414 | Properties: 415 | Name: custom-auth 416 | RestApiId: !Ref ApiGatewayRestApi 417 | Type: REQUEST 418 | IdentitySource: method.request.header.Authorization 419 | AuthorizerResultTtlInSeconds: '300' 420 | AuthorizerCredentials: !GetAtt ApiGatewayCustomAuthIAMRole.Arn 421 | AuthorizerUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${CustomAuthLambdaFunction.Arn}/invocations 422 | # <<< API Gateway authorizer 423 | # >>> Inputs 424 | Parameters: 425 | ExternalUserRole: 426 | Type: String 427 | Default: Client 428 | Description: Enter Cognito External Pool username role. 429 | ExternalUser1Name: 430 | Type: String 431 | Default: Dave 432 | Description: Enter Cognito External Pool username. 433 | ExternalUser2Name: 434 | Type: String 435 | Default: Joy 436 | Description: Enter Cognito External Pool username2. 437 | InternalUserRole: 438 | Type: String 439 | Default: Doctor 440 | Description: Enter Cognito Internal Pool username2 role. 441 | InternalUser1Name: 442 | Type: String 443 | Default: Jane 444 | Description: Enter Cognito Internal Pool username2. 445 | InternalUser2Name: 446 | Type: String 447 | Default: Adam 448 | Description: Enter Cognito Internal Pool username3. 449 | UserPassword: 450 | Type: String 451 | AllowedPattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\^$*.\[\]{}\(\)?\-“!@#%&/,><\’:;|_~`])\S{8,99}$' 452 | Description: |- 453 | Enter Cognito users password. Password must fulfill User Pool Password Requirements. 454 | See documentaton for more details https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-policies.html 455 | # >>> Outputs 456 | Outputs: 457 | CognitoUserPoolClientInternal: 458 | Value: !Ref CognitoUserPoolClientInternal 459 | CognitoUserPoolClientExternal: 460 | Value: !Ref CognitoUserPoolClientExternal 461 | CognitoHostedUiExternalDomainUrl: 462 | Value: !Sub https://${CognitoUserPoolDomainExternal}.auth.${AWS::Region}.amazoncognito.com/login?client_id=${CognitoUserPoolClientExternal}&response_type=token&scope=email+openid+profile&redirect_uri=http://localhost 463 | CognitoHostedUiInternalDomainUrl: 464 | Value: !Sub https://${CognitoUserPoolDomainInternal}.auth.${AWS::Region}.amazoncognito.com/login?client_id=${CognitoUserPoolClientInternal}&response_type=token&scope=email+openid+profile&redirect_uri=http://localhost 465 | ApiGatewayDeploymentUrlApiEndpoint: 466 | Value: !Sub https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/dev/appointment 467 | -------------------------------------------------------------------------------- /pets-clinic-api/lambda.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | appointments = { 4 | "PI-T123": { 5 | "id": "PI-T123", 6 | "name": "Dave", 7 | "Pet" : "Onyx - Dog. 2y 3m", 8 | "Phone Number": "+1234567", 9 | "Visit History": "Patient History from last visit with primary vet", 10 | "Assigned Veterinarian": "Jane" 11 | }, 12 | "PI-T124": { 13 | "id": "PI-T124", 14 | "name": "Joy", 15 | "Pet" : "Jelly - Dog. 6y 2m", 16 | "Phone Number": "+1368728", 17 | "Visit History": "None", 18 | "Assigned Veterinarian": "Jane" 19 | }, 20 | "PI-T125": { 21 | "id": "PI-T125", 22 | "name": "Dave", 23 | "Pet" : "Sassy - Cat. 1y", 24 | "Phone Number": "+1398777", 25 | "Visit History": "Patient History from last visit with primary vet", 26 | "Assigned Veterinarian": "Adam" 27 | } 28 | } 29 | 30 | def handler(event, context): 31 | print('Event: ', event) 32 | 33 | try: 34 | path = event['path'] 35 | http_method = event['httpMethod'] 36 | 37 | if '/appointment/' in path and http_method == 'GET': 38 | appointment_id = path.split('/appointment/')[1] 39 | if appointments.get(appointment_id): 40 | return response_handler({'appointment': appointments[appointment_id]}, 200) 41 | else: 42 | return response_handler({}, 404) 43 | 44 | 45 | except Exception as e: 46 | print(e) 47 | return response_handler({'msg': 'Internal Server Error'}, 500) 48 | 49 | 50 | def response_handler(payload, status_code): 51 | return { 52 | "statusCode": status_code, 53 | "headers": { 54 | "Content-Type": "application/json" 55 | }, 56 | "body": json.dumps(payload), 57 | "isBase64Encoded": False 58 | } 59 | -------------------------------------------------------------------------------- /pets-clinic-api/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/multiple-identity-providers-amazon-api-gateway/4d00e56ac0773db5a2f10afd31b57f7ef3b4060c/pets-clinic-api/requirements.txt --------------------------------------------------------------------------------