<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Code Genie | Blog</title><description>Generate Full Stack Serverless AWS Apps based on your description or data model. Projects include Next.js, React, Express, Lambda, API Gateway, Cognito Auth, DynamoDB, and more.</description><link>https://codegenie.codes/</link><language>en</language><item><title>So long API Gateway, and thanks for all the routes</title><link>https://codegenie.codes/blog/so-long-api-gateway-and-thanks-for-all-the-routes/</link><guid isPermaLink="true">https://codegenie.codes/blog/so-long-api-gateway-and-thanks-for-all-the-routes/</guid><description>For nearly a decade I&amp;#39;ve been building APIs with API Gateway and Lambda. However, most of the time I just need a way to invoke a Lambda Function over HTTP with a custom domain name, and this is exactly what CloudFront + Lambda Function URL (CLFURL) enables.

</description><pubDate>Thu, 19 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Image } from &amp;#39;astro:assets&amp;#39;;
import clefairy from &amp;#39;../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/clefairy.gif&amp;#39;
import lfurlWorkaroundTweets from &amp;#39;../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/lfurl-workaround-tweets.png&amp;#39;
import cognitoJwtVerifier from &amp;#39;../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/cognito-jwt-verifier.png&amp;#39;
import jwtExpressMiddleware from &amp;#39;../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/jwt-express-middleware.png&amp;#39;
import apigwLfurlClfurlPerformance from &amp;#39;../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/apigw-lfurl-clfurl-performance.png&amp;#39;
import codeGenieMaxDescription from &amp;#39;../../../assets/images/blog/so-long-api-gateway-and-thanks-for-all-the-routes/code-genie-max-description.png&amp;#39;&lt;/p&gt;
&lt;p&gt;For nearly a decade I&amp;#39;ve been building APIs with API Gateway and Lambda. I was even fortunate enough to be part of the team that launched API Gateway back in 2015! It&amp;#39;s served me well and I&amp;#39;m sure I&amp;#39;ll use it again on future projects that require some of its more advanced features.&lt;/p&gt;
&lt;p&gt;However, most of the time I just need a way to invoke a Lambda Function over HTTP with a custom domain name, and this is exactly what CloudFront + Lambda Function URL (CLFURL) enables.&lt;/p&gt;
&lt;figure&gt;
  &lt;a href=&quot;https://codegenie.codes/blog/so-long-api-gateway-and-thanks-for-all-the-routes&quot;&gt;
            &lt;div&gt;&lt;img src=&quot;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MDAgMjUwIiB3aWR0aD0iNDAwIiBoZWlnaHQ9IjI1MCI+CiAgPHJlY3Qgd2lkdGg9IjQwMCIgaGVpZ2h0PSIyNTAiIGZpbGw9IiNCM0IwQjBGRiI+PC9yZWN0PgogIDx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0ibW9ub3NwYWNlIiBmb250LXNpemU9IjI1cHgiIGZpbGw9IiMyNjI2MjZGRiI+SW1hZ2U8L3RleHQ+ICAgCjwvc3ZnPg==&quot; alt=&quot;Clefairy&quot; /&gt;&lt;/div&gt;
            &lt;blockquote&gt;&lt;em&gt;Original image available in the blog post.&lt;/em&gt;&lt;/blockquote&gt;
          &lt;/a&gt;
          &lt;br /&gt;
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{&quot; margin:=&quot;&quot; color:=&quot;&quot; =&quot;}}&quot;&gt;
    Clefairy: The unofficial mascot of CLFURL
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2&gt;Why change?&lt;/h2&gt;
&lt;h3&gt;Cost&lt;/h3&gt;
&lt;p&gt;Since launch, one of the biggest criticisms of API Gateway has been its pricing. One would expect that Lambda (the thing that&amp;#39;s doing the heavy lifting) would make up the majority of the cost, and not the thing that&amp;#39;s simply connecting it to a client. In reality, API Gateway (even the newer, cheaper HTTP API variant) often exceeds Lambda costs by more than 2x. Under similar conditions, CloudFront costs ~10% of what Lambda does.&lt;/p&gt;
&lt;p&gt;:::caution[WARNING: Gross oversimplification to prove a point detected]
Saying API Gateway is just a service that connects Lambda over HTTP is unfair. &lt;a href=&quot;https://x.com/theburningmonk&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Yan Cui&lt;/a&gt; has an excellent comparison of the two approaches in his blog post &lt;a href=&quot;https://theburningmonk.com/2024/03/when-to-use-api-gateway-vs-lambda-function-urls/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;When to use API Gateway vs. Lambda Function URLs&lt;/a&gt;. He covers many of the advanced features of API Gateway and concludes with a preference for API Gateway in most scenarios.
:::&lt;/p&gt;
&lt;p&gt;The one area where API Gateway has a cost advantage is on unauthorized requests. When using its Authorizer feature, API Gateway eats those costs for you. With CLFURL you would pay both the CloudFront and Lambda costs for unauthorized requests. This is mostly only a concern when it comes to DDOS (or Denial of Wallet) attacks. Fortunately, &lt;a href=&quot;https://docs.aws.amazon.com/waf/latest/developerguide/ddos-event-mitigation-logic-continuous-inspection.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS Shield Standard&lt;/a&gt; (a free DDOS protection service included with CloudFront) &lt;em&gt;should&lt;/em&gt; mitigate those attacks.&lt;/p&gt;
&lt;h3&gt;Max timeout&lt;/h3&gt;
&lt;p&gt;API Gateway has a max timeout of 29 seconds, which is more than enough for most REST API needs. However, with AI becoming a core feature of many products, those extra 31 seconds afforded by CloudFront (a total of 1 minute) can be crucial.&lt;/p&gt;
&lt;p&gt;In fact, this is the main reason I started investigating this option in the first place! &lt;a href=&quot;https://codegenie.codes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Code Genie&lt;/a&gt; uses AI to generate data models based on the description of your app. There is currently a 500 character limit on this description to mitigate the chance of it taking longer than 29 seconds. By moving to CLFURL, Code Genie will be able to handle significantly more complex requests, and enable other AI features in the future.&lt;/p&gt;
&lt;figure&gt;
  
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{&quot; margin:=&quot;&quot; color:=&quot;&quot; =&quot;}}&quot;&gt;
    500 characters is not enough to describe complex applications 😞
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;:::note[API Gateway max timeout can be increased on request]
API Gateway &lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2024/06/amazon-api-gateway-integration-timeout-limit-29-seconds/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;recently announced&lt;/a&gt; it can increase the max timeout via support request. This is only available for Regional REST APIs (not the less-expensive HTTP API offering, or Edge-Optimized REST APIs).
:::&lt;/p&gt;
&lt;h3&gt;Performance?&lt;/h3&gt;
&lt;p&gt;In my tests comparing the performance of API Gateway to CloudFront, I found both approaches yield similar results. Here are the highlights:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CloudFront adds a shocking +300ms latency for cross-region (other-side-of-the-world) requests &lt;strong&gt;when not under load&lt;/strong&gt;. These findings are why I didn&amp;#39;t switch to CLFURL earlier this year. I recently decided to test the performance again, hoping that AWS had resolved the issue. They hadn&amp;#39;t. However, after diving deeper I discovered that this latency is akin to a cold start. When under load, this extra latency is a rare occurrence.&lt;/li&gt;
&lt;li&gt;Under load, CloudFront offers marginally better performance than API Gateway HTTP API. ~4% faster on average. &lt;strong&gt;These tests were only run against the cheaper, faster HTTP API option&lt;/strong&gt;. If you&amp;#39;re using the original REST API option, you might see a more significant performance difference around the 20% mark (along with 3x the cost).&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;
  
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{&quot; margin:=&quot;&quot; color:=&quot;&quot; =&quot;}}&quot;&gt;
    API Gateway vs Lambda Function URL vs CloudFront + LFURL
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;You can try it out yourself at &lt;a href=&quot;https://production.d1gtqqp4ixg5qm.amplifyapp.com/public-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://production.d1gtqqp4ixg5qm.amplifyapp.com/public-api&lt;/a&gt; (no guarantees on how long this website will be live) or deploy your own version by cloning this repo &lt;a href=&quot;https://github.com/CodeGenieApp/cloudfront-lambda-url-vs-apigw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/CodeGenieApp/cloudfront-lambda-url-vs-apigw&lt;/a&gt; and running &lt;code&gt;npm run init:dev&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Locking down the Lambda FURL&lt;/h2&gt;
&lt;p&gt;If you want to enforce CloudFront security features such as Shield and WAF, you need to ensure attackers can&amp;#39;t bypass CloudFront by calling your Lambda Function URL directly. To accomplish this, &lt;a href=&quot;https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS recommends&lt;/a&gt; setting the LFURL&amp;#39;s authorization to AWS_IAM and using OAC to grant access for the distribution. If only it were that simple. Unfortunately, there are two challenges  with this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CloudFront overrides the &lt;code&gt;Authorization&lt;/code&gt; header when invoking your Lambda Function, so you need to use a different, non-standard header to include your auth token. You can use CloudFront Functions to copy the original &lt;code&gt;Authorization&lt;/code&gt; header to a new header like &lt;code&gt;X-Auth&lt;/code&gt; so that at least your clients can continue using the standard header name.&lt;/li&gt;
&lt;li&gt;The client needs to sign the payload in PUT/POST methods. See this post by &lt;a href=&quot;https://speedrun.nobackspacecrew.com/blog/2024/05/22/using-cloudfront-as-a-lightweight-proxy.html#lambda-oac&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;David Behroozi&lt;/a&gt; for more details.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A simpler solution proposed by &lt;a href=&quot;https://x.com/ryan_sb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ryan Scott Brown&lt;/a&gt; is to have CloudFront add a custom origin header with a secret: &lt;code&gt;&amp;#39;X-CloudFront-Secret&amp;#39;: &amp;#39;NoBadGuysAllowed&amp;#39;&lt;/code&gt;. Your Lambda Function can check the secret in the header, and return an error code if it doesn&amp;#39;t match. Another solution proposed by &lt;a href=&quot;https://x.com/dreamorosi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Andrea Amorosi&lt;/a&gt; uses Lambda@Edge. Since an attacker would need to discover both the Lambda Function URL and the secret before successfully bypassing CloudFront, this solution is likely more than enough for most applications.&lt;/p&gt;
&lt;a href=&quot;https://x.com/ryan_sb/status/1835363459979960624&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
&lt;figure&gt;
  
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{&quot; margin:=&quot;&quot; color:=&quot;&quot; =&quot;}}&quot;&gt;
    Simpler solutions to locking down your Lambda FURL
  &lt;/figcaption&gt;
&lt;/figure&gt;
  &lt;/a&gt;

&lt;h2&gt;Replacing API Gateway Authorizer with in-Lambda JWT validation&lt;/h2&gt;
&lt;p&gt;The only &amp;quot;advanced&amp;quot; API Gateway feature I use on all of my APIs is the Cognito/JWT Authorizer. Since CloudFront doesn&amp;#39;t have a similar native feature, we need to perform the JWT validation inside the Lambda Function. This has the added bonus of making our app more portable and easier to run locally, while also granting us more flexibility.&lt;/p&gt;
&lt;figure&gt;
  
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{&quot; margin:=&quot;&quot; color:=&quot;&quot; =&quot;}}&quot;&gt;
    Express Middleware for JWT validation
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure&gt;
  
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{&quot; margin:=&quot;&quot; color:=&quot;&quot; =&quot;}}&quot;&gt;
    Cognito JWT Validation via aws-jwt-verify
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Performing Cognito JWT validation yourself is also incredibly fast (~4ms cold; ~0.3ms warm in Node.js) thanks to &lt;a href=&quot;https://x.com/AWSbrett/status/1779422735539847454&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;this tip by David Behroozi&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Another option is to use a &lt;a href=&quot;https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example-function-validate-token.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CloudFront Function to perform JWT validation&lt;/a&gt;. This has the benefit of rejecting the request before it makes it to your Lambda Function, at the cost of a little extra complexity and having to do some magic to get local development working.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;CloudFront + Lambda Function URL (CLFURL) is an excellent combination for Serverless APIs when you don&amp;#39;t need any of the advanced features offered by API Gateway. With CloudFront being an order of magnitude cheaper than API Gateway, it puts it more in line with what&amp;#39;d you&amp;#39;d expect when compared to the cost of other components of your Serverless architecture.&lt;/p&gt;
&lt;p&gt;The minor performance improvement compared to HTTP API is nice, but likely to be unnoticeable to the end user (compared to REST API may be a different story). On the other hand, the increase in the max timeout will surely be appreciated by developers looking to add Generative AI to their API.&lt;/p&gt;
&lt;p&gt;A repo is available at &lt;a href=&quot;https://github.com/CodeGenieApp/cloudfront-lambda-url-vs-apigw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/CodeGenieApp/cloudfront-lambda-url-vs-apigw&lt;/a&gt;. This repo was generated using &lt;a href=&quot;https://app.codegenie.codes/&quot;&gt;Code Genie&lt;/a&gt; and modified to include CloudFront + Lambda Function URL and benchmarks. Check out the commit history to find the interesting parts.&lt;/p&gt;
&lt;p&gt;Code Genie will soon offer CLFURL as an option when generating your source code, and will continue to support API Gateway HTTP APIs as well.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;ve found this blog useful, please give it a reshare! You can follow &lt;a href=&quot;https://x.com/AWSbrett&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;me&lt;/a&gt; and &lt;a href=&quot;https://x.com/CodeGenieCodes/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Code Genie&lt;/a&gt; on Twitter. Finally, if you&amp;#39;re building something new (or want to refresh something old), be sure to check out Code Genie. It takes care of all the boring parts of starting a new project so you can focus on what&amp;#39;s interesting!&lt;/p&gt;
</content:encoded><category>api-gateway</category><category>lambda</category><category>aws</category><category>cloudfront</category><category>serverless</category></item><item><title>Creating AWS CloudWatch Dashboards and Alarms with CDK</title><link>https://codegenie.codes/blog/creating-aws-cloudwatch-dashboards-and-alarms-with-cdk/</link><guid isPermaLink="true">https://codegenie.codes/blog/creating-aws-cloudwatch-dashboards-and-alarms-with-cdk/</guid><description>Use CDK to create CloudWatch Alarms and a Dashboard for a full stack application.

