This code functions to bring together AWS Cognito with AWS Connect using the Lambda defined in the Custom AWS IDP repo. It utilizes Cognito's Hosted UI 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.
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. Also, 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 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:
<body onload="document.forms[0].submit()">
<form ... >
<!-- hidden inputs... -->
</form>
<h1>Loading...</h1>
</body>
I thought that was cool enough to keep around even though we are not using it.
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 Simple Email Service 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.
Domain-based Message Authentication, Reporting and Conformance (DMARC) helps protect an email domain against spoofing and phishing. It does this using two mechanisms:
- The Sender Policy Framework (SPF) which specifies what servers/domains are authorized to be mail-senders for a given email domain, and
- 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:
- innovation.nj.gov as a verified identity domain
- Configuring the use of Easy DKIM under "Advanced DKIM settings" with
RSA_2048_BIT DKIM signing key length and DKIM signatures enabled
- Requiring OIT to publish three CNAME records
- Configuring the Custom MAIL FROM domain aws-email.innovation.nj.gov
- Requiring OIT to publish AWS-provided DNS records
- Configuring the use of Easy DKIM under "Advanced DKIM settings" with
RSA_2048_BIT DKIM signing key length and DKIM signatures enabled
- [email protected] as a verified identity email address
- 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
- 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 <[email protected]>".
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.
Getting SMS configured required a number of steps, lead time, and also some bug-dodging trickery.
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.
Cognito links to this documentation 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.
Cognito links to this documentation 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.
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:
- Must be configured in Amplify as the
NEXT_PUBLIC_COGNITO_REDIRECT_URI
environment variable, as described in the README - Must be configured in Cognito in the App Client of the User Pool as an Allowed Callback URL
- Must be configured in Cognito under Messaging as the URL in the Invitation Message template
- Must be configured in Amplify as the
- The User Pool ID and App Client ID:
- These must be configured as environment variables in the Lambda's serverless.yml file (the other repo)
- The App Client ID must be configured in Amplify as the
NEXT_PUBLIC_COGNITO_CLIENT_ID
environment variable, as described in the README - 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
To create a user who can receive a good temporary password in their invitation email:
- Go to the User Pool, under the "Users" tab press "Create user"
- Choose "Send an email invitation"
- Provide their email address
- Set the email address as verified (otherwise the email won't be sent)
- Select "Generate a password"
- Press the button at the bottom "Create user"