Auth Portal powered by AWS/AzureAD and built with CDKs
Tags: azure, cdk, cdktf, aws
Published: 2022-06-10 08:39:28

This one aims to bring together all the pieces required to build and deploy an authentication portal in AWS leveraging Azure AD as IdP. Something that has recently been more and more often used across AWS projects and this time I thought I would go about it a bit differently to try out new things and therefore gain more insights into some tech I haven’t had a chance to master yet. As usual, at the same time sharing some thoughts and experiences with whoever’s interested.

## Tech stack
From the stack outlined below, I’ve already got a huge experience with AWS and Terraform which should not be a surprise if you read my previous [publications](https://medium.com/@sebolabs). However, this time I wanted to shift my focus to using some other popular, open-source tooling I had limited knowledge of and put them into the mix.

- **AWS** — to host the Portal
- **AWS CDK** (v. 2.26.0)— for developing the Portal (_type-script_)
- **Azure AD** — as an identity service provider (federated authentication)
- **CDKtf** (v. 0.11.0) — for configuring Azure AD (_type-script_)
- **React/Amplify** — for a bit of frontend

As for my job, I must take decisions from time to time on what tooling should be used to deliver a solution, I badly wanted to give **_AWS CDK_** a full end-to-end go to witness whether that framework can be easily used for something more than just a PoC. And as there was **_CDKtf_** too I decided to give that one a chance as well.
Regarding the frontend bit, don’t expect too much ;) It’s not something I do and only set that up to cover up the entire infrastructure stack this story is mainly about and to show a tangible result.

## Design
The high-level diagram below represents the spectrum of services composing the Auth Portal in AWS and its integration with Azure AD.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ya4hrjly1yb70u9fhwvn.png) 

In case you want to deepen your understanding of the SAML user pool IdP authentication flow please navigate to this page: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-saml-idp-authentication.html

### Goals
Apart from the fact that I wanted to get some hands-on experience with CDKs I also set several goals for myself in terms of what I wanted to try out. These were:

- Cognito hosted UI (with customisations)
- Cognito custom domain
- Pre token generation Lambda trigger
- Cognito required claims
- Azure AD additional/custom claims
- Azure AD → Cognito claims mappings
- Azure App access restrictions based on security group membership

While I abandoned the last one as I wanted to keep the _FREE_ Azure subscription plan that only allows access restrictions on the user account basis, the rest turned out not to be difficult to configure.

### Source code
The source code for the entire stack can be referenced below.

https://github.com/sebolabs/auth-portal

To use it, you’ll have to export the required environment variables.
Additionally, it expects you to have your own public domain and a valid ACM certificate in the N. Virginia region (_CloudFront and Cognito requirement_).

Because the frontend part is optional I decided to keep it separate from the AWS portal stack code. If you want to test the authentication flow in the simplest possible way just set the `S3_DUMMY_PAGE_DEPLOY` environment variable to `true`, surf to the address below, sign in and then check things with Developers Tools in the browser and [jwt.io](https://jwt.io).

`https://<cognito custom domain>/login?response_type=code&client_id=<cognito client id>&redirect_uri=<portal site url>`

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/391ehz6fazfe7f1t7bbp.png) 

### Caveats
Cognito **custom domain** feature assumes that if you expose the Cognito authentication endpoint at `auth.test.example.com` then your landing page is `test.example.com`. If that's not the case, like in my example, then Cognito expects you to have a resolvable A record configured for `test.example.com` to perform some verification. Moreover, on the Azure side, you must also let your root domain `example.com` be verified by configuring a TXT record with a provided value.

Cognito **required claims** can only be set up at the user pool provisioning stage and so cannot be modified at a later time. This means that if you change your mind you’ll have to delete your Cognito user pool and create it again. This also means your user pool ID changes along with the client application ID and so both the Azure AD and the frontend configuration must be updated with new values. What can turn out to be even more disturbing is the fact that you lose all user profiles previously created in Cognito. Luckily, users can now be imported from CSV.

The **Pre token generation Lambda function** hasn’t got any logic customising the claims and was set up only to see what useful information it can produce _out of the box_.

