diff --git a/README.md b/README.md index 2233481..0b6168b 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,9 @@ This site relies on some environment variables: - `NEXT_PUBLIC_COGNITO_HOSTED_UI_URL` The base URL for the Hosted UI Cognito sign-in page associated with this - UserPool's application (the current working UserPool is "_Cognito for Connect - Call Centers_" (ID: _us-east-1_AZyvZQdFN_) and the "App integration" client is - "_Amplify App_") + User Pool's application (the current working User Pool is "_Cognito for + Connect Call Centers_" (ID: _us-east-1_AZyvZQdFN_) and the "App integration" + client is "_Amplify App_") - `NEXT_PUBLIC_COGNITO_CLIENT_ID` @@ -34,8 +34,9 @@ This site relies on some environment variables: The URL of this web app **Note:** This must be configured here as well as inside Cognito, in the - UserPool, in the App client, under "_Hosted UI_", under "_Allowed callback - URLs_" + User Pool, in the App client, under "_Hosted UI_", under "_Allowed callback + URLs_", and it should also be reflected in the Invitation Message under + "_Messaging_" - `NEXT_PUBLIC_SSO_DEFAULT_DURATION` @@ -51,3 +52,6 @@ This site relies on some environment variables: This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +See [The Inside Story](docs/THE_INSIDE_STORY.md) for more information about all +the random hurdles and gotchas that were overcome in order to get this working. diff --git a/docs/THE_INSIDE_STORY.md b/docs/THE_INSIDE_STORY.md new file mode 100644 index 0000000..245e277 --- /dev/null +++ b/docs/THE_INSIDE_STORY.md @@ -0,0 +1,186 @@ +# The Inside Story + +This code functions to bring together AWS Cognito with AWS Connect using the +Lambda defined in the +[Custom AWS IDP](https://github.com/newjersey/custom-aws-idp) +repo. It utilizes Cognito's +[Hosted UI](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-hosted-ui-user-sign-in.html) +so that the baseline login security is fully managed by AWS (with JWTs +containing login session metadata). In order to send invite-emails to new users +and manage password resetting, AWS Simple Email Service (SES) has been used to +register a verified domain and a verified email. The Cognito User Pool is also +configured to require MFA. One of those MFA methods is allowed to be SMS, and +setting up the phone number for those text messages is done in Amazon Simple +Notification Service (SNS) and Amazon Pinpoint / Pinpoint SMS. + +To make this all work took some unintuitive steps and some long lead times, so +this document serves to help anyone in the future trying to replicate or emulate +some parts of this system as well as to update the right things if this system +changes. + +## Generating (and handling) a valid SAML Response + +As described in the README for Custom AWS IDP, there was a lot of difficulty +generating a valid, signed SAML Response which AWS could consume. As well, +getting it all pieced together correctly took learning new things. + +It turns out that SAML-based SSO seems to lean heavily on browsers to do a lot +of correct handling and redirecting. This happens after the SAML Response is +POSTed to the Service Provider (SP, in our case AWS) using the +`application/x-www-form-urlencoded` content type. As well, in order for AWS to +correctly redirect the browser to the right AWS Connect instance after +successful authentication, the `RelayState` must be included in that POST body. +As can also be seen in the Custom AWS IDP repo, the `SAMLResponse` is +transmitted in base64 format. + +In order to POST the SAML Response and Relay State to AWS without forcing the +user to manually submit a form, a +[Self Submitting Form](../components/SelfSubmittingSsoForm.tsx) is used (also +using hidden inputs so the user doesn't have to see anything confusing either). + +ASIDE: It is not useful for this work now, but an earlier draft (without the +need for a frontend with a call center picker) relied on a Lambda response with +content type `text/html` and an embedded self-submitting form as the body, like +this: + +```html + +
+ +
+

Loading...