</description><pubDate>Fri, 12 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Image } from &amp;#39;astro:assets&amp;#39;
import { Tabs, TabItem } from &amp;#39;@astrojs/starlight/components&amp;#39;
import demoDashboard from &amp;#39;../../../assets/images/blog/dashboard-1440.webp&amp;#39;&lt;/p&gt;
&lt;p&gt;Monitoring is a crucial part of software operations, but can also be one of the most tedious to implement. If you&amp;#39;re using a traditional config-based IAC tool such as CloudFormation, SAM, Serverless, Terraform, et al. you&amp;#39;ll find yourself copy-pasting dozens of lines for each new metric that you want to graph and alert on.&lt;/p&gt;
&lt;p&gt;Modern IAC tools such as CDK enable you to define your infrastructure using actual code (often TypeScript) rather than config (JSON, YAML). I like to refer to these two approaches to IAC as IACode and IAConfig respecively.&lt;/p&gt;
&lt;p&gt;In this article we&amp;#39;ll cover how to build a &lt;a href=&quot;https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Dashboards.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CloudWatch Dashboard&lt;/a&gt; and &lt;a href=&quot;https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Alarms&lt;/a&gt; to monitor a Full Stack Serverless AWS application consisting of &lt;a href=&quot;https://aws.amazon.com/api-gateway&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;API Gateway&lt;/a&gt;, &lt;a href=&quot;https://aws.amazon.com/lambda&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Lambda&lt;/a&gt;, &lt;a href=&quot;https://aws.amazon.com/cognito&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cognito&lt;/a&gt;, &lt;a href=&quot;https://aws.amazon.com/dynamodb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DynamoDB&lt;/a&gt;, and a web app hosted on &lt;a href=&quot;https://aws.amazon.com/amplify/hosting&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Amplify Hosting&lt;/a&gt;. Here&amp;#39;s what the end result looks like (though you&amp;#39;ll need to use your imagination to fill in the empty charts with pretty lines).&lt;/p&gt;
&lt;figure&gt;
  
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{&quot; margin:=&quot;&quot; color:=&quot;&quot; =&quot;}}&quot;&gt;
    Example dashboard that would look so much more interesting if it had actual data.
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2&gt;Monitoring Construct&lt;/h2&gt;
&lt;p&gt;Enough preamble. Let&amp;#39;s get into the code! We&amp;#39;ll start by creating a &lt;code&gt;Monitoring&lt;/code&gt; construct that allows us to encapsulate all of the relevant infrastructure in a single place.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Construct } from &amp;#39;constructs&amp;#39;
import {
  Alarm,
  AlarmRule,
  AlarmState,
  AlarmStatusWidget,
  CompositeAlarm,
  Dashboard,
  GraphWidget,
  Metric,
  TextWidget,
  TextWidgetBackground,
} from &amp;#39;aws-cdk-lib/aws-cloudwatch&amp;#39;
import { IFunction } from &amp;#39;aws-cdk-lib/aws-lambda&amp;#39;
import { IHttpApi } from &amp;#39;aws-cdk-lib/aws-apigatewayv2&amp;#39;
import { IApp } from &amp;#39;@aws-cdk/aws-amplify-alpha&amp;#39;
import { ITableV2 } from &amp;#39;aws-cdk-lib/aws-dynamodb&amp;#39;
import { Topic } from &amp;#39;aws-cdk-lib/aws-sns&amp;#39;
import { EmailSubscription } from &amp;#39;aws-cdk-lib/aws-sns-subscriptions&amp;#39;
import { SnsAction } from &amp;#39;aws-cdk-lib/aws-cloudwatch-actions&amp;#39;

interface MonitoringProps {
  amplifyApp: IApp
  api: IHttpApi
  userPoolId: string
  userPoolClientId: string
  functions: Array&amp;lt;IFunction&amp;gt;
  tables: Array&amp;lt;ITableV2&amp;gt;
}

const ALARM_NOTIFICATION_EMAIL = &amp;#39;you@example.com&amp;#39;

export default class Monitoring extends Construct {
  public readonly dashboard: Dashboard
  public readonly alarmSnsTopic: Topic
  constructor(scope: Construct, id: string, props: MonitoringProps) {
    super(scope, id)
    this.alarmSnsTopic = new Topic(scope, &amp;#39;AlarmTopic&amp;#39;)
    this.alarmSnsTopic.addSubscription(new EmailSubscription(ALARM_NOTIFICATION_EMAIL))
    this.dashboard = new Dashboard(this, &amp;#39;Dashboard&amp;#39;, {
      start: &amp;#39;-P10D&amp;#39;,
    })

    const { alarms: apiAlarms } = this.addApiWidgets({ api: props.api })
    const { alarms: lambdaAlarms } = this.addLambdaWidgets({ functions: props.functions })
    this.addTableWidgets({ tables: props.tables })
    this.addCognitoWidgets({ userPoolId: props.userPoolId, userPoolClientId: props.userPoolClientId })
    this.addWebAppWidgets({ amplifyApp: props.amplifyApp })
    this.addAlarmStatusWidget({ alarms: [...apiAlarms, ...lambdaAlarms] })
  }

  addHeading(heading: string) {
    this.dashboard.addWidgets(
      new TextWidget({
        markdown: `# ${heading}`,
        width: 24,
        height: 1,
        background: TextWidgetBackground.TRANSPARENT,
      })
    )
  }

  subscribeAlarm(alarm: Alarm | CompositeAlarm) {
    alarm.addAlarmAction(new SnsAction(this.alarmSnsTopic))
  }
  /*
  ...
  */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Monitoring&lt;/code&gt; construct class accepts details on other resources within our app such as our Amplify Hosting App, API Gateway, Cognito User Pool and Client, and a list of Lambda Functions and DynamoDB Tables that we want to monitor.&lt;/p&gt;
&lt;p&gt;The constructor creates an SNS Topic that&amp;#39;s set up to send an email whenever it receives a message (i.e whenever an alarm is triggered). Make sure you replace &lt;code&gt;ALARM_NOTIFICATION_EMAIL&lt;/code&gt; with the email address you want to receive notifications.&lt;/p&gt;
&lt;p&gt;:::note[Notifications]
You can configure SNS to notify other services such as Slack or your Incident Response tool instead of email, however, that&amp;#39;s not the focus of this blog. For solo/hobby projects, email is fine.
:::&lt;/p&gt;
&lt;p&gt;We&amp;#39;re also creating the CloudWatch &lt;code&gt;Dashboard&lt;/code&gt; resource and calling methods to add widgets and alarms (we&amp;#39;ll get to the implementation of these next).&lt;/p&gt;
&lt;p&gt;Finally, we have some helper methods for adding headings to our Dashboard and subscribing alarms to our SNS Topic.&lt;/p&gt;
&lt;p&gt;Next we&amp;#39;ll get into creating metrics, alarms, and dashboard widgets for our API Gateway resource.&lt;/p&gt;
&lt;h2&gt;API Gateway Metrics, Alarms and Dashboard Widgets&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;addApiWidgets({ api }: { api: IHttpApi }) {
  this.addHeading(&amp;#39;API Gateway&amp;#39;)
  const clientErrorsMetric = api.metricClientError()
  const serverErrorsMetric = api.metricServerError()
  const clientErrorsAlarm = clientErrorsMetric.createAlarm(this, &amp;#39;ApiClientErrorsAlarm&amp;#39;, {
    evaluationPeriods: 3,
    threshold: 10,
  })
  this.subscribeAlarm(clientErrorsAlarm)
  const serverErrorsAlarm = serverErrorsMetric.createAlarm(this, &amp;#39;ApiServerErrorsAlarm&amp;#39;, {
    evaluationPeriods: 1,
    threshold: 1,
  })
  this.subscribeAlarm(serverErrorsAlarm)

  this.dashboard.addWidgets(
    new GraphWidget({
      width: 12,
      left: [
        clientErrorsMetric,
        serverErrorsMetric,
      ],
      leftAnnotations: [
        clientErrorsAlarm.toAnnotation(),
        serverErrorsAlarm.toAnnotation(),
      ],
    }),
    new GraphWidget({
      width: 12,
      left: [
        api.metricLatency(),
        api.metricIntegrationLatency(),
      ],
    }),
    new GraphWidget({
      width: 12,
      left: [
        api.metricCount(),
      ],
    }),
    new GraphWidget({
      width: 12,
      left: [
        api.metricDataProcessed(),
      ],
    }),
  )