## AWS & CDK
With CDK the stack gets synthesized and translated into a CloudFormation template. Depending on features you decide to leverage across your code there could be additional, custom resources spun up for you to satisfy requirements.
A cool feature here is that any changes to the IAM resources require your attention and approval before getting deployed. This is useful from the security/audit perspective.
One thing to keep in mind when bootstrapping a CDK project is the `qualifier` option that helps you avoid resource name clashes when provisioning multiple bootstrap stacks in the same AWS account.

Must read: https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html

```
$ cdk deploy
✨  Synthesis time: 3.59s

PortalStack: deploying...
[0%] start: Publishing 483ae06ed27ef8ca76e011264d772420593a6cfe8544759c306ef3b98c9e25be:XXXXXXXXXXXX-eu-central-1
[...]
[100%] success: Published ca2e471276d39c586eae61d73c2e253eb08b4a648a8676f2000f81271b73a405:XXXXXXXXXXXX-eu-central-1

PortalStack: creating CloudFormation changeset...
✅  PortalStack
✨  Deployment time: 393.82s

Outputs:
PortalStack.cognitoDomainName = https://auth.test.sebolabs.net
PortalStack.cognitoUserPoolClientId = 72661fo2r4bgob1ateqskfhicd
PortalStack.cognitoUserPoolId = eu-central-1_XXXXXXXXX
PortalStack.frontendS3BucketName = sebolabs-test-portal-XXXXXXXXXXXX-eu-central-1
PortalStack.portalSiteUrl = https://portal.test.sebolabs.net

Stack ARN:
arn:aws:cloudformation:eu-central-1:XXXXXXXXXXXX:stack/PortalStack/e9974180-e322-11ec-ab92-02e74f818fb0

✨  Total time: 397.4s
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3an9jvtpkpkderkoyky7.png) 

### Testing
One of the fundamentals and a huge advantage of using CDK is the fact that infrastructure code can be tested just like application code.

```
$ npm test
> portal@0.1.0 test
> jest
 PASS  test/cf.test.ts
 PASS  test/cognito.test.ts
 PASS  test/s3.test.ts
 PASS  test/lambda.test.ts
Test Suites: 4 passed, 4 total
Tests:       9 passed, 9 total
Snapshots:   0 total
Time:        3.852 s, estimated 4 s
Ran all test suites.
```

## Azure AD & CDKtf
With CDKtf the stack gets synthesized and translated into a Terraform plan (zipped) that is then executed either locally or remotely.
When using Terraform Cloud for your backend you can have it to store your state and optionally use it to run Terraform keeping a track of all your runs in one place. Another nice feature of using that remote backend is that it also versions states and highlights changes (diff) between consecutive Terraform runs.
Furthermore, CDKtf supports most of the Terraform well-known commands e.g. `output` that can become very useful for example to pass certain values between stages in a CI/CD pipeline, but also `locals`,`remote states` and other fundamental Terraform features.

Must read: https://www.terraform.io/cdktf

```
$ cdktf deploy
sebolabs-aad-auth-portal  Initializing the backend...
sebolabs-aad-auth-portal  Initializing provider plugins...
                          - Reusing previous version of hashicorp/azuread from the dependency lock file
sebolabs-aad-auth-portal  - Using previously-installed hashicorp/azuread v2.22.0
sebolabs-aad-auth-portal  Terraform has been successfully initialized!

azuread_claims_mapping_policy.portal_cmp (portal_cmp): Refreshing state... [id=a733375e-b67c-49de-9142-12af23de9afa]
azuread_group.portal_users (portal_users): Refreshing state... [id=e426314e-14fb-4415-9fd8-e170981b378e]
azuread_application.portal_app (portal_app): Refreshing state... [id=764cc3ae-2cf0-4cd5-9521-091b7bd3bece]
azuread_service_principal.portal_sp (portal_sp): Refreshing state... [id=a4d4926d-c5cc-402c-96c4-016dc396325f]
azuread_service_principal_claims_mapping_policy_assignment.portal_cmpa (portal_cmpa): Refreshing state... [id=a4d4926d-c5cc-402c-96c4-016dc396325f/claimsMappingPolicy/a733375e-b67c-49de-9142-12af23de9afa]