+ +``` + +I thought that was cool enough to keep around even though we are not using it. + +## Setting up emails + +We wanted to set up Cognito's *Messaging* configuration so that users would be +able to get an invitation-email with their first (temporary) password and also +so that users could receive emails to help for forgotten-password resets. As it +turned out, this was much harder than it seemed, requiring enough understanding +of email security and the AWS Email Service (SES) to get to the point where +emails could at least reach their destination without being bounced or dropped, +even though under V1, emails still end up in state junk folders. + +### DMARC = DKIM + SPF + +Domain-based Message Authentication, Reporting and Conformance (DMARC) helps +protect an email domain against spoofing and phishing. It does this using two +mechanisms: +1. The Sender Policy Framework (SPF) which specifies what servers/domains are + authorized to be mail-senders for a given email domain, and +2. DomainKeys Identified Mail (DKIM) which adds digital signatures to all + outgoing mail, allowing receivers to verify the mail sender + +The state uses DMARC (thankfully) with "relaxed alignment," which just means we +are allowed to set up the MAIL-FROM domain as a custom subdomain and still pass +DMARC. The fine details of these protocols are not important here, but with that +baseline information in place, this system required going to Amazon SES and +configuring: +1. *innovation.nj.gov* as a verified identity domain + 1. Configuring the use of *Easy DKIM* under "Advanced DKIM settings" with + *RSA_2048_BIT* DKIM signing key length and DKIM signatures enabled + 1. Requiring OIT to publish three CNAME records + 2. Configuring the Custom MAIL FROM domain *aws-email.innovation.nj.gov* + 1. Requiring OIT to publish AWS-provided DNS records +2. *callcenters@innovation.nj.gov* as a verified identity email address + 1. Requiring OIT/ops to create the email address and provide us with access + to its inbox; AWS sends a verification email to the inbox and the address + is only verified after a link in that verification email is clicked +3. Production Mode (by default, SES starts out in Sandbox Mode where emails can + only be sent to other SES verified email addresses, a request must be made to + graduate from Sandbox to Production, and I don't remember but this may also + require a request for some limit increase) + +Once the setup is complete, Cognito can be configured to use the verified email +address as the "FROM Email Address" by editing "Email" on the Messaging tab in +the User Pool. An optional "FROM Sender Name" can also be configured, such as +"New Jersey Call Centers ". + +Special thanks to the New Jersey Cyber Communication & Integration Cell (NJCCIC) +and AWS support engineers who helped us figure out how to set this up. + +## Setting up SMS + +Getting SMS configured required a number of steps, lead time, and also some +bug-dodging trickery. + +### Allowing SMS for MFA without shooting yourself in the foot + +As it turns out, at least as of this writing in early December 2023, if a User +Pool wants to allow both MFA methods (Authenticator apps + SMS message) +utilizing the Cognito Hosted UI, you __*should not*__ configure the "SMS +message" option while in the User Pool creation flow. If "SMS message" is +selected, then on the following creation flow page you will see that +`phone_number` is one of the user "Required Attributes". When the phone number +is a required attribute, it becomes impossible for the user to choose SMS as +their MFA method. Only when `phone_number` is not required does Cognito +correctly prompt the user to choose SMS or Authenticator App during their first +login. At least this is how it was for our system which does not allow +self-registration but does not necessarily know phone numbers when a user is +created. + +This seems pretty obviously like a bug in Cognito, but while I did try to urge a +support engineer to submit a bug report for me when I encountered this issue, I +am not confident that one was opened. + +### An originating phone number + +Cognito links to +[this documentation](https://docs.aws.amazon.com/sns/latest/dg/channels-sms-originating-identities.html) +for creating an originating identity. We use a Toll-Free Origination Number. +Fortunately for me, I did not set this piece up, but then unfortunately for this +documentation, I don't have a summary of any hurdles that had to be overcome +here. + +### Enabling SMS + +Cognito links to +[this documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-sms-settings.html) +for setting up SMS for Cognito. In practice, it primarily involved following the +steps which Cognito suggests on the *Messaging* tab of the User Pool: moving SNS +out of Sandbox mode (just like SES) and requesting a limit increase (or a few +increases?) to SNS and possibly also Pinpoint and Pinpoint SMS. + +Something that was a bit surprising was that even after following all the steps +and getting SMS correctly setup, there is still an *Info* box on the SMS part of +the Messaging page with advice for the steps necessary to set up SMS. For this +confusing UI, I believe an AWS support engineer did submit a request to change +it so the info box goes away when everything is correctly configured. + +Where to configure the base URL + +## Multiple truth sources + +Unfortunately with the way the code ended up in two repos and with the different +AWS products, a few pieces of information must be manually duplicated in a few +places: +* The URL for this web app: + 1. Must be configured in Amplify as the `NEXT_PUBLIC_COGNITO_REDIRECT_URI` + environment variable, as described in the README + 2. Must be configured in Cognito in the App Client of the User Pool as an + *Allowed Callback URL* + 3. Must be configured in Cognito under Messaging as the URL in the *Invitation + Message* template +* The User Pool ID and App Client ID: + 1. These must be configured as environment variables in the Lambda's + serverless.yml file (the other repo) + 2. The App Client ID must be configured in Amplify as the + `NEXT_PUBLIC_COGNITO_CLIENT_ID` environment variable, as described in the + README + 3. Perhaps this should be removed, but they are sometimes referenced either by + name or by ID in READMEs in both repos, and this should stay up-to-date + with reality + +## How to create a user? + +To create a user who can receive a good temporary password in their invitation +email: +1. Go to the User Pool, under the "Users" tab press "*Create user*" +2. Choose "*Send an email invitation*" +3. Provide their email address +4. Set the email address as verified (otherwise the email won't be sent) +5. Select "*Generate a password*" +6. Press the button at the bottom "*Create user*" \ No newline at end of file