  return {
    alarms: [serverErrorsAlarm, clientErrorsAlarm],
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Many CDK Constructs come with helper &lt;code&gt;metric*&lt;/code&gt; methods that return metrics for the most popular standard metrics of each AWS service. In API Gateway&amp;#39;s case, &lt;strong&gt;all&lt;/strong&gt; of the standard metrics we&amp;#39;re interested in have these helper metrics. Thank you CDK Team! 🙏&lt;/p&gt;
&lt;p&gt;We create alarms for both Client Errors and Server Errors. The thresholds are low and you should tweak them to meet your needs (maybe you don&amp;#39;t want to alarm on Client Errors at all since they&amp;#39;re often not actionable). We also add these error metrics to our dashboard and add &amp;quot;red lines&amp;quot; (horizontal annotations) so that we can see how close we get to breaching our alarms. Once again, the CDK Team shows great attention to detail by including a &lt;code&gt;toAnnotation()&lt;/code&gt; method on their alarms to make this super easy! 🙌&lt;/p&gt;
&lt;p&gt;:::tip[Short and Long alarms]
A best practice for creating alarms is to create two flavors of each alarm: a short/spikey alarm and a long/continuous alarm. This allows you to set two different thresholds for different scenarios. For example, you may want to alarm if your average latency breaches 3 seconds in a single 5 minute evaluation period, OR if it remains above 1 second for several evaluation periods. We didn&amp;#39;t include that here for brevity, however, CDK enables you to turn this into a repeatable pattern with ease.
:::&lt;/p&gt;
&lt;p&gt;We also add metrics for latency (consider adding alarms for this), number of requests, and amount of data processed.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s move onto Lambda.&lt;/p&gt;
&lt;h2&gt;Lambda Metrics, Alarms and Dashboard Widgets&lt;/h2&gt;
&lt;p&gt;The main difference with the Lambda metrics is that we&amp;#39;re dealing with &lt;strong&gt;&lt;em&gt;multiple&lt;/em&gt;&lt;/strong&gt; Lambda Functions (as opposed to a single API Gateway Resource). Each graph includes the metrics for all of the Lambda Functions passed into the Monitoring construct.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;addLambdaWidgets({ functions }: { functions: Array&amp;lt;IFunction&amp;gt; }) {
  this.addHeading(&amp;#39;Lambda Functions&amp;#39;)
  const errorMetrics = functions.map(fn =&amp;gt; ({ fn, metric: fn.metricErrors()}))
  const throttleMetrics = functions.map(fn =&amp;gt; ({ fn, metric: fn.metricThrottles()}))
  const errorAlarms = errorMetrics.map(errorMetric =&amp;gt; errorMetric.metric.createAlarm(this, `ErrorAlarm${errorMetric.fn.node.id}`, {
    evaluationPeriods: 1,
    threshold: 1,
  }))
  const throttleAlarms = throttleMetrics.map(throttleMetric =&amp;gt; throttleMetric.metric.createAlarm(this, `ThrottleAlarm${throttleMetric.fn.node.id}`, {
    evaluationPeriods: 1,
    threshold: 1,
  }))
  const errorCompositeAlarm = new CompositeAlarm(this, &amp;#39;ErrorCompositeAlarm&amp;#39;, {
    alarmRule: AlarmRule.anyOf(
      ...errorAlarms.map(errorAlarm =&amp;gt; AlarmRule.fromAlarm(errorAlarm, AlarmState.ALARM)),
      ...throttleAlarms.map(throttleAlarm =&amp;gt; AlarmRule.fromAlarm(throttleAlarm, AlarmState.ALARM)),
    ),
  })
  this.subscribeAlarm(errorCompositeAlarm)
  this.dashboard.addWidgets(
    new GraphWidget({
      width: 8,
      left: [
        // Alternatively, you could use `functions.map` to instead create one GraphWidget per function.
        ...functions.map(fn =&amp;gt; fn.metricDuration()),
        ...functions.map(fn =&amp;gt; fn.metricDuration({
          statistic: &amp;#39;P95&amp;#39;,
        })),
      ],
    }),
    new GraphWidget({
      width: 8,
      left: functions.map(fn =&amp;gt; fn.metricInvocations()),
    }),
    new GraphWidget({
      width: 8,
      left: errorMetrics.map(m =&amp;gt; m.metric),
      right: throttleMetrics.map(m =&amp;gt; m.metric),
      leftAnnotations: [errorAlarms[0].toAnnotation()],
    }),
  )

  return {
    alarms: [...errorAlarms, ...throttleAlarms],
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The metrics we&amp;#39;re graphing are duration (both average and P95), number of invocations, number of errors, and number of times a function was throttled. We&amp;#39;re only alarming on the error and throttle metrics here, though you should also consider alarming on duration.&lt;/p&gt;
&lt;p&gt;We&amp;#39;re creating 2 alarms per Lambda Function for throttle and error. These alarms aren&amp;#39;t set up to notify. Instead, we create a composite alarm so that we don&amp;#39;t get inundated with notifications when a single issue causes multiple functions to fail.&lt;/p&gt;
&lt;p&gt;:::caution[Pricing]
Alarms cost $0.10 per month (though you get 10 free) and the composite alarm costs $0.50 a month. If you don&amp;#39;t want to have N alarms per function, Lambda also emits standard metrics that cover ALL Lambda Functions within your account. This is a viable option if you&amp;#39;re following the best practice of only running a single App within an AWS Account.
:::&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://docs.aws.amazon.com/lambda/latest/dg/monitoring-metrics.html#monitoring-metrics-types&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Lambda Metrics Documentation&lt;/a&gt; for other available standard metrics.&lt;/p&gt;
&lt;h2&gt;DynamoDB Metrics and Dashboard Widgets&lt;/h2&gt;
&lt;p&gt;There&amp;#39;s nothing new to learn here that isn&amp;#39;t covered by the Lambda section. For each DynamoDB Table provided we&amp;#39;re graphing the number of consumed Read and Write Capacity Units, and the number of User Errors.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;addTableWidgets({ tables }: { tables: Array&amp;lt;ITableV2&amp;gt; }) {
  this.addHeading(&amp;#39;DynamoDB Tables&amp;#39;)
  this.dashboard.addWidgets(
    new GraphWidget({
      width: 8,
      left: tables.map(table =&amp;gt; table.metricConsumedReadCapacityUnits()),
    }),
    new GraphWidget({
      width: 8,
      left: tables.map(table =&amp;gt; table.metricConsumedWriteCapacityUnits()),
    }),
    new GraphWidget({
      width: 8,
      left: tables.map(table =&amp;gt; table.metricUserErrors()),
    }),
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Consider adding an alarm for the &lt;code&gt;UserErrors&lt;/code&gt; metric since it&amp;#39;s likely due to a bug in your code. If you&amp;#39;re not using DynamoDB&amp;#39;s On Demand billing, you should also consider adding alarms for RCUs and WCUs.&lt;/p&gt;
&lt;p&gt;DynamoDB offers significantly more standard metrics than most AWS Services, so you should definitely familiarize yourself with them and decide what&amp;#39;s important for you to graph and alarm on. But don&amp;#39;t go overboard! It&amp;#39;s important to find the right balance between alarming on too many things and not enough.&lt;/p&gt;
&lt;p&gt;:::tip
If it&amp;#39;s not actionable, don&amp;#39;t alarm on it.
:::&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/metrics-dimensions.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DynamoDB Metrics Documentation&lt;/a&gt; for other available standard metrics.&lt;/p&gt;
&lt;h2&gt;Cognito Metrics and Dashboard Widgets&lt;/h2&gt;
&lt;p&gt;Adding Cognito User Pools graphs gives great insights on the number of new and returning users. You may even be able to use the &lt;code&gt;TokenRefreshSuccesses&lt;/code&gt; metric to get a rough number of currently active users.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;addCognitoWidgets({ userPoolId, userPoolClientId }: { userPoolId: string, userPoolClientId: string }) {
  this.addHeading(&amp;#39;Cognito&amp;#39;)
  const dimensionsMap = {
    UserPool: userPoolId,
    UserPoolClient: userPoolClientId,
  }
  this.dashboard.addWidgets(
    new GraphWidget({
      width: 8,
      left: [new Metric({
        namespace: &amp;#39;AWS/Cognito&amp;#39;,
        metricName: &amp;#39;TokenRefreshSuccesses&amp;#39;,
        dimensionsMap,
      })],
    }),
    new GraphWidget({
      width: 8,
      left: [new Metric({
        namespace: &amp;#39;AWS/Cognito&amp;#39;,
        metricName: &amp;#39;SignUpSuccesses&amp;#39;,
        dimensionsMap,
      })],
    }),
    new GraphWidget({
      width: 8,
      left: [
        new Metric({
          namespace: &amp;#39;AWS/Cognito&amp;#39;,
          metricName: &amp;#39;SignInSuccesses&amp;#39;,
          dimensionsMap,
        }),
        new Metric({
          namespace: &amp;#39;AWS/Cognito&amp;#39;,
          metricName: &amp;#39;FederationSuccesses&amp;#39;,
          dimensionsMap,
        }),
      ],
    }),
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, the Cognito User Pool Construct doesn&amp;#39;t have those handy &lt;code&gt;metric*&lt;/code&gt; methods, so we need to manually create them using &lt;code&gt;new Metric&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;See the &lt;a href=&quot;https://docs.aws.amazon.com/cognito/latest/developerguide/metrics-for-cognito-user-pools.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cognito Metrics Documentation&lt;/a&gt; for other available standard metrics.&lt;/p&gt;
&lt;h2&gt;Amplify Hosting Metrics and Dashboard Widgets&lt;/h2&gt;
&lt;p&gt;I&amp;#39;ve encountered way too many people that don&amp;#39;t know about Amplify Hosting. If you need to host a web app on AWS: I highly recommend Amplify Hosting over the more traditional approach of S3 + CloudFront.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;addWebAppWidgets({ amplifyApp }: { amplifyApp: IApp }) {
  this.addHeading(&amp;#39;Web App&amp;#39;)
  const dimensionsMap = {
    App: amplifyApp.appId,
  }
  this.dashboard.addWidgets(
    new GraphWidget({
      width: 12,
      left: [new Metric({
        namespace: &amp;#39;AWS/AmplifyHosting&amp;#39;,
        metricName: &amp;#39;Requests&amp;#39;,
        dimensionsMap,
      })],
    }),
    new GraphWidget({
      width: 12,
      left: [new Metric({
        namespace: &amp;#39;AWS/AmplifyHosting&amp;#39;,
        metricName: &amp;#39;Latency&amp;#39;,
        dimensionsMap,
      })],
    }),
    new GraphWidget({
      width: 12,
      left: [new Metric({
        namespace: &amp;#39;AWS/AmplifyHosting&amp;#39;,
        metricName: &amp;#39;BytesDownloaded&amp;#39;,
        dimensionsMap,
      }),
      new Metric({
        namespace: &amp;#39;AWS/AmplifyHosting&amp;#39;,
        metricName: &amp;#39;BytesUploaded&amp;#39;,
        dimensionsMap,
      })],
    }),
    new GraphWidget({
      width: 12,
      left: [
        new Metric({
          namespace: &amp;#39;AWS/AmplifyHosting&amp;#39;,
          metricName: &amp;#39;4xxErrors&amp;#39;,
          dimensionsMap,
        }),
        new Metric({
          namespace: &amp;#39;AWS/AmplifyHosting&amp;#39;,
          metricName: &amp;#39;5xxErrors&amp;#39;,
          dimensionsMap,
        }),
      ],
    }),
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Alarm Status Dashboard Widget&lt;/h2&gt;
&lt;p&gt;Finally we&amp;#39;ll display a list of our alarms and their current status.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;addAlarmStatusWidget({ alarms }: { alarms: Array&amp;lt;Alarm&amp;gt; }) {
  this.addHeading(&amp;#39;Alarms&amp;#39;)
  const alarmStatusWidgetHeight = 1 + Math.ceil(alarms.length / 6)
  this.dashboard.addWidgets(new AlarmStatusWidget({
    alarms,
    width: 24,
    height: alarmStatusWidgetHeight,
  }))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should also check out the other &lt;a href=&quot;https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.ConcreteWidget.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CDK CloudWatch Dashboard Widgets&lt;/a&gt;. The &lt;code&gt;LogQueryWidget&lt;/code&gt; is especially useful for adding a quickview of logs that can be filtered using query insights (e.g. you can display the latest error logs).&lt;/p&gt;
&lt;h2&gt;Using the Construct&lt;/h2&gt;
&lt;p&gt;Now that we&amp;#39;ve created the &lt;code&gt;Monitoring&lt;/code&gt; construct, we can add it to our CDK stack by instantiating an instance and passing in details about our full stack application.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;new Monitoring(this, &amp;#39;Monitoring&amp;#39;, {
  userPoolId: userPool.userPoolId,
  userPoolClientId: userPoolClient.userPoolClientId,
  amplifyApp: amplifyApp,
  api: api,
  functions: [lambdaFunction1, lambdaFunction2],
  tables: [dynamoDbTable1, dynamoDbTable2],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;A note on laying out widgets&lt;/h2&gt;
&lt;p&gt;CloudWatch Dashboards are rendered as a 24 column layout. Keep this in mind when deciding on the width of each widget.&lt;/p&gt;
&lt;p&gt;According to the docs, every widget included in a single &lt;code&gt;addWidgets&lt;/code&gt; is rendered next to each other, and every new call to &lt;code&gt;addWidgets&lt;/code&gt; creates a new row. In practice, I found that when my widgets within a single &lt;code&gt;addWidgets&lt;/code&gt; didn&amp;#39;t add up to a multiple of 24, the layout started to go a little wonky, and widgets that I expected to be rendered in a new row (because they were in a separate &lt;code&gt;addWidgets&lt;/code&gt;) were not.&lt;/p&gt;
&lt;p&gt;There are &lt;code&gt;Row&lt;/code&gt; and &lt;code&gt;Column&lt;/code&gt; constructs that can also give you more control over layout.&lt;/p&gt;
&lt;h2&gt;Custom Metrics&lt;/h2&gt;
&lt;p&gt;Graphing and alarming on the standard metrics that AWS provides us with out-of-the-box is only half the story. To improve your &amp;quot;operational excellence&amp;quot; you&amp;#39;ll want to add &lt;a href=&quot;https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Custom Metrics&lt;/a&gt; relevant to your business requirements.&lt;/p&gt;
&lt;h2&gt;Try it yourself with Code Genie&lt;/h2&gt;
&lt;p&gt;Building a full stack Serverless app on AWS takes a lot of time, effort, and expertise. Code Genie lets you get up and running fast with a solid software foundation based on your data model. In minutes you can have a full stack application deployed to your own AWS account, and the source code downloaded for you to start hacking. Metrics, Monitoring, and a Dashboard like the one described in this article is included out-of-the-box. Check out the &lt;a href=&quot;/docs/guides/getting-started&quot;&gt;Getting Started Guide&lt;/a&gt; for more details.&lt;/p&gt;
</content:encoded><category>cloudwatch</category><category>cdk</category><category>aws</category><category>iac</category></item><item><title>AWS Cognito User Pools: Add Auth to a React App</title><link>https://codegenie.codes/blog/aws-cognito-user-pools-add-auth-to-a-react-app/</link><guid isPermaLink="true">https://codegenie.codes/blog/aws-cognito-user-pools-add-auth-to-a-react-app/</guid><description>Use Amplify JS Auth module to create all of the pages and hooks required to implement a complete Cognuito auth flow.

</description><pubDate>Sat, 02 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Image } from &amp;#39;astro:assets&amp;#39;;
import { Tabs, TabItem } from &amp;#39;@astrojs/starlight/components&amp;#39;
import signInPage from &amp;#39;../../../assets/images/blog/aws-cognito-google-sso-saml-linked-accounts/cognito-example-sign-in.webp&amp;#39;;&lt;/p&gt;
&lt;figure style=&quot;{{margin:&quot; =&quot;&apos;360px&apos;}}&quot; auto=&quot;&apos;,&quot; width:=&quot;&quot;&gt;
  
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{margin:&quot; color:=&quot;&quot; =&quot;&apos;0.8rem&apos;}}&quot; fontSize:=&quot;&quot;&gt;Pretty picture of the Login UI.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/cognito/&quot;&gt;Cognito User Pools&lt;/a&gt; is an AWS service that provides applications with user identity and auth. This series of articles cover a full stack solution that enables users to sign in with their Email + Password, Google Sign In, or SSO (SAML), and link all methods to the same user within the app:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/blog/aws-cognito-user-pools-create-resources-with-cdk&quot;&gt;Create Resources with CDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/aws-cognito-user-pools-sign-in-with-email-google-saml-and-link-to-a-single-user&quot;&gt;Sign in with Email, Google, or SAML and link to a single user&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Add Auth to a React App 👈 You are here&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This article focuses on integrating Cognito User Pools with a &lt;a href=&quot;https://react.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;React&lt;/a&gt; (&lt;a href=&quot;https://nextjs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Next.js&lt;/a&gt;) Web App, though the fundamentals remain the same for all frameworks. The example also uses the &lt;a href=&quot;https://ant.design/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ant Design UI Framework&lt;/a&gt; to make everything look good.&lt;/p&gt;
&lt;p&gt;:::tip
The complete source code is available by generating a Code Genie app and specifying &lt;code&gt;&amp;#39;Google&amp;#39;&lt;/code&gt; and &lt;code&gt;&amp;#39;SAML&amp;#39;&lt;/code&gt; &lt;code&gt;idp&lt;/code&gt; options:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npx @codegenie/cli generate --name &amp;quot;Todo&amp;quot; \
--idp &amp;#39;Google&amp;#39; --idp &amp;#39;SAML&amp;#39; \
--description &amp;quot;A todo list app that lets users create lists and add items to the list. Items should have a title, description, be markable as completed, have a due date, and have an image.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://codegenie.codes&quot;&gt;Code Genie&lt;/a&gt; generates a Full Stack AWS Serverless application based on your own data model, including a React (Next.js) Web App, Serverless Express REST API, DynamoDB Database, and Cognito Auth.
:::&lt;/p&gt;
&lt;h2&gt;React Cognito Auth&lt;/h2&gt;
&lt;p&gt;To add Cognito Auth to our React web app (specifically Next.js in this case)  we&amp;#39;ll use the &lt;a href=&quot;https://docs.amplify.aws/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Amplify JavaScript Library&lt;/a&gt;, which simplifies the complex &lt;a href=&quot;https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OAuth flow&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The work required boils down to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Configuring the Amplify library with details about our Cognito User Pool&lt;/li&gt;
&lt;li&gt;Configuring Axios with the base URL of our API endpoint, and attaching the Authorizers header on each API request.&lt;/li&gt;
&lt;li&gt;Adding pages for Login, Register, Verify, Forgot Password, Reset Password. See &lt;a href=&quot;/docs/project-walkthrough/frontend/pages&quot;&gt;Code Genie pages docs&lt;/a&gt; for more details.&lt;/li&gt;
&lt;li&gt;Wrapping each of the Amplify methods we use with &lt;a href=&quot;https://tanstack.com/query/latest/docs/react/guides/queries&quot;&gt;TanStack Query&lt;/a&gt; &lt;code&gt;useQuery&lt;/code&gt; hooks for improved state management. See &lt;a href=&quot;/docs/project-walkthrough/frontend/hooks&quot;&gt;Code Genie hooks docs&lt;/a&gt; for more details.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;_app.tsx&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Amplify } from &amp;#39;aws-amplify&amp;#39;
import { fetchAuthSession } from &amp;#39;aws-amplify/auth&amp;#39;
import axios from &amp;#39;axios&amp;#39;

const oauthRedirectUrl = [&amp;#39;http://localhost:3001/&amp;#39;]

if (global.window?.location.origin) oauthRedirectUrl.push(global.window.location.origin)

Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId: process.env.NEXT_PUBLIC_CognitoUserPoolId!,
      userPoolClientId: process.env.NEXT_PUBLIC_CognitoUserPoolClientId!,
      loginWith: {
        oauth: {
          redirectSignIn: oauthRedirectUrl,
          redirectSignOut: oauthRedirectUrl,
          // replace &amp;#39;example&amp;#39; with the regionallyUniqueDomainPrefix value defined in CDK
          domain: &amp;#39;example.auth.us-west-2.amazoncognito.com&amp;#39;,
          responseType: &amp;#39;code&amp;#39;,
          scopes: [&amp;#39;aws.cognito.signin.user.admin&amp;#39;, &amp;#39;email&amp;#39;, &amp;#39;openid&amp;#39;, &amp;#39;phone&amp;#39;, &amp;#39;profile&amp;#39;],
        },
      },
    },
  },
}, {
  ssr: false,
})

axios.defaults.baseURL = process.env.NEXT_PUBLIC_ApiEndpoint

// Set Authorization header on all requests if user is signed in; othwerise, redirect to login page
axios.interceptors.request.use(async (config) =&amp;gt; {
  try {
    const authSession = await fetchAuthSession()
    config.headers.Authorization = authSession.tokens?.idToken?.toString()
  } catch (e) {
    const redirectRoute = getRedirectToLoginPageUrl()
    global.window.location.href = redirectRoute
  }

  return config
})
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Login Page&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React, { useEffect, useRef } from &amp;#39;react&amp;#39;
import {
  Button,
  Checkbox,
  Divider,
  Form,
  Input,
  InputRef,
  Typography,
} from &amp;#39;antd&amp;#39;
import Link from &amp;#39;next/link&amp;#39;
import UnauthenticatedPage from &amp;#39;../components/layouts/UnauthenticatedPage&amp;#39;
import { useSignInMutation, useSignInWithGoogleMutation, useSignInWithSsoMutation } from &amp;#39;../components/Me/meHooks&amp;#39;
import { useRouter } from &amp;#39;next/router&amp;#39;
import { GoogleOutlined, LockOutlined } from &amp;#39;@ant-design/icons&amp;#39;

export default function LoginPage() {
  const [form] = Form.useForm()
  const passwordInputRef = useRef&amp;lt;InputRef&amp;gt;(null)
  const signInMutation = useSignInMutation()
  const signInWithGoogleMutation = useSignInWithGoogleMutation()
  const signInWithSsoMutation = useSignInWithSsoMutation()
  const router = useRouter()
  const queryParamEmail = (router.query.email as string) || &amp;#39;&amp;#39;
  const isSigningIn = signInMutation.isLoading || signInWithGoogleMutation.isLoading || signInWithSsoMutation.isLoading

  useEffect(() =&amp;gt; {
    if (queryParamEmail) {
      form.setFieldsValue({
        email: queryParamEmail,
      })
      passwordInputRef.current?.focus()
    }
  }, [queryParamEmail])

  return (
    &amp;lt;UnauthenticatedPage pageTitle=&amp;#39;Login&amp;#39;&amp;gt;
      &amp;lt;div style={{display: &amp;#39;flex&amp;#39;, justifyContent: &amp;#39;space-between&amp;#39;}}&amp;gt;
        &amp;lt;Button
          icon={&amp;lt;GoogleOutlined /&amp;gt;}
          disabled={isSigningIn}
          loading={signInWithGoogleMutation.isLoading}
          type=&amp;#39;primary&amp;#39;
          onClick={() =&amp;gt; signInWithGoogleMutation.mutate()}
        &amp;gt;
          Sign in with Google
        &amp;lt;/Button&amp;gt;
        &amp;lt;Button
          icon={&amp;lt;LockOutlined /&amp;gt;}
          disabled={isSigningIn}
          loading={signInWithSsoMutation.isLoading}
          onClick={() =&amp;gt; signInWithSsoMutation.mutate()}
        &amp;gt;
          SSO
        &amp;lt;/Button&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;Divider&amp;gt;&amp;lt;Typography.Text type=&amp;#39;secondary&amp;#39; italic&amp;gt;or sign in with email&amp;lt;/Typography.Text&amp;gt;&amp;lt;/Divider&amp;gt;
      &amp;lt;Form
        layout=&amp;#39;vertical&amp;#39;
        name=&amp;#39;login_form&amp;#39;
        initialValues={{ remember: true }}
        onFinish={signInMutation.mutate}
        form={form}
        disabled={isSigningIn}
      &amp;gt;
        &amp;lt;Form.Item
          label=&amp;#39;Email&amp;#39;
          name=&amp;#39;email&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Email is required.&amp;#39;,
              type: &amp;#39;email&amp;#39;,
            },
          ]}
        &amp;gt;
          &amp;lt;Input type=&amp;#39;email&amp;#39; /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item
          label=&amp;#39;Password&amp;#39;
          name=&amp;#39;password&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Password is required.&amp;#39;,
            },
          ]}
        &amp;gt;
          &amp;lt;Input.Password ref={passwordInputRef} /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item&amp;gt;
          &amp;lt;Form.Item name=&amp;#39;remember&amp;#39; valuePropName=&amp;#39;checked&amp;#39; noStyle&amp;gt;
            &amp;lt;Checkbox&amp;gt;Remember me&amp;lt;/Checkbox&amp;gt;
          &amp;lt;/Form.Item&amp;gt;
          &amp;lt;Button
            loading={signInMutation.isLoading}
            style={{ float: &amp;#39;right&amp;#39; }}
            type=&amp;#39;primary&amp;#39;
            htmlType=&amp;#39;submit&amp;#39;
          &amp;gt;
            Sign in
          &amp;lt;/Button&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;div style={{ display: &amp;#39;flex&amp;#39;, justifyContent: &amp;#39;space-between&amp;#39; }}&amp;gt;
          &amp;lt;Link href=&amp;#39;/register&amp;#39;&amp;gt;Register&amp;lt;/Link&amp;gt;
          &amp;lt;Link href=&amp;#39;/forgot-password&amp;#39;&amp;gt;Forgot your password?&amp;lt;/Link&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/Form&amp;gt;
    &amp;lt;/UnauthenticatedPage&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Register Page&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &amp;#39;react&amp;#39;
import {
  Button,
  Form,
  Input,
} from &amp;#39;antd&amp;#39;
import Link from &amp;#39;next/link&amp;#39;
import UnauthenticatedPage from &amp;#39;../components/layouts/UnauthenticatedPage&amp;#39;
import { useSignUpMutation } from &amp;#39;../components/Me/meHooks&amp;#39;

export default function App() {
  const signUpMutation = useSignUpMutation()

  return (
    &amp;lt;UnauthenticatedPage pageTitle=&amp;#39;Register&amp;#39;&amp;gt;
      &amp;lt;Form
        layout=&amp;#39;vertical&amp;#39;
        name=&amp;#39;register_form&amp;#39;
        onFinish={signUpMutation.mutate}
        validateTrigger=&amp;#39;onBlur&amp;#39;
      &amp;gt;
        &amp;lt;Form.Item
          label=&amp;#39;Name&amp;#39;
          name=&amp;#39;name&amp;#39;
          required={false}
          rules={[
            { required: true, message: &amp;#39;Please enter your name.&amp;#39; },
          ]}
        &amp;gt;
          &amp;lt;Input /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item
          label=&amp;#39;Email&amp;#39;
          name=&amp;#39;email&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Please enter your email.&amp;#39;,
              type: &amp;#39;email&amp;#39;,
            },
          ]}
        &amp;gt;
          &amp;lt;Input type=&amp;#39;email&amp;#39; /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item
          style={{ marginBottom: &amp;#39;10px&amp;#39; }}
          label=&amp;#39;Password&amp;#39;
          name=&amp;#39;password&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Please enter your password.&amp;#39;,
            },
          ]}
        &amp;gt;
          &amp;lt;Input.Password /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item style={{ margin: &amp;#39;0&amp;#39; }}&amp;gt;
          &amp;lt;div style={{ display: &amp;#39;flex&amp;#39;, justifyContent: &amp;#39;flex-end&amp;#39; }}&amp;gt;
            &amp;lt;Button style={{ marginRight: &amp;#39;1rem&amp;#39; }}&amp;gt;
              &amp;lt;Link href=&amp;#39;/&amp;#39;&amp;gt;Back to login&amp;lt;/Link&amp;gt;
            &amp;lt;/Button&amp;gt;
            &amp;lt;Button
              type=&amp;#39;primary&amp;#39;
              disabled={signUpMutation.isLoading}
              loading={signUpMutation.isLoading}
              htmlType=&amp;#39;submit&amp;#39;
              className=&amp;#39;login-form-button&amp;#39;
            &amp;gt;
              Register
            &amp;lt;/Button&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
      &amp;lt;/Form&amp;gt;
    &amp;lt;/UnauthenticatedPage&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Verify Page&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React, { useEffect } from &amp;#39;react&amp;#39;
import {
  Button,
  Typography,
  Form,
  Input,
} from &amp;#39;antd&amp;#39;
import { useRouter } from &amp;#39;next/router&amp;#39;
import Link from &amp;#39;next/link&amp;#39;
import UnauthenticatedPage from &amp;#39;../components/layouts/UnauthenticatedPage&amp;#39;
import { useVerifyAccountMutation } from &amp;#39;../components/Me/meHooks&amp;#39;

const { Title } = Typography

export default function VerifyPage() {
  const [form] = Form.useForm()
  const router = useRouter()
  const verifyAccountMutation = useVerifyAccountMutation()
  const queryParamEmail = (router.query.email as string) || &amp;#39;&amp;#39;
  const queryParamCode = (router.query.code as string) || &amp;#39;&amp;#39;

  useEffect(() =&amp;gt; {
    if (queryParamEmail &amp;amp;&amp;amp; queryParamCode) {
      verifyAccountMutation.mutate({ email: queryParamEmail, code: queryParamCode })
    }
    form.setFieldsValue({
      email: queryParamEmail,
      code: queryParamCode,
    })
  }, [queryParamEmail, queryParamCode])

  return (
    &amp;lt;UnauthenticatedPage pageTitle=&amp;#39;Verify Account&amp;#39;&amp;gt;
      &amp;lt;Title level={3}&amp;gt;Verify your account&amp;lt;/Title&amp;gt;
      {queryParamCode ? null : &amp;lt;p&amp;gt;Check your inbox for a verification email that includes a verification code, and enter it here. Alternatively, simply click the link in the email.&amp;lt;/p&amp;gt;}
      &amp;lt;Form
        layout=&amp;#39;vertical&amp;#39;
        name=&amp;#39;verifyAccountForm&amp;#39;
        form={form}
        onFinish={verifyAccountMutation.mutate}
      &amp;gt;
        &amp;lt;Form.Item
          label=&amp;#39;Email&amp;#39;
          name=&amp;#39;email&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Email is required.&amp;#39;,
              type: &amp;#39;email&amp;#39;,
            },
          ]}
        &amp;gt;
          &amp;lt;Input type=&amp;#39;email&amp;#39; /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item
          label=&amp;#39;Verification Code&amp;#39;
          name=&amp;#39;code&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Code is required.&amp;#39;,
              max: 6,
            },
          ]}
        &amp;gt;
          &amp;lt;Input /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item style={{ margin: &amp;#39;0&amp;#39; }}&amp;gt;
          &amp;lt;div style={{ display: &amp;#39;flex&amp;#39;, justifyContent: &amp;#39;flex-end&amp;#39; }}&amp;gt;
            &amp;lt;Button style={{ marginRight: &amp;#39;1rem&amp;#39; }}&amp;gt;
              &amp;lt;Link href=&amp;#39;/&amp;#39;&amp;gt;Back to login&amp;lt;/Link&amp;gt;
            &amp;lt;/Button&amp;gt;
            &amp;lt;Button
              type=&amp;#39;primary&amp;#39;
              disabled={verifyAccountMutation.isLoading}
              loading={verifyAccountMutation.isLoading}
              htmlType=&amp;#39;submit&amp;#39;
              className=&amp;#39;verify-account-form-button&amp;#39;
            &amp;gt;
              Verify Account
            &amp;lt;/Button&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
      &amp;lt;/Form&amp;gt;
    &amp;lt;/UnauthenticatedPage&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Forgot Password Page&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &amp;#39;react&amp;#39;
import {
  Button,
  Typography,
  Form,
  Input,
} from &amp;#39;antd&amp;#39;
import Link from &amp;#39;next/link&amp;#39;
import UnauthenticatedPage from &amp;#39;../components/layouts/UnauthenticatedPage&amp;#39;
import { useForgotPasswordMutation } from &amp;#39;../components/Me/meHooks&amp;#39;

const { Title } = Typography

export default function ForgotPassword() {
  const forgotPasswordMutation = useForgotPasswordMutation()

  return (
    &amp;lt;UnauthenticatedPage pageTitle=&amp;#39;Forgot Password&amp;#39;&amp;gt;
      &amp;lt;Title level={3}&amp;gt;Forgot your password?&amp;lt;/Title&amp;gt;
      &amp;lt;Form
        layout=&amp;#39;vertical&amp;#39;
        name=&amp;#39;forgotPasswordForm&amp;#39;
        onFinish={forgotPasswordMutation.mutate}
      &amp;gt;
        &amp;lt;Form.Item
          label=&amp;#39;Email&amp;#39;
          name=&amp;#39;email&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Please enter your email.&amp;#39;,
              type: &amp;#39;email&amp;#39;,
            },
          ]}
        &amp;gt;
          &amp;lt;Input type=&amp;#39;email&amp;#39; /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item style={{ margin: &amp;#39;0&amp;#39; }}&amp;gt;
          &amp;lt;div style={{ display: &amp;#39;flex&amp;#39;, justifyContent: &amp;#39;flex-end&amp;#39; }}&amp;gt;
            &amp;lt;Button style={{ marginRight: &amp;#39;1rem&amp;#39; }}&amp;gt;
              &amp;lt;Link href=&amp;#39;/&amp;#39;&amp;gt;Back to login&amp;lt;/Link&amp;gt;
            &amp;lt;/Button&amp;gt;
            &amp;lt;Button
              type=&amp;#39;primary&amp;#39;
              disabled={forgotPasswordMutation.isLoading}
              loading={forgotPasswordMutation.isLoading}
              htmlType=&amp;#39;submit&amp;#39;
              className=&amp;#39;forgot-password-form-button&amp;#39;
            &amp;gt;
              Reset Password
            &amp;lt;/Button&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
      &amp;lt;/Form&amp;gt;
    &amp;lt;/UnauthenticatedPage&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reset Password Page&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React, { useEffect } from &amp;#39;react&amp;#39;
import {
  Button,
  Typography,
  Form,
  Input,
} from &amp;#39;antd&amp;#39;
import { useRouter } from &amp;#39;next/router&amp;#39;
import Link from &amp;#39;next/link&amp;#39;
import UnauthenticatedPage from &amp;#39;../components/layouts/UnauthenticatedPage&amp;#39;
import { useResetPasswordMutation } from &amp;#39;../components/Me/meHooks&amp;#39;

const { Title } = Typography

export default function RestPassword() {
  const [form] = Form.useForm()
  const router = useRouter()
  const queryParamEmail = (router.query.email as string) || &amp;#39;&amp;#39;
  useEffect(() =&amp;gt; {
    form.setFieldsValue({
      email: queryParamEmail,
    })
  }, [queryParamEmail])
  const resetPasswordMutation = useResetPasswordMutation()

  return (
    &amp;lt;UnauthenticatedPage pageTitle=&amp;#39;Reset Password&amp;#39;&amp;gt;
      &amp;lt;Title level={3}&amp;gt;Reset your password&amp;lt;/Title&amp;gt;
      &amp;lt;Form
        layout=&amp;#39;vertical&amp;#39;
        name=&amp;#39;reset_password_form&amp;#39;
        form={form}
        onFinish={resetPasswordMutation.mutate}
      &amp;gt;
        &amp;lt;Form.Item
          label=&amp;#39;Email&amp;#39;
          name=&amp;#39;email&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Please enter your email.&amp;#39;,
              type: &amp;#39;email&amp;#39;,
            },
          ]}
        &amp;gt;
          &amp;lt;Input type=&amp;#39;email&amp;#39; /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item
          label=&amp;#39;Reset code&amp;#39;
          name=&amp;#39;code&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Please enter your reset code.&amp;#39;,
              max: 6,
            },
          ]}
        &amp;gt;
          &amp;lt;Input /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item
          style={{ marginBottom: &amp;#39;10px&amp;#39; }}
          label=&amp;#39;New Password&amp;#39;
          name=&amp;#39;password&amp;#39;
          required={false}
          rules={[
            {
              required: true,
              message: &amp;#39;Please enter your password.&amp;#39;,
            },
          ]}
        &amp;gt;
          &amp;lt;Input.Password autoComplete=&amp;#39;new-password&amp;#39; /&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
        &amp;lt;Form.Item style={{ margin: &amp;#39;0&amp;#39; }}&amp;gt;
          &amp;lt;div style={{ display: &amp;#39;flex&amp;#39;, justifyContent: &amp;#39;flex-end&amp;#39; }}&amp;gt;
            &amp;lt;Button style={{ marginRight: &amp;#39;1rem&amp;#39; }}&amp;gt;
              &amp;lt;Link href=&amp;#39;/&amp;#39;&amp;gt;Back to login&amp;lt;/Link&amp;gt;
            &amp;lt;/Button&amp;gt;
            &amp;lt;Button
              type=&amp;#39;primary&amp;#39;
              disabled={resetPasswordMutation.isLoading}
              loading={resetPasswordMutation.isLoading}
              htmlType=&amp;#39;submit&amp;#39;
              className=&amp;#39;forgot-password-form-button&amp;#39;
            &amp;gt;
              Reset Password
            &amp;lt;/Button&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/Form.Item&amp;gt;
      &amp;lt;/Form&amp;gt;
    &amp;lt;/UnauthenticatedPage&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hooks&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;&amp;#39;use client&amp;#39;

import axios from &amp;#39;axios&amp;#39;
import { useQuery, useMutation, useQueryClient } from &amp;#39;@tanstack/react-query&amp;#39;
import {
  confirmResetPassword,
  confirmSignUp,
  getCurrentUser,
  resetPassword,
  signIn,
  signInWithRedirect,
  signOut,
  signUp,
} from &amp;#39;aws-amplify/auth&amp;#39;
import { useRouter } from &amp;#39;next/router&amp;#39;
import { notification } from &amp;#39;antd&amp;#39;

const api = {
  getMe: () =&amp;gt; axios.get(&amp;#39;/me&amp;#39;),
  updateMe: ({ data }) =&amp;gt; axios.put(&amp;#39;/me&amp;#39;, { me: data }),
}

export function useCurrentUserQuery({ redirectOnNotAuth = true } = {}) {
  const router = useRouter()
  const currentUserQuery = useQuery([&amp;#39;currentUser&amp;#39;], async () =&amp;gt; {
    try {
      const currentAuthenticatedUser = await getCurrentUser()
      return currentAuthenticatedUser
    } catch (error) {
      if (redirectOnNotAuth) {
        router.push(&amp;#39;/&amp;#39;)
      }
      return null
    }
  }, {
    retry: false,
  })
  return currentUserQuery
}

export function useSignInWithGoogleMutation() {
  const signInWithGoogleMutation = useMutation(() =&amp;gt; {
    signInWithRedirect({ provider: &amp;#39;Google&amp;#39; })
    return new Promise(() =&amp;gt; null)
  },
  {
    onError: (err: Error) =&amp;gt; {
      notification.error({
        message: &amp;#39;Sign in with Google failed&amp;#39;,
        description: err.message,
        placement: &amp;#39;topRight&amp;#39;,
      })
    },
  })

  return signInWithGoogleMutation
}

export function useSignInWithSsoMutation() {
  const signInWithSsoMutation = useMutation(() =&amp;gt; {
    signInWithRedirect({
      provider: {
        custom: process.env.NEXT_PUBLIC_GoogleSamlIdpName!,
      },
    })
    return new Promise(() =&amp;gt; null)
  },
  {
    onError: (err: Error) =&amp;gt; {
      notification.error({
        message: &amp;#39;Sign in with SSO failed&amp;#39;,
        description: err.message,
        placement: &amp;#39;topRight&amp;#39;,
      })
    },
  })

  return signInWithSsoMutation
}

export function useSignInMutation() {
  const currentUserQuery = useCurrentUserQuery({ redirectOnNotAuth: false })
  const signInMutation = useMutation(async ({ email, password }: any) =&amp;gt; {
    await signIn({ username: email, password })
    await currentUserQuery.refetch()
  },
  {
    onError: (err: Error) =&amp;gt; {
      notification.error({
        message: &amp;#39;Login failed&amp;#39;,
        description: err.message,
        placement: &amp;#39;topRight&amp;#39;,
      })
    },
  })

  return signInMutation
}

export function useSignUpMutation() {
  const signInMutation = useSignInMutation()
  const router = useRouter()
  const signUpMutation = useMutation(async ({ name, password, email }: any) =&amp;gt; {
    await signUp({
      username: email,
      password,
      options: {
        userAttributes: { email, name },
      },
    })

    if (process.env.NEXT_PUBLIC_AUTO_VERIFY_USERS) {
      await signInMutation.mutateAsync({ email, password })
      router.push(&amp;#39;/posts&amp;#39;)
      return
    }

    router.push(`/verify?email=${encodeURIComponent(email)}`)
  },
  {
    onError: async (err: Error) =&amp;gt; notification.error({
      message: &amp;#39;Error&amp;#39;,
      description: err.message,
      placement: &amp;#39;topRight&amp;#39;,
    }),
  })

  return signUpMutation
}

export function useSignOutMutation({ includeEmailQueryStringParam = false } = {}) {
  const queryClient = useQueryClient()
  const currentUserQuery = useCurrentUserQuery({ redirectOnNotAuth: false })
  const signOutMutation = useMutation(async () =&amp;gt; {
    try {
      await signOut({ global: true })
    } catch (error: any) {
      notification.error({
        message: &amp;#39;Error trying to logout&amp;#39;,
        description: error.message,
        placement: &amp;#39;topRight&amp;#39;,
      })
    } finally {
      queryClient.cancelQueries()
      queryClient.clear()
      queryClient.invalidateQueries()
      queryClient.removeQueries()
      window.localStorage.clear()
      const signInRoute = includeEmailQueryStringParam ? `/?${currentUserQuery.data?.username}` : &amp;#39;/&amp;#39;
      global.window.location.href = signInRoute
    }
  })

  return signOutMutation
}

export function useMeQuery({ isAuthenticated = true } = {}) {
  const meQuery = useQuery([&amp;#39;me&amp;#39;], async () =&amp;gt; {
    const apiResponse = await api.getMe()
    return apiResponse.data
  }, { retry: false, enabled: isAuthenticated })
  return meQuery
}

export function useUpdateMeMutation() {
  const queryClient = useQueryClient()
  const updateMeMutation = useMutation&amp;lt;any, any, any&amp;gt;(async ({ userId, data }) =&amp;gt; {
    try {
      const response = await api.updateMe({ data })

      await Promise.all([
        queryClient.invalidateQueries([&amp;#39;me&amp;#39;]),
      ])

      return response
    } catch (error: any) {
      notification.error({
        message: &amp;#39;Update failed&amp;#39;,
        description: error?.response?.data?.message || error?.message || &amp;#39;Unknown error&amp;#39;,
        placement: &amp;#39;topRight&amp;#39;,
      })
    }
  })

  return updateMeMutation
}

export function useForgotPasswordMutation() {
  const router = useRouter()
  const forgotPasswordMutation = useMutation(
    async ({ email }: { email: string }) =&amp;gt; {
      await resetPassword({ username: email })
      notification.success({
        message: &amp;#39;Password reset link sent&amp;#39;,
        description: &amp;#39;Instructions have been sent to your email.&amp;#39;,
        placement: &amp;#39;topRight&amp;#39;,
      })
      await router.push(`/reset-password?email=${email}`)
    },
    {
      onError: async (err: Error) =&amp;gt; {
        notification.error({
          message: &amp;#39;Forgot password failed&amp;#39;,
          description: err.message,
          placement: &amp;#39;topRight&amp;#39;,
        })
      },
    },
  )

  return forgotPasswordMutation
}

export function useResetPasswordMutation() {
  const signInMutation = useSignInMutation()
  const router = useRouter()
  const resetPasswordMutation = useMutation(async ({ email, code, password }: { email: string, code: string, password: string }) =&amp;gt; {
    await confirmResetPassword({
      username: email.trim(),
      confirmationCode: code.trim(),
      newPassword: password.trim(),
    })
    await signInMutation.mutateAsync({ email, password })
    router.push(&amp;#39;/&amp;#39;)
  },
  {
    onError: async (err: Error) =&amp;gt; notification.error({
      message: &amp;#39;Error resetting password&amp;#39;,
      description: err.message,
      placement: &amp;#39;topRight&amp;#39;,
    }),
  })

  return resetPasswordMutation
}

export function useVerifyAccountMutation() {
  const router = useRouter()
  const verifyAccountMutation = useMutation(async ({ email, code }: { email: string, code: string }) =&amp;gt; {
    await confirmSignUp({
      username: email.trim(),
      confirmationCode: code.trim(),
    })
    notification.success({
      message: &amp;#39;Account confirmed! 🙌&amp;#39;,
      description: &amp;#39;You may now sign in.&amp;#39;,
      placement: &amp;#39;topRight&amp;#39;,
    }),
    router.push(`/?email=${encodeURIComponent(email)}`)
  },
  {
    onError: async (err: Error) =&amp;gt; notification.error({
      message: &amp;#39;Error confirming account&amp;#39;,
      description: err.message,
      placement: &amp;#39;topRight&amp;#39;,
    }),
  })

  return verifyAccountMutation
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Custom Verify User Email&lt;/h2&gt;
&lt;p&gt;While not strictly part of the React implementation, we need to use a custom Verify User email so that we can link to the correct page and prefill the email and code. Modify the CDK from the first article in this series with this &lt;code&gt;addCustomMessageTrigger&lt;/code&gt; method and Lambda Function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export const handler = async (event, context) =&amp;gt; {
  if (event.triggerSource === &amp;#39;CustomMessage_SignUp&amp;#39;) {
    return handleVerifyUserEmail(event)
  }

  return event
}

function handleVerifyUserEmail(event) {
  const { codeParameter, userAttributes } = event.request
  const verifyLink = `https://example.com/verify?email=${encodeURIComponent(userAttributes.email)}&amp;amp;code=${codeParameter}`

  const emailMessage = `&amp;lt;div style=&amp;#39;text-align: center&amp;#39;&amp;gt;
  &amp;lt;p&amp;gt;Please verify your email address to complete account setup.&amp;lt;/p&amp;gt;
  &amp;lt;p&amp;gt;Your verification code is&amp;lt;/p&amp;gt;
  &amp;lt;h3&amp;gt;${codeParameter}&amp;lt;/h3&amp;gt;
  &amp;lt;p&amp;gt;Navigate to &amp;lt;a href=&amp;quot;${verifyLink}&amp;quot;&amp;gt;${verifyLink}&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;`
  event.response.emailMessage = emailMessage
  event.response.emailSubject =&amp;#39;Verify your account ✅&amp;#39;

  return event
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;addCustomMessageTrigger() {
  // Custom message https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-message.html
  const { logRetentionInDays } = getEnvironmentConfig(this.node)
  const isSourceMapsEnabled = getIsSourceMapsEnabled({ node: this.node })
  const cognitoCustomMessageLogGroup = new LogGroup(this, &amp;#39;CustomMessageLogGroup&amp;#39;, {
    retention: logRetentionInDays,
  })
  const environment: StringMap = {}

  if (isSourceMapsEnabled) {
    environment.NODE_OPTIONS = &amp;#39;--enable-source-maps&amp;#39;
  }

  const cognitoCustomMessageFunction = new NodejsFunction(this, &amp;#39;CustomMessageFunction&amp;#39;, {
    runtime: Runtime.NODEJS_20_X,
    handler: &amp;#39;handler&amp;#39;,
    entry: path.join(cognitoPackageDir, &amp;#39;cognito-custom-message.ts&amp;#39;),
    timeout: Duration.seconds(10),
    memorySize: 1024,
    logGroup: cognitoCustomMessageLogGroup,
    bundling: {
      sourceMap: isSourceMapsEnabled,
    },
    environment,
  })
  this.userPool.addTrigger(UserPoolOperation.CUSTOM_MESSAGE, cognitoCustomMessageFunction)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;End&lt;/h2&gt;
&lt;p&gt;After reading this series of articles you should understand how a full stack identity solution with AWS Cognito User Pools works. If you haven&amp;#39;t done so already, you can generate a full stack project with this solution and more with a single Code Genie command:&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npx @codegenie/cli generate --name &amp;quot;Todo&amp;quot; \
--idp &amp;#39;Google&amp;#39; --idp &amp;#39;SAML&amp;#39; \
--description &amp;quot;A todo list app that lets users create lists and add items to the list. Items should have a title, description, be markable as completed, have a due date, and have an image.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://codegenie.codes&quot;&gt;Code Genie&lt;/a&gt; generates a Full Stack AWS Serverless application based on your own data model, including a React (Next.js) Web App, Serverless Express REST API, DynamoDB Database, and Cognito Auth.
:::&lt;/p&gt;
</content:encoded><category>cognito</category><category>aws</category></item><item><title>AWS Cognito User Pools: Create Resources with CDK</title><link>https://codegenie.codes/blog/aws-cognito-user-pools-create-resources-with-cdk/</link><guid isPermaLink="true">https://codegenie.codes/blog/aws-cognito-user-pools-create-resources-with-cdk/</guid><description>how to

</description><pubDate>Sat, 02 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Image } from &amp;#39;astro:assets&amp;#39;;
import { Tabs, TabItem } from &amp;#39;@astrojs/starlight/components&amp;#39;
import signInPage from &amp;#39;../../../assets/images/blog/aws-cognito-google-sso-saml-linked-accounts/cognito-example-sign-in.webp&amp;#39;;&lt;/p&gt;
&lt;figure style=&quot;{{margin:&quot; =&quot;&apos;360px&apos;}}&quot; auto=&quot;&apos;,&quot; width:=&quot;&quot;&gt;
  
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{margin:&quot; color:=&quot;&quot; =&quot;&apos;0.8rem&apos;}}&quot; fontSize:=&quot;&quot;&gt;Pretty picture of the Login UI we build in the [React Cognito User Pools](/blog/aws-cognito-user-pools-add-auth-to-a-react-app) article.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/cognito/&quot;&gt;Cognito User Pools&lt;/a&gt; is an AWS service that provides applications with user identity and auth. This series of articles cover a full stack solution that enables users to sign in with their Email + Password, Google Sign In, or SSO (SAML), and link all methods to the same user within the app:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create Resources with CDK 👈 You are here&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/aws-cognito-user-pools-sign-in-with-email-google-saml-and-link-to-a-single-user&quot;&gt;Sign in with Email, Google, or SAML and link to a single user&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/aws-cognito-user-pools-add-auth-to-a-react-app&quot;&gt;Add Auth to a React App&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This article focuses on using &lt;a href=&quot;https://docs.aws.amazon.com/cdk/v2/guide/home.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CDK&lt;/a&gt; to create our Cognito User Pool resources as Infrastructure as Code (IAC). If you&amp;#39;re already familiar with this you should consider moving onto the next part of this series &lt;a href=&quot;/blog/aws-cognito-user-pools-sign-in-with-email-google-saml-and-link-to-a-single-user&quot;&gt;Sign in with Email, Google, or SAML and link to a single user&lt;/a&gt;, since IAC can be a little lengthy (and boring).&lt;/p&gt;
&lt;p&gt;:::tip
The complete source code is available by generating a Code Genie app and specifying &lt;code&gt;&amp;#39;Google&amp;#39;&lt;/code&gt; and &lt;code&gt;&amp;#39;SAML&amp;#39;&lt;/code&gt; &lt;code&gt;idp&lt;/code&gt; options:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npx @codegenie/cli generate --name &amp;quot;Todo&amp;quot; \
--idp &amp;#39;Google&amp;#39; --idp &amp;#39;SAML&amp;#39; \
--description &amp;quot;A todo list app that lets users create lists and add items to the list. Items should have a title, description, be markable as completed, have a due date, and have an image.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://codegenie.codes&quot;&gt;Code Genie&lt;/a&gt; generates a Full Stack AWS Serverless application based on your own data model, including a React (Next.js) Web App, Serverless Express REST API, DynamoDB Database, and Cognito Auth.
:::&lt;/p&gt;
&lt;h2&gt;Cognito User Pool CDK (IAC)&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s start with the basic setup of the Auth construct before we dive into the details defined in the class methods:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { existsSync, readFileSync } from &amp;#39;fs&amp;#39;
import path = require(&amp;#39;path&amp;#39;)
import { Aws, CfnOutput, Duration } from &amp;#39;aws-cdk-lib&amp;#39;
import {
  ProviderAttribute,
  UserPool,
  UserPoolClient,
  UserPoolEmail,
  UserPoolIdentityProviderGoogle,
  UserPoolIdentityProviderSaml,
  UserPoolIdentityProviderSamlMetadata,
  UserPoolOperation,
} from &amp;#39;aws-cdk-lib/aws-cognito&amp;#39;
import { Runtime } from &amp;#39;aws-cdk-lib/aws-lambda&amp;#39;
import { NodejsFunction } from &amp;#39;aws-cdk-lib/aws-lambda-nodejs&amp;#39;
import { Construct } from &amp;#39;constructs&amp;#39;
import { Effect, PolicyStatement } from &amp;#39;aws-cdk-lib/aws-iam&amp;#39;
import { ITable } from &amp;#39;aws-cdk-lib/aws-dynamodb&amp;#39;
import { LogGroup } from &amp;#39;aws-cdk-lib/aws-logs&amp;#39;
// These methods are responsible for getting environment-specific config (.e.g dev, staging, prod)
import {
  getEnvironmentConfig,
  getIsDeletionProtectionEnabled,
  getIsSourceMapsEnabled,
  getRemovalPolicy
} from &amp;#39;../environment-config&amp;#39;
import type { StringMap } from &amp;#39;../../../common/types&amp;#39;

// SAML-related variables
const GOOGLE_SAML_IDENTITY_PROVIDER_NAME = &amp;#39;GoogleSaml&amp;#39;
const googleIDPMetadataPath = path.resolve(__dirname, &amp;#39;../../GoogleIDPMetadata.xml&amp;#39;)
const googleIDPMetadataExists = existsSync(googleIDPMetadataPath)

// Cognito Lambda Functions path
const cognitoPackageDir = path.resolve(__dirname, &amp;#39;../../../cognito&amp;#39;)

interface AuthProps {
  userTable: ITable
  webAppUrl: string
}

export default class Auth extends Construct {
  readonly userPool: UserPool
  readonly userPoolClient: UserPoolClient
  constructor(scope: Construct, id: string, props: AuthProps) {
    super(scope, id)

    this.userPool = this.createUserPool()
    this.userPoolClient = this.createUserPoolClient({ additionalOauthCallbackUrls: props.additionalOauthCallbackUrls })
    this.addDomainName()
    this.addTriggers({ userTable: props.userTable })
  }
  ...additional methods defined below
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;User Pool&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  createUserPool() {
    const userPool = new UserPool(this, &amp;#39;UserPool&amp;#39;, {
      signInCaseSensitive: false,
      deletionProtection: getIsDeletionProtectionEnabled({ node: this.node }),
      removalPolicy: getRemovalPolicy({ node: this.node }),
      passwordPolicy: {
        minLength: 8,
      },
      selfSignUpEnabled: true,
      signInAliases: {
        username: false,
        email: true,
      },
    })

    new CfnOutput(this, &amp;#39;UserPoolId&amp;#39;, { key: &amp;#39;UserPoolId&amp;#39;, value: userPool.userPoolId })

    return userPool
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
Always specify &lt;code&gt;signInCaseSensitive: false&lt;/code&gt;. By default, User Pools treat usernames/emails as case sensitive, resulting in &lt;a href=&quot;mailto:foo@bar.com&quot;&gt;foo@bar.com&lt;/a&gt; and &lt;a href=&quot;mailto:Foo@bar.com&quot;&gt;Foo@bar.com&lt;/a&gt; being treated as two separate users.&lt;/p&gt;
&lt;p&gt;For production environments, specify &lt;code&gt;deletionProtection: true&lt;/code&gt; and &lt;code&gt;removalPolicy: cdk.RemovalPolicy.RETAIN&lt;/code&gt; to protect against accidentally deleting your User Pool (and users along with it). Here we are conditionally setting it based on environment, since we generally don&amp;#39;t want/need protection in development environments.
:::&lt;/p&gt;
&lt;h3&gt;User Pool Client&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;createUserPoolClient({ webAppUrl }: { webAppUrl: string }) {
  const environmentConfig = getEnvironmentConfig(this.node)
  const googleIdentityProvider = this.createGoogleIdentityProvider()
  const googleSamlIdentityProvider = this.createGoogleSamlIdentityProvider()
  const supportedIdentityProviders: {name: string}[] = []

  if (googleIdentityProvider) {
    supportedIdentityProviders.push({ name: googleIdentityProvider.providerName })
  }

  if (googleSamlIdentityProvider) {
    supportedIdentityProviders.push({ name: googleSamlIdentityProvider.providerName })
  }

  const callbackUrls = [&amp;#39;http://localhost:3001/&amp;#39;]

  if (webAppUrl) {
    callbackUrls.push(webAppUrl)
  }

  const userPoolClient = this.userPool.addClient(&amp;#39;UserPoolClient&amp;#39;, {
    idTokenValidity: Duration.days(1),
    refreshTokenValidity: Duration.days(90),
    supportedIdentityProviders,
    oAuth: {
      callbackUrls: callbackUrls,
      logoutUrls: callbackUrls,
    },
  })

  if (googleIdentityProvider) {
    userPoolClient.node.addDependency(googleIdentityProvider)
  }

  if (googleSamlIdentityProvider) {
    userPoolClient.node.addDependency(googleSamlIdentityProvider)
  }

  new CfnOutput(this, &amp;#39;UserPoolClientId&amp;#39;, { key: &amp;#39;UserPoolClientId&amp;#39;, value: userPoolClient.userPoolClientId })

  return userPoolClient
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We specify up to 2 OAuth callback URLs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;http://localhost:3001/&lt;/code&gt; for local development. Consider removing this for production.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webAppUrl&lt;/code&gt; The live web app URL. In Code Genie, this is passed in as either the web app&amp;#39;s custom domain name (if one is defined in cdk.json for the environment we&amp;#39;re deploying) or the default Amplify Hosting URL.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Identity Providers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;createGoogleIdentityProvider() {
  const { auth } = getEnvironmentConfig(this.node)

  if (!auth.googleClientId &amp;amp;&amp;amp; !auth.googleClientId) return

  const googleIdentityProvider = new UserPoolIdentityProviderGoogle(this, &amp;#39;GoogleIdp&amp;#39;, {
    userPool: this.userPool,
    clientId: auth.googleClientId,
    clientSecret: auth.googleClientSecret,
    scopes: [&amp;#39;profile&amp;#39;, &amp;#39;email&amp;#39;, &amp;#39;openid&amp;#39;],
    attributeMapping: {
      email: ProviderAttribute.GOOGLE_EMAIL,
      fullname: ProviderAttribute.GOOGLE_NAME,
      familyName: ProviderAttribute.GOOGLE_FAMILY_NAME,
      givenName: ProviderAttribute.GOOGLE_GIVEN_NAME,
      profilePicture: ProviderAttribute.GOOGLE_PICTURE,
    },
  })
  new CfnOutput(this, &amp;#39;GoogleIdpName&amp;#39;, { key: &amp;#39;GoogleIdpName&amp;#39;, value: googleIdentityProvider.providerName })

  return googleIdentityProvider
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Define the scopes and attributes from the External IDP that we want to store against our User in our User Pool. We&amp;#39;re grabbing the &lt;code&gt;profilePicture&lt;/code&gt; so that we can display the user&amp;#39;s profile picture against their name.&lt;/p&gt;
&lt;p&gt;:::tip
Consider storing the Google Client Secret in &lt;a href=&quot;https://aws.amazon.com/secrets-manager/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS Secrets Manager&lt;/a&gt; and using the &lt;code&gt;clientSecretValue&lt;/code&gt; prop instead. See the &lt;a href=&quot;https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito-readme.html#identity-providers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CDK Cognito User Pool Identity Provider docs&lt;/a&gt; for more details.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;createGoogleSamlIdentityProvider() {
  if (!googleIDPMetadataExists) return null
  const googleIdpMetadataContents = readFileSync(googleIDPMetadataPath, &amp;#39;utf-8&amp;#39;)
  const userPoolIdentityProviderSamlMetadata = UserPoolIdentityProviderSamlMetadata.file(googleIdpMetadataContents)

  const googleSamlIdentityProvider = new UserPoolIdentityProviderSaml(this, &amp;#39;GoogleSamlIdp&amp;#39;, {
    name: GOOGLE_SAML_IDENTITY_PROVIDER_NAME,
    userPool: this.userPool,
    metadata: userPoolIdentityProviderSamlMetadata,
    attributeMapping: {
      email: ProviderAttribute.GOOGLE_EMAIL,
      fullname: ProviderAttribute.GOOGLE_NAME,
      familyName: ProviderAttribute.GOOGLE_FAMILY_NAME,
      givenName: ProviderAttribute.GOOGLE_GIVEN_NAME,
    },
  })
  new CfnOutput(this, &amp;#39;GoogleSamlIdpName&amp;#39;, { key: &amp;#39;GoogleSamlIdpName&amp;#39;, value: googleSamlIdentityProvider.providerName })

  return googleSamlIdentityProvider
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There&amp;#39;s a circular dependency between the Cognito User Pool and the SAML App you create in Google Workspace. Exit early if no &lt;code&gt;GoogleIDPMetadata.xml&lt;/code&gt; file exists. When you create the SAML App, you&amp;#39;ll need to provide the &lt;code&gt;UserPoolRedirectUrlACS&lt;/code&gt; and &lt;code&gt;UserPoolEntityId&lt;/code&gt; values from &lt;code&gt;cdk-outputs.development.json&lt;/code&gt;. Download the SAML App&amp;#39;s metadata file and redeploy your CDK Stack.&lt;/p&gt;
&lt;p&gt;We must hardcode the SAML Identity Provider name (&lt;code&gt;GOOGLE_SAML_IDENTITY_PROVIDER_NAME&lt;/code&gt;) to prevent a second circular dependency between the SAML Identity Provider and the &lt;code&gt;PreSignup&lt;/code&gt; Cognito Trigger (which we&amp;#39;ll meet soon). It doesn&amp;#39;t seem like this should be a circular dependency since both resources are attached to the User Pool and the SAML Identity Provider has nothing to do with Triggers, but CDK will give you a &amp;quot;Circular dependency between resources&amp;quot; nonetheless.&lt;/p&gt;
&lt;p&gt;Our attribute mappings for the SAML Identity Provider are similar to the Google Identity Provider. Unfortunately we don&amp;#39;t have access to a profile picture, and even though we&amp;#39;re requesting &lt;code&gt;fullname&lt;/code&gt; here, Google doesn&amp;#39;t allow you to map it when creating the SAML App. We&amp;#39;ll handle this later.&lt;/p&gt;
&lt;h3&gt;Triggers&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;addTriggers({ userTable }: { userTable: ITable }) {
  this.addPreSignupTrigger()
  this.addPreTokenGenerationTrigger({  userTable  })
}

// Pre Signup https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html
addPreSignupTrigger() {
  const { auth, logRetentionInDays } = getEnvironmentConfig(this.node)
  const isSourceMapsEnabled = getIsSourceMapsEnabled({ node: this.node })

  const cognitoPreSignupLogGroup = new LogGroup(this, &amp;#39;PreSignupLogGroup&amp;#39;, {
    retention: logRetentionInDays,
  })

  const environment: StringMap = {}

  if (isSourceMapsEnabled) {
    environment.NODE_OPTIONS = &amp;#39;--enable-source-maps&amp;#39;
  }

  if (auth?.autoVerifyUsers) {
    environment.AUTO_VERIFY_USERS = &amp;#39;1&amp;#39;
  }

  environment.GOOGLE_SAML_IDENTITY_PROVIDER_NAME = GOOGLE_SAML_IDENTITY_PROVIDER_NAME

  const cognitoPreSignupFunction = new NodejsFunction(this, &amp;#39;PreSignupFunction&amp;#39;, {
    runtime: Runtime.NODEJS_20_X,
    handler: &amp;#39;handler&amp;#39;,
    entry: path.join(cognitoPackageDir, &amp;#39;cognito-pre-signup.ts&amp;#39;),
    timeout: Duration.seconds(10),
    memorySize: 1024,
    logGroup: cognitoPreSignupLogGroup,
    bundling: {
      sourceMap: isSourceMapsEnabled,
    },
    environment,
  })
  const updateCognitoUserPoolPolicyStatement = new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [
      &amp;#39;cognito-idp:AdminUpdateUserAttributes&amp;#39;,
      &amp;#39;cognito-idp:AdminLinkProviderForUser&amp;#39;,
      &amp;#39;cognito-idp:AdminCreateUser&amp;#39;,
      &amp;#39;cognito-idp:AdminSetUserPassword&amp;#39;,
      &amp;#39;cognito-idp:ListUsers&amp;#39;,
    ],
    resources: [
      `arn:aws:cognito-idp:${Aws.REGION}:${Aws.ACCOUNT_ID}:userpool/*`,
    ],
  })
  cognitoPreSignupFunction.addToRolePolicy(updateCognitoUserPoolPolicyStatement)
  this.userPool.addTrigger(UserPoolOperation.PRE_SIGN_UP, cognitoPreSignupFunction)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Setting &lt;code&gt;AUTO_VERIFY_USERS&lt;/code&gt; in development and testing environments is really convenient. It&amp;#39;s just as convenient in production, but there are good reasons to require email confirmation (such as GDPR compliance).&lt;/p&gt;
&lt;p&gt;We add our &lt;code&gt;GOOGLE_SAML_IDENTITY_PROVIDER_NAME&lt;/code&gt; environment variable since we&amp;#39;ll need that in the Lambda Function&amp;#39;s code.&lt;/p&gt;
&lt;p&gt;Our Lambda Function requires 5 permissions for the User Pool. Unfortunately we can&amp;#39;t specify &lt;code&gt;this.userPool.userPoolArn&lt;/code&gt; as the resource since CDK once again complains about a circular dependency. We&amp;#39;re left with granting access to all User Pools within the same account and region.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;addPreTokenGenerationTrigger({ userTable }: { userTable: ITable }) {
  // Pre Token Generation https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html
  const { logRetentionInDays } = getEnvironmentConfig(this.node)
  const isSourceMapsEnabled = getIsSourceMapsEnabled({ node: this.node })
  const cognitoPreTokenGenerationLogGroup = new LogGroup(this, &amp;#39;PreTokenGenerationLogGroup&amp;#39;, {
    retention: logRetentionInDays,
  })

  const environment: StringMap = {
    USER_TABLE: userTable.tableName,
  }

  if (isSourceMapsEnabled) {
    environment.NODE_OPTIONS = &amp;#39;--enable-source-maps&amp;#39;
  }

  const cognitoPreTokenGenerationFunction = new NodejsFunction(this, &amp;#39;PreTokenGenerationFunction&amp;#39;, {
    runtime: Runtime.NODEJS_20_X,
    handler: &amp;#39;handler&amp;#39;,
    entry: path.join(cognitoPackageDir, &amp;#39;cognito-pre-token-generation.ts&amp;#39;),
    timeout: Duration.seconds(10),
    memorySize: 1024,
    logGroup: cognitoPreTokenGenerationLogGroup,
    bundling: {
      sourceMap: isSourceMapsEnabled,
    },
    environment,
  })

  // Give the Lambda function permission to read and write to DynamoDB
  const dynamoDBReadWritePolicy = new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [
      &amp;#39;dynamodb:GetItem&amp;#39;,
      &amp;#39;dynamodb:PutItem&amp;#39;,
      &amp;#39;dynamodb:UpdateItem&amp;#39;,
    ],
    resources: [
      userTable.tableArn,
    ],
  })
  cognitoPreTokenGenerationFunction.addToRolePolicy(dynamoDBReadWritePolicy)

  // Give the Lambda function permission to update Cognito User Attributes
  const updateCognitoUserPoolPolicyStatement = new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [
      &amp;#39;cognito-idp:AdminUpdateUserAttributes&amp;#39;,
    ],
    resources: [
      `arn:aws:cognito-idp:${Aws.REGION}:${Aws.ACCOUNT_ID}:userpool/*`,
    ],
  })
  cognitoPreTokenGenerationFunction.addToRolePolicy(updateCognitoUserPoolPolicyStatement)
  this.userPool.addTrigger(UserPoolOperation.PRE_TOKEN_GENERATION, cognitoPreTokenGenerationFunction)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
You can create your own CDK Constructs to abstract away some of the repetitiveness you see in these Node.js Lambda Functions.
:::&lt;/p&gt;
&lt;h3&gt;Environment Config&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import * as cdk from &amp;#39;aws-cdk-lib/core&amp;#39;
import type { Node } from &amp;#39;constructs&amp;#39;

interface NodeEnvProps {
  node?: Node
  env?: string
}

export interface CdkJsonEnvironmentConfig {
  [environment: string]: CdkJsonEnvironmentConfigEnvironment
}
interface CdkJsonEnvironmentConfigEnvironment {
  profile: string
  region: string
  auth: CdkJsonEnvironmentConfigAuth
  logRetentionInDays: number
  email: CdkJsonEnvironmentConfigEmail
  api?: CdkJsonEnvironmentConfigApi
  ui?: CdkJsonEnvironmentConfigUi
  isSourceMapsEnabled?: boolean
}

interface CdkJsonEnvironmentConfigAuth {
  autoVerifyUsers?: boolean
  googleClientId?: string
  googleClientSecret?: string
}

interface CdkJsonEnvironmentConfigEmail {
  organizationInviteEmail: string
  verifyUserEmail?: string
  sandboxApprovedToEmails?: string[]
  verifiedDomain?: string
}

interface CdkJsonEnvironmentConfigApi {
  domainName: string
  validationDomain: string
}

interface CdkJsonEnvironmentConfigUi {
  domainName: string
}

export function getEnvironmentConfig(node: Node): CdkJsonEnvironmentConfigEnvironment {
  const env = getEnvironmentName(node)
  const environmentConfig = node.getContext(&amp;#39;environmentConfig&amp;#39;)[env]

  if (!environmentConfig) {
    throw new Error(`Missing environment config for ${env}`)
  }

  return environmentConfig
}

export function getEnvironmentName(node: Node) {
  return node.tryGetContext(&amp;#39;env&amp;#39;) || &amp;#39;development&amp;#39;
}

export function getIsProd({
  node,
  env = node ? getEnvironmentName(node) : undefined,
}: NodeEnvProps) {
  return env === &amp;#39;production&amp;#39;
}

export function getIsProdish({
  node,
  env = node ? getEnvironmentName(node) : undefined,
}: NodeEnvProps) {
  return [&amp;#39;staging&amp;#39;, &amp;#39;production&amp;#39;].includes(env)
}

export function getIsDev({
  node,
  env = node ? getEnvironmentName(node) : undefined,
}: NodeEnvProps) {
  return env === &amp;#39;development&amp;#39;
}

// Source maps are extremely slow; don&amp;#39;t run in prod
export function getIsSourceMapsEnabled({
  node,
}: { node: Node}) {
  const environmentConfig = getEnvironmentConfig(node)
  return environmentConfig.isSourceMapsEnabled ?? getIsDev({ node })
}

export function getIsPointInTimeRecoveryEnabled({
  node,
  env = node ? getEnvironmentName(node) : undefined,
}: NodeEnvProps) {
  return getIsProdish({ env })
}

export function getRemovalPolicy({
  node,
  env = node ? getEnvironmentName(node) : undefined,
}: NodeEnvProps) {
  // NOTE: During initial setup of staging and prod environments, it&amp;#39;s beneficial to start with Deletion Protection off and
  // and Removal Policy set to Destroy. This way, if things go wrong during setup, it&amp;#39;s easy to tear down and start again.
  // Once you have stable staging and production environments, remove these early return statements.
  return cdk.RemovalPolicy.DESTROY
  return getIsDev({ env }) ? cdk.RemovalPolicy.DESTROY : cdk.RemovalPolicy.RETAIN
}

export function getIsDeletionProtectionEnabled({
  node,
  env = node ? getEnvironmentName(node) : undefined,
}: NodeEnvProps) {
  return false
  return getIsProdish({ env })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;cdk.json&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;app&amp;quot;: &amp;quot;ts-node --prefer-ts-exts bin/cdk.ts&amp;quot;,
  &amp;quot;context&amp;quot;: {
    &amp;quot;environmentConfig&amp;quot;: {
      &amp;quot;development&amp;quot;: {...},
      &amp;quot;staging&amp;quot;: {...},
      &amp;quot;production&amp;quot;: {
        &amp;quot;auth&amp;quot;: {
          &amp;quot;autoVerifyUsers&amp;quot;: false,
          &amp;quot;googleClientId&amp;quot;: &amp;quot;&amp;quot;,
          &amp;quot;googleClientSecret&amp;quot;: &amp;quot;&amp;quot;
        },
        &amp;quot;logRetentionInDays&amp;quot;: 14,
        &amp;quot;ui&amp;quot;: {
          &amp;quot;domainName&amp;quot;: &amp;quot;app.example.com&amp;quot;
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;StringMap&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export interface StringMap {
  [name: string]: string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Create External Identity Providers&lt;/h2&gt;
&lt;h3&gt;Google Sign-in&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Login to Google Cloud Console and select or create the relevant project.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://console.cloud.google.com/apis/credentials/oauthclient?&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Create OAuth client ID&lt;/a&gt;:&lt;ol&gt;
&lt;li&gt;Select &amp;quot;Web application&amp;quot; as the &amp;quot;Application type&amp;quot; and enter a name&lt;/li&gt;
&lt;li&gt;Click &amp;quot;Add URI&amp;quot; under &amp;quot;Authorised JavaScript origins&amp;quot;. Copy the value for &lt;code&gt;UserPoolRedirectUrlACS&lt;/code&gt; from &lt;code&gt;cdk-outputs.dev.json&lt;/code&gt; and paste in only the domain part (i.e remove the &amp;#39;/saml2/idpresponse&amp;#39;).&lt;/li&gt;
&lt;li&gt;Click &amp;quot;Add URI&amp;quot; under &amp;quot;Authorised redirect URIs&amp;quot;. Copy the same value from above, but this time add &amp;quot;/oauth2/idpresponse&amp;quot; to the end.&lt;/li&gt;
&lt;li&gt;Click Save&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Configure &lt;a href=&quot;https://console.cloud.google.com/apis/credentials/oauthclient?&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;OAuth consent screen&lt;/a&gt;:&lt;ol&gt;
&lt;li&gt;Select User Type &amp;quot;External&amp;quot; and mark it for &amp;quot;Production&amp;quot;&lt;/li&gt;
&lt;li&gt;Under &amp;quot;Authorised domains&amp;quot; add &amp;quot;amazoncognito.com&amp;quot;&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Google SAML&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Login to Google Workspace Admin and navigate to &lt;a href=&quot;https://admin.google.com/ac/apps/unified&quot;&gt;Apps =&amp;gt; Web and Mobile Apps&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Click &amp;quot;Add app&amp;quot; =&amp;gt; &amp;quot;Add custom SAML App&amp;quot;. Enter a name for your app.&lt;/li&gt;
&lt;li&gt;Download the metadata file to &lt;code&gt;./packages/cdk&lt;/code&gt; and click Continue.&lt;/li&gt;
&lt;li&gt;From &lt;code&gt;cdk-outputs.development.json&lt;/code&gt;, copy the values for &lt;code&gt;UserPoolRedirectUrlACS&lt;/code&gt; and &lt;code&gt;UserPoolEntityId&lt;/code&gt; and paste them into the respective fields in the Service Provider Details form and click Continue.&lt;/li&gt;
&lt;li&gt;Add attribute mappings: Primary email =&amp;gt; email; First name =&amp;gt; first_name; Last name =&amp;gt; family_name. Click Finish.&lt;/li&gt;
&lt;li&gt;Click the User Access card; select &amp;quot;ON for everyone&amp;quot; and click Save.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;npm run deploy:dev&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;End&lt;/h2&gt;
&lt;p&gt;Now that we&amp;#39;ve created our AWS Cognito User Pool and both External (Sign in with Google) and Internal (SAML) Google Apps, we&amp;#39;re ready to move onto more interesting topics: &lt;a href=&quot;/blog/aws-cognito-user-pools-sign-in-with-email-google-saml-and-link-to-a-single-user&quot;&gt;Sign in with Email, Google, or SAML and link to a single user&lt;/a&gt;. This next article focuses on the Lambda Handler logic of the Lambda Functions we created with CDK.&lt;/p&gt;
&lt;p&gt;:::tip
The complete source code is available by generating a Code Genie app and specifying &lt;code&gt;&amp;#39;Google&amp;#39;&lt;/code&gt; and &lt;code&gt;&amp;#39;SAML&amp;#39;&lt;/code&gt; &lt;code&gt;idp&lt;/code&gt; options:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npx @codegenie/cli generate --name &amp;quot;Todo&amp;quot; \
--idp &amp;#39;Google&amp;#39; --idp &amp;#39;SAML&amp;#39; \
--description &amp;quot;A todo list app that lets users create lists and add items to the list. Items should have a title, description, be markable as completed, have a due date, and have an image.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://codegenie.codes&quot;&gt;Code Genie&lt;/a&gt; generates a Full Stack AWS Serverless application based on your own data model, including a React (Next.js) Web App, Serverless Express REST API, DynamoDB Database, and Cognito Auth.
:::&lt;/p&gt;
</content:encoded><category>cognito</category><category>aws</category></item><item><title>AWS Cognito User Pools: Sign in with Email, Google, or SAML and link to a single user</title><link>https://codegenie.codes/blog/aws-cognito-user-pools-sign-in-with-email-google-saml-and-link-to-a-single-user/</link><guid isPermaLink="true">https://codegenie.codes/blog/aws-cognito-user-pools-sign-in-with-email-google-saml-and-link-to-a-single-user/</guid><description>Allow users to sign in with their Email + Password, Google, SSO (SAML), or other Identity Provider, and have them all link to the same account using Cognito Lambda Triggers.

</description><pubDate>Sat, 02 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Image } from &amp;#39;astro:assets&amp;#39;;
import { Tabs, TabItem } from &amp;#39;@astrojs/starlight/components&amp;#39;
import signInPage from &amp;#39;../../../assets/images/blog/aws-cognito-google-sso-saml-linked-accounts/cognito-example-sign-in.webp&amp;#39;;&lt;/p&gt;
&lt;figure style=&quot;{{margin:&quot; =&quot;&apos;360px&apos;}}&quot; auto=&quot;&apos;,&quot; width:=&quot;&quot;&gt;
  
  &lt;figcaption 0=&quot;,&quot; style=&quot;{{margin:&quot; color:=&quot;&quot; =&quot;&apos;0.8rem&apos;}}&quot; fontSize:=&quot;&quot;&gt;Pretty picture of the Login UI we build in the [React Cognito User Pools](/blog/aws-cognito-user-pools-add-auth-to-a-react-app) article.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/cognito/&quot;&gt;Cognito User Pools&lt;/a&gt; is an AWS service that provides applications with user identity and auth. This series of articles cover a full stack solution that enables users to sign in with their Email + Password, Google Sign In, or SSO (SAML), and link all methods to the same user within the app:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/blog/aws-cognito-user-pools-create-resources-with-cdk&quot;&gt;Create Resources with CDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Sign in with Email, Google, or SAML and link to a single user 👈 You are here&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/aws-cognito-user-pools-add-auth-to-a-react-app&quot;&gt;Add Auth to a React App&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This article focuses on the logic behind the &lt;a href=&quot;https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cognito Triggers&lt;/a&gt; (&lt;a href=&quot;https://aws.amazon.com/lambda/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Lambda Functions&lt;/a&gt;) that fire when users register or sign in. This solution allows users to sign in with multiple methods such as Email + Password, Google, and SAML, and have them all link to the same account within the app.&lt;/p&gt;
&lt;p&gt;:::tip
The complete source code is available by generating a Code Genie app and specifying &lt;code&gt;&amp;#39;Google&amp;#39;&lt;/code&gt; and &lt;code&gt;&amp;#39;SAML&amp;#39;&lt;/code&gt; &lt;code&gt;idp&lt;/code&gt; options:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npx @codegenie/cli generate --name &amp;quot;Todo&amp;quot; \
--idp &amp;#39;Google&amp;#39; --idp &amp;#39;SAML&amp;#39; \
--description &amp;quot;A todo list app that lets users create lists and add items to the list. Items should have a title, description, be markable as completed, have a due date, and have an image.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://codegenie.codes&quot;&gt;Code Genie&lt;/a&gt; generates a Full Stack AWS Serverless application based on your own data model, including a React (Next.js) Web App, Serverless Express REST API, DynamoDB Database, and Cognito Auth.
:::&lt;/p&gt;
&lt;p&gt;:::note[Terminology]
This example uses both Google Social Sign in, and Google SSO/SAML. So that we don&amp;#39;t confuse the two, I&amp;#39;ll just refer to the latter as the &amp;quot;SAML&amp;quot; Identity Provider (IDP) going forward. The terms &amp;quot;Federated&amp;quot; and &amp;quot;External&amp;quot; are used interchangeably.
:::&lt;/p&gt;
&lt;h2&gt;Cognito Triggers&lt;/h2&gt;
&lt;p&gt;The core of this solution relies on two Cognito Triggers. The high-level flow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;a href=&quot;https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Pre sign-up Trigger&lt;/a&gt; fires when a user signs up using Google, SAML, or Email + Password. It&amp;#39;s also triggered when an existing user signs in using a new external identity for the first time (e.g. if a user signs up using SAML and then later signs in using Google SSO with the same email).&lt;ol&gt;
&lt;li&gt;If they&amp;#39;re signing up with Email + Password, continue.&lt;/li&gt;
&lt;li&gt;If they&amp;#39;re signing up via Google or SAML and they haven&amp;#39;t already signed up using a different method with the same email: a native Cognito User is created for that email.&lt;/li&gt;
&lt;li&gt;The External Identity is linked to the native Cognito User for that email.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;a href=&quot;https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Pre token generation Trigger&lt;/a&gt; fires on every sign in.&lt;ol&gt;
&lt;li&gt;If this is their first time signing in, a User record is created in DynamoDB.&lt;/li&gt;
&lt;li&gt;If the user has External Identities linked to their account, the account is marked as verified.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We&amp;#39;ll first look at the code for each trigger and then discuss the details.&lt;/p&gt;
&lt;h3&gt;Pre Sign-up Lambda Handler&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { randomBytes } from &amp;#39;crypto&amp;#39;
import {
  AdminCreateUserCommand,
  AdminLinkProviderForUserCommand,
  AdminSetUserPasswordCommand,
  CognitoIdentityProviderClient,
  ListUsersCommand,
  MessageActionType,
} from &amp;#39;@aws-sdk/client-cognito-identity-provider&amp;#39;
import type { PreSignUpTriggerEvent } from &amp;#39;aws-lambda&amp;#39;
import UnsupportedIdentityProviderNameException from &amp;#39;./exceptions/UnsupportedIdentityProviderNameException&amp;#39;
import getFullName from &amp;#39;./getFullName&amp;#39;

const providerNamesLowerCaseLookup = {
  &amp;#39;google&amp;#39;: &amp;#39;Google&amp;#39;,
  // &amp;#39;facebook&amp;#39;: &amp;#39;Facebook&amp;#39;,
}

// Env vars are passed in via CDK defined in the previous article
const { AUTO_VERIFY_USERS, GOOGLE_SAML_IDENTITY_PROVIDER_NAME } = process.env

if (GOOGLE_SAML_IDENTITY_PROVIDER_NAME) {
  providerNamesLowerCaseLookup[GOOGLE_SAML_IDENTITY_PROVIDER_NAME.toLowerCase()] = GOOGLE_SAML_IDENTITY_PROVIDER_NAME
}

const cognitoIdpClient = new CognitoIdentityProviderClient({ region: &amp;#39;us-west-2&amp;#39; })

export async function handler (event: PreSignUpTriggerEvent) {
  switch(event.triggerSource) {
  case &amp;#39;PreSignUp_ExternalProvider&amp;#39;:
    await handleExternalProvider({ event })
    break
  case &amp;#39;PreSignUp_SignUp&amp;#39;:
  case &amp;#39;PreSignUp_AdminCreateUser&amp;#39;:
    if (AUTO_VERIFY_USERS) {
      event.response.autoConfirmUser = true
      event.response.autoVerifyEmail = true
    }
  }

  return event
}

async function handleExternalProvider({ event }: { event: PreSignUpTriggerEvent }) {
  const {
    userName,
    userPoolId,
    request: {
      userAttributes,
    },
  } = event

  const { email, name, given_name: givenName, family_name: familyName } = userAttributes
  const [providerName, providerUserId] = userName.split(&amp;#39;_&amp;#39;)
  const providerNameWithCorrectCapitalization = providerNamesLowerCaseLookup[providerName.toLowerCase()]

  if (!providerNameWithCorrectCapitalization) {
    throw new UnsupportedIdentityProviderNameException({ providerName, validIdentityProviderNamesMap: providerNamesLowerCaseLookup })
  }

  const cognitoUsersWithEmail = await listCognitoUsersWithEmail({ userPoolId, email })

  if (cognitoUsersWithEmail.Users?.length) {
    const cognitoUsername = cognitoUsersWithEmail.Users[0].Username || &amp;#39;username-not-found&amp;#39;

    await linkCognitoUserAccounts({ cognitoUsername, userPoolId, providerName: providerNameWithCorrectCapitalization, providerUserId })
  } else {
    const newNativeCognitoUser = await createCognitoUser({ userPoolId, email, givenName, familyName, name })

    const newNativeCognitoUserUsername = newNativeCognitoUser.User?.Username || &amp;#39;username-not-found&amp;#39;

    await linkCognitoUserAccounts({ cognitoUsername: newNativeCognitoUserUsername, userPoolId, providerName: providerNameWithCorrectCapitalization, providerUserId })
  }

  return event
}

function listCognitoUsersWithEmail ({ userPoolId, email }) {
  return cognitoIdpClient.send(new ListUsersCommand({
    UserPoolId: userPoolId,
    Filter: `email = &amp;#39;${email}&amp;#39;`,
  }))
}

function linkCognitoUserAccounts ({ cognitoUsername, userPoolId, providerName, providerUserId }) {
  return cognitoIdpClient.send(new AdminLinkProviderForUserCommand({
    SourceUser: {
      ProviderName: providerName,
      ProviderAttributeName: &amp;#39;Cognito_Subject&amp;#39;,
      ProviderAttributeValue: providerUserId,
    },
    DestinationUser: {
      ProviderName: &amp;#39;Cognito&amp;#39;,
      ProviderAttributeValue: cognitoUsername,
    },
    UserPoolId: userPoolId,
  }))
}

async function createCognitoUser ({ userPoolId, email, givenName, familyName, name }) {
  const fullName = getFullName({ name, givenName, familyName })
  const createdCognitoUser = await cognitoIdpClient.send(new AdminCreateUserCommand({
    UserPoolId: userPoolId,
    // Don&amp;#39;t send an email with the temporary password
    MessageAction: MessageActionType.SUPPRESS,
    Username: email,
    UserAttributes: [
      {
        Name: &amp;#39;name&amp;#39;,
        Value: fullName,
      },
      {
        Name: &amp;#39;email&amp;#39;,
        Value: email,
      },
      {
        Name: &amp;#39;email_verified&amp;#39;,
        Value: &amp;#39;true&amp;#39;,
      },
    ],
  }))

  // Set password to confirm user; AdminConfirmSignUp doesn&amp;#39;t work on manually created users
  await setCognitoUserPassword({ userPoolId, email })

  return createdCognitoUser
}

function setCognitoUserPassword ({ userPoolId, email }) {
  const password = randomBytes(16).toString(&amp;#39;hex&amp;#39;)

  return cognitoIdpClient.send(new AdminSetUserPasswordCommand({
    Password: password,
    UserPoolId: userPoolId,
    Username: email,
    Permanent: true,
  }))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This handler starts by checking the &lt;code&gt;event.triggerSource&lt;/code&gt; to determine if the signup is from an External Identity Provider (e.g. Google) or if it&amp;#39;s with Email + Password. If the signup is with Email + Password and the &lt;code&gt;AUTO_VERIFY_USERS&lt;/code&gt; environment variable is set, the account is automatically verified and confirmed. The user doesn&amp;#39;t receive a verification email and is instantly signed in, reducing friction during the critical user registration flow.&lt;/p&gt;
&lt;p&gt;:::caution
There are good reasons to require email confirmation (such as GDPR compliance). It&amp;#39;s advised to only auto-verify in development/preview environments (Code Genie projects are configured this way out-of-the-box)
:::&lt;/p&gt;
&lt;p&gt;If the signup is from an External IDP we first get the provider name from the &lt;code&gt;event.userName&lt;/code&gt; property. This property&amp;#39;s value looks like &lt;code&gt;&amp;#39;google_108986458847054040795&amp;#39;&lt;/code&gt;, so we split on &lt;code&gt;&amp;#39;_&amp;#39;&lt;/code&gt; to get the &lt;code&gt;providerName&lt;/code&gt; and &lt;code&gt;providerUserId&lt;/code&gt; (the user&amp;#39;s ID within the External IDP). We then lookup &lt;code&gt;providerName&lt;/code&gt; in the &lt;code&gt;providerNamesLowerCaseLookup&lt;/code&gt; map to get the correct capitalization required by Cognito&amp;#39;s &lt;code&gt;adminLinkProviderForUser&lt;/code&gt; API. If no matching provider is found within the map, we throw an error.&lt;/p&gt;
&lt;p&gt;:::note
This is an unfortunate extra step we&amp;#39;re forced to do and it would be great if the Cognito Trigger included the correct capitliazation of the provider name for us.&lt;/p&gt;
&lt;p&gt;Every other resource that I found on this topic suggests simply uppercasing the first character in &lt;code&gt;providerName&lt;/code&gt;. This doesn&amp;#39;t work if you have a SAML/OIDC provider and specify a name like &lt;code&gt;&amp;#39;GoogleSaml&amp;#39;&lt;/code&gt; (which is what we named it in our previous article).
:::&lt;/p&gt;
&lt;p&gt;Next we query Cognito for an existing account under that Email, and if one exists we simply link the External IDP account to it. Otherwise, we create a native Cognito User with a random password and then link the External IDP account.&lt;/p&gt;
&lt;p&gt;:::note
This approach handles both scenarios where:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a user first registers with Email + Password and later signs in with Google/SAML&lt;/li&gt;
&lt;li&gt;a user first registers with Google/SAML and later signs in with Email + Password&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Cognito doesn&amp;#39;t support linking an Email + Password account to an &amp;quot;External&amp;quot; account. By first creating a native Cognito account in scenario 2, we enable the user to login with their email address in the future. Ideally, the user would be able to go through the normal Register with Email + Password flow and things would &amp;quot;just work&amp;quot;. Unfortunately, they must use the Forgot Password flow to reset their password before they can sign in with this method.
:::&lt;/p&gt;
&lt;h3&gt;Pre Token Generation Lambda Handler&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { AdminUpdateUserAttributesCommand, CognitoIdentityProviderClient } from &amp;#39;@aws-sdk/client-cognito-identity-provider&amp;#39;
import axios from &amp;#39;axios&amp;#39;
import type { PreTokenGenerationTriggerEvent } from &amp;#39;aws-lambda&amp;#39;
import { createUser, getUser, updateUser } from &amp;#39;../api/controllers/user&amp;#39;
import getFullName from &amp;#39;./getFullName&amp;#39;

const cognitoIdpClient = new CognitoIdentityProviderClient({ region: &amp;#39;us-west-2&amp;#39; })

interface Identity {
  providerType: &amp;#39;Google&amp;#39; | &amp;#39;Facebook&amp;#39; | &amp;#39;SAML&amp;#39; | &amp;#39;OIDC&amp;#39; | string
}

export async function handler (event: PreTokenGenerationTriggerEvent) {
  const {
    userPoolId,
    userName,
    request: {
      userAttributes,
    },
  } = event
  // Don&amp;#39;t await here so that we can run the Dynamo and Cognito operations in parallel
  const syncUserToDynamoPromise = syncUserToDynamo(userAttributes)
  let setUserEmailVerifiedTruePromise

  const { identities, email } = userAttributes

  if (email &amp;amp;&amp;amp; identities) {
    const identitiesArray: Identity[] = JSON.parse(identities)
    const hasExternalIdentity = identitiesArray.some(identity =&amp;gt; [&amp;#39;Google&amp;#39;, &amp;#39;Facebook&amp;#39;, &amp;#39;SAML&amp;#39;, &amp;#39;OIDC&amp;#39;].includes(identity.providerType))

    if (hasExternalIdentity) {
      // Cognito has a &amp;quot;feature&amp;quot; that sets all attributes to their default values when not present on the external identity provider.
      // This results in the email_verified being set to false on each login, which causes features like forgot password to not work.
      // Force it back to email_verified=true.
      setUserEmailVerifiedTruePromise = setUserEmailVerifiedTrue({ userPoolId, username: userName })
    }
  }

  await Promise.all([syncUserToDynamoPromise, setUserEmailVerifiedTruePromise])

  return event
}

async function setUserEmailVerifiedTrue ({ userPoolId, username }) {
  return cognitoIdpClient.send(new AdminUpdateUserAttributesCommand({
    UserPoolId: userPoolId,
    Username: username,
    UserAttributes: [{Name: &amp;#39;email_verified&amp;#39;, Value: &amp;#39;true&amp;#39;}],
  }))
}

async function syncUserToDynamo (userAttributes) {
  const {
    sub: userId,
    email,
    given_name: givenName,
    family_name: familyName,
    name,
    picture,
  } = userAttributes

  const fullName = getFullName({ name, givenName, familyName })
  const existingUser = await getUser({ userId })

  if (existingUser) {
    // If the user doesn&amp;#39;t have an avatar set and one is available from the external IDP: set it to the user&amp;#39;s avatar
    // NOTE: Uploading user avatar to S3 instead of storing the base64 in DynamoDB is a better solution.
    if (picture &amp;amp;&amp;amp; !existingUser.data.avatar) {
      const base64EncodedProfilePicture = await fetchAndBase64EncodeImage(picture)
      await updateUser({ userId, user: { avatar: base64EncodedProfilePicture }})
    }
    return
  }

  const user: any = {
    name: fullName,
    email,
  }

  if (picture) {
    const base64EncodedProfilePicture = await fetchAndBase64EncodeImage(picture)

    if (base64EncodedProfilePicture) {
      user.avatar = base64EncodedProfilePicture
    }
  }

  return createUser({ userId, user })
}

async function fetchAndBase64EncodeImage(imageUrl) {
  try {
    const image = await axios.get(imageUrl, {responseType: &amp;#39;arraybuffer&amp;#39;})
    const base64 = Buffer.from(image.data).toString(&amp;#39;base64&amp;#39;)
    return base64 ? `data:image/png;base64, ${base64}` : null
  } catch(error: unknown) {
    // If we encounter an error while fetching/encoding the image, it&amp;#39;s better to just log and continue.
    // The user won&amp;#39;t have their profile picture, but at least they&amp;#39;ll be registered/logged in!
    console.error(&amp;#39;cognito.preTokenGeneration.syncUserToDynamo.fetchAndBase64EncodeImage.error&amp;#39;, {
      errorName: (error as Error).name,
      errorMessage: (error as Error).message,
    })
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This handler&amp;#39;s primary purposes are to create a user record in DynamoDB if one doesn&amp;#39;t exist (&lt;code&gt;syncUserToDynamo&lt;/code&gt;), and to ensure the Cognito user remains verified (&lt;code&gt;setUserEmailVerifiedTrue&lt;/code&gt;). Both operations are executed in parallel to improve performance thanks to our &lt;code&gt;await Promise.all&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;syncUserToDynamo&lt;/code&gt; queries DynamoDB for a User item with the Cognito User&amp;#39;s ID (&lt;code&gt;event.request.userAttributes.sub&lt;/code&gt;), and if one doesn&amp;#39;t exist it&amp;#39;s inserted into DynamoDB. If a user already exists but doesn&amp;#39;t have a profile picture set and one is available through the sign in method they&amp;#39;re using, it updates DynamoDB with the profile picture (this happens when they first signed in with Email or SAML which doesn&amp;#39;t expose a profile picture, and then signed in with Google). You could also choose to sync other data here, e.g. if you want to update the user&amp;#39;s name or other data to match what&amp;#39;s in the External IDP.&lt;/p&gt;
&lt;p&gt;:::tip
It&amp;#39;s recommended to store User data in DynamoDB, and let Cognito just handle the auth. Consider storing the profile picture in S3 instead.
:::&lt;/p&gt;
&lt;p&gt;If the Cognito User has External Identities linked to it, &lt;code&gt;setUserEmailVerifiedTrue&lt;/code&gt; is called to force the &lt;code&gt;email_verified&lt;/code&gt; attribute back to true. There is an unfortunate Cognito &amp;quot;feature&amp;quot; that sets all User attributes to their default value when signing in with an External IDP that doesn&amp;#39;t return that value. For &lt;code&gt;email_verified&lt;/code&gt; this is &lt;code&gt;false&lt;/code&gt;. We must keep &lt;code&gt;email_verified=true&lt;/code&gt;, otherwise the user won&amp;#39;t be able to sign in with their Email + Password or use the &amp;quot;Forgot Password&amp;quot; flow.&lt;/p&gt;
&lt;p&gt;:::note
The &lt;code&gt;createUser, getUser, updateUser&lt;/code&gt; functions are just wrappers around a &lt;a href=&quot;https://github.com/jeremydaly/dynamodb-toolbox&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DynamoDB Toolbox Entity&lt;/a&gt;.
:::&lt;/p&gt;
&lt;h3&gt;getFullName&lt;/h3&gt;
&lt;p&gt;A small helper function for getting the user&amp;#39;s full name from the External IDP when it doesn&amp;#39;t support mapping one.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Google SAML doesn&amp;#39;t allow mapping the full name field, so we instead must construct this ourselves based on Given Name and Family Name.
export default function getFullName({ name, givenName, familyName}) {
  let fullName = name

  // If the name is JUST the given name, AND a family name exists: concatenate them into full name.
  // This is especially useful in Google SAML where it doesn&amp;#39;t include a full name attribute.
  if (givenName &amp;amp;&amp;amp; familyName &amp;amp;&amp;amp; (!fullName || fullName === givenName)) {
    fullName = `${givenName} ${familyName}`
  } else if (!fullName) {
    fullName = givenName || familyName
  }

  return fullName
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;End&lt;/h2&gt;
&lt;p&gt;Ideally, multi-account linking between External Identity Providers and native Cognito Users would be built into Cognito, but thanks to the power of Lambda Triggers it&amp;#39;s something we&amp;#39;re able to implement ourselves.&lt;/p&gt;
&lt;p&gt;If you want to build a full stack AWS application that includes functionality like this and much more out of the box, you should try &lt;a href=&quot;https://codegenie.codes&quot;&gt;Code Genie&lt;/a&gt; 🧞‍♂️&lt;/p&gt;
&lt;p&gt;:::tip
The complete source code is available by generating a Code Genie app and specifying &lt;code&gt;&amp;#39;Google&amp;#39;&lt;/code&gt; and &lt;code&gt;&amp;#39;SAML&amp;#39;&lt;/code&gt; &lt;code&gt;idp&lt;/code&gt; options:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npx @codegenie/cli generate --name &amp;quot;Todo&amp;quot; \
--idp &amp;#39;Google&amp;#39; --idp &amp;#39;SAML&amp;#39; \
--description &amp;quot;A todo list app that lets users create lists and add items to the list. Items should have a title, description, be markable as completed, have a due date, and have an image.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://codegenie.codes&quot;&gt;Code Genie&lt;/a&gt; generates a Full Stack AWS Serverless application based on your own data model, including a React (Next.js) Web App, Serverless Express REST API, DynamoDB Database, and Cognito Auth.
:::&lt;/p&gt;
</content:encoded><category>cognito</category><category>aws</category><category>lambda</category></item><item><title>Introducing Code Genie 🧞‍♂️</title><link>https://codegenie.codes/blog/introducing-code-genie/</link><guid isPermaLink="true">https://codegenie.codes/blog/introducing-code-genie/</guid><description>Today we’re excited to launch Code Genie — the FASTEST way to build software! With a single AI-powered command wish, Code Genie generates a custom Full Stack Serverless AWS Application based on your description. In under a minute!

</description><pubDate>Wed, 31 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today we&amp;#39;re excited to launch Code Genie -- the FASTEST way to build software! 🚀&lt;/p&gt;
&lt;p&gt;With a single &lt;del&gt;AI-powered command&lt;/del&gt; wish 🧞‍♂️, Code Genie generates a custom Full Stack Serverless AWS Application based on your description. In under a minute! Try it out for yourself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npx @codegenie/cli generate --name &amp;quot;Wildlife Rescue&amp;quot; \
--description &amp;quot;An app that lets users upload photos, location, time, species and other information so that Wildlife Rescuers can get notified and respond to reports of injured wildlife in their area.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[Deploy 🚀]
You can include the &lt;code&gt;--deploy&lt;/code&gt; flag to instantly deploy to AWS, or you can run &lt;code&gt;npm run init:dev&lt;/code&gt; within the project directory after it&amp;#39;s generated.
:::&lt;/p&gt;
&lt;p&gt;Projects include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Next.js Web App hosted on AWS Amplify Hosting&lt;/li&gt;
&lt;li&gt;Serverless Express REST API on Lambda and API Gateway&lt;/li&gt;
&lt;li&gt;DynamoDB Database&lt;/li&gt;
&lt;li&gt;Cognito User Pools for Auth&lt;/li&gt;
&lt;li&gt;CDK for all AWS infrastructure&lt;/li&gt;
&lt;li&gt;Local Development&lt;/li&gt;
&lt;li&gt;Multi-environment support (dev, staging, production)&lt;/li&gt;
&lt;li&gt;Custom domain names for Web App and API&lt;/li&gt;
&lt;li&gt;Custom Emails&lt;/li&gt;
&lt;li&gt;CI/CD via GitHub Actions (WIP)&lt;/li&gt;
&lt;li&gt;Much more (and even more to come)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can&amp;#39;t wait to see what you build! Join us on &lt;a href=&quot;https://discord.gg/YJ9gQhheyn&quot;&gt;Discord&lt;/a&gt;, and follow us on &lt;a href=&quot;https://twitter.com/CodeGenieCodes&quot;&gt;Twitter&lt;/a&gt; and &lt;a href=&quot;https://www.linkedin.com/company/code-genie-codes&quot;&gt;LinkedIn&lt;/a&gt; for support and announcements.&lt;/p&gt;
</content:encoded><category>announcement</category></item></channel></rss>