No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

sebolabs-aad-auth-portal
  app_id = 6e8b9c9d-715b-4371-810f-4avc07a27c2x
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4i1dgjgwhcc2k7v9zssk.png) 

### Testing
Likewise, CDKtf also enables you to run unit tests against your code.

```
$ npm test
> portal-azure@1.0.0 test
> jest
PASS  __tests__/main-test.ts
  Terraform
    ✓ check if the produced terraform configuration is valid
    ✎ todo check if this can be planned
  AzureAD configuration
    ✎ todo should contain an application
    ✎ todo should contain a service principal
    ✎ todo should contain a claims mapping policy
    ✎ todo should contain a service principal claims mapping policy assignment
Test Suites: 1 passed, 1 total
Tests:       5 todo, 1 passed, 6 total
Snapshots:   0 total
Time:        4.438 s, estimated 5 s
Ran all test suites.
```

## Outcome
And finally, here’s the result of putting all the things together…

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4u11iwr6fyobq3m2rgr.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jues1xw0mnwxj6kx0kdk.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5ef4po1ljvl695gcajhu.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7gi8g5w1xzzueqswwdcc.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qmnvm11n2tt8bprgf0b8.png)     

💡 In case some user information was not retrieved or mapped as expected, it’s worth comparing the SAML response (**_idpresponse_** payload) with the ID JWT token (**_token_** preview) using Developer Tools from within a browser.

### CloudWatch Logs Insights
The aforementioned Lambda function that is meant to be used for customising ID token claims can also be used to just log certain information carried by tokens. Especially when Cognito is a black box not revealing anything, such data can become very useful e.g. if the authentication flow must be debugged or to generate some users activity statistics. Moreover, very often project teams working on the AWS side of things have no access to the Azure AD application sign-in logs and so that way they can gain some insights.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8c5hd3o07r7mi1nw6459.png) 

## Wrap-up
### AWS & Azure AD
First of all, setting up an end-to-end authentication flow using Amazon Cognito and Azure AD is fairly simple. Obviously, there are some caveats here and there + things one must be aware of as both services are managed cloud services working based on some assumptions etc. Therefore, it is worth spending some time reading relevant documentation in advance.

The authentication mechanism configured as a part of this story is just a beginning though. Going further, there’s logic one may want to implement with Lambda triggers and also the entire authorisation flow when integrating such a frontend solution with backend services. Regarding the latter, there are decisions to be made e.g. what type of an authoriser should be used when integrating with API Gateways and even more on access scopes. Either way, having a token carrying correct claims is a good start.

### AWS CDK & CDKtf
On the CDKs side of things, I must admit both have turned out to be very appealing and promising! During my try-out, I certainly enjoyed the fact things simply happen automatically without me having to worry about things I used to worry about when working with cloud infrastructure orchestrators natively. There were several issues I came across but they were rather related either to the underlying orchestrators or cloud providers themselves, nothing that would want me to ditch any of the CDKs.

In terms of AWS CDK, I’m not a big fan of some of the concepts CloudFormation is based on, e.g. the fact you can’t delete a resource manually in the console and then rerun `cdk deploy` to calculate what’s missing and reprovision that resource. Instead, you get a resource not found exception.

In terms of CDKtf, having configured only five resources is probably not representative enough to draw big conclusions, however, it just worked and literally took me minutes to have a working deployment mechanism.

Just like with any other frameworks, e.g. the serverless framework, CDKs can massively simplify developers' lives. With AWS Security Speciality hat on, however, I want to emphasize the security aspect of the infrastructure being provisioned as a part of application development. I’ve seen many times already how insecure such infrastructure can become when it’s developed by individuals having not enough security-in-the-cloud awareness, especially when there’s networking involved. Therefore, getting compliant constructs or modules is definitely something that should be considered by organisations and project teams who care more about just application features.

Finally, both CDKs require you to figure out the best way you want to go about environments and how you’re going to provide environment-specific values when deploying stacks. It’s not as straightforward as with Terraform environment files but once you get it right you can’t go wrong with CDKs. Both frameworks can still be considered quite new and I strongly believe that with time they’ll become even more robust.