Migrating a Legacy App to Cloud Native – Part 7

This is part 7 in a series documenting my journey migrating my progressive web app, called SqAC, to AWS cloud native. If you haven’t been following it before now, here are the previous posts:

Now in part 7, I’ll enhance user accounts first with Google Sign-in (federation), and then with local (guest) accounts for users to try out the app first.

But first…

Updated Dependencies

This series isn’t about Angular, so I won’t go into details. However I found that to update to the latest Amplify libraries, I had to upgrade Angular from 6.0 to 8.0. At the same time, I updated various other dependencies.

Just about a week before AWS deprecated Node.js v8 in Lambdas, Amplify finally added support for Node.js v10 and so that update has been performed as well.

Pull Requests of code changes:

Add Google Sign-In

The new cloud native version of SqAC gained user authentication via Cognito back in part 3. At the time, I didn’t add any federated sign-ins. The legacy app supported Google and Facebook sign-in, but I found that Facebook sign-in wasn’t popular. With Cognito accounts available in this new app, there’s no true need for any federated accounts, but personally I like using my Google account for authentication because it’s easier than tracking another account. Besides, this is another feature of Amplify to explore!

Amplify’s documentation to setup Google is here. Yes, I actually used a bit of Google Cloud Platform for my AWS native app. Breath. It’s okay.

$ amplify auth update
Scanning for plugins...
Plugin scan successful
Please note that certain attributes may not be overwritten if you choose to use defaults settings.

You have configured resources that might depend on this Cognito resource.  Updating this Cognito resource could have unintended side effects.

Using service: Cognito, provided by: awscloudformation
 What do you want to do? Update OAuth social providers
 Select the identity providers you want to configure for your user pool: Google
  
 You've opted to allow users to authenticate via Google.  If you haven't already, you'll need to go to https://developers.google.com/identity and create 
an App ID. 
 
 Enter your Google Web Client ID for your OAuth flow:  <hidden>
 Enter your Google Web Client Secret for your OAuth flow:  <hidden>
Successfully updated resource sqacauth locally

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

An amplify push later, and Cognito is setup to use Google Sign-In. Once again, Amplify makes authentication easy! 🥳

In my Angular application’s account page, I inject AmplifyService as amplifySvc and added the function:

signInWithGoogle() {
   this.amplifySvc.auth().federatedSignIn({provider: 'Google'});
}

A button on the page calls this function. This takes me to the familiar Google account selection page, and then navigates back to my application. There were a few problems though:

  1. It doesn’t recognize that I’m signed in until I navigated to the account page. Simply rendering the <amplify-authenticator> component triggered the sign-in to be recognized, but I don’t want that on the app landing page.
  2. My name is “Unknown”.
  3. No photograph of myself was received.

We’ll get to problem #1 in a moment. The second two problems caught me by surprise because the legacy SqAC application also supported Google sign-in, through Passport. In the Cognito AWS Console, I was able to select attribute mapping for name and picture (email and sub were already checked). This resolved receiving my name and photograph. This was easy to do, but I already knew where to look because I have experience with OAuth and Cognito.

screenshot from Cognito console

Problem #1 remains: sign-in isn’t recognized right away. Upon sign-in with Google, the app reloads and I see the Cognito user information in browser local storage. Yet, Auth.authStateChange$ reports a state of “signedOut”. Explicitly requesting the current auth user also returns “No current user”. If I reload the page, then the state becomes “signedIn” and all is well. This seems like a bug. 🐞 I added information to the existing Amplify-js Issue #4621. After some messages exchanged and some experimentation, here’s a copy of my comment after I resolved this issue:

Okay, found two fixes based on your comments @Amplifiyer, both involving using the Hub to listen for signIn. I first used this as a direct analogue to monitoring Auth.authStateChange$. The Hub told me when a federated user signed in/out, and authStateChange$ for a user pool user. I still wanted to land on my accounts page though. So I changed it to navigate to the account page on Hub signIn event. Doing that caused Auth.authStateChange$ to trigger when the auth component rendered, so I just rely on that. Here’s the code change: kernwig/sqac-amplify@8de758e

Note: The Hub doesn’t play nice with Angular, thus the need to use NgZone directly.

Amplify Hub is an in-browser publish-subscribe capability that is used by Amplify and available for us to use too. In Angular, services and RxJS provide for similar behavior and so hadn’t used Hub before this.

Auth Secrets

Upon going to commit these changes to github, I noticed that my Google Web Client Secret was stored in amplify/team-provider-info.json. Whether or not to commit this file is discussed in this new section of the Amplify documentation. I ended up removing the file from source control.

Secure Hosting & Authentication

To take federated login live, I needed HTTPS hosting. (OAuth doesn’t allow http callback URLs.) Thus I had to update my hosting configuration to include CloudFront, the AWS Content Delivery Network:

$ amplify update hosting
? Specify the section to configure CloudFront
CloudFront is NOT in the current hosting
? Add CloudFront to hosting Yes
? default object to return from origin index.html
? Default TTL for the default cache behavior 86400
? Max TTL for the default cache behavior 31536000
? Min TTL for the default cache behavior 60
? Configure Custom Error Responses No
? Specify the section to configure Publish
You can configure the publish command to ignore certain directories or files.
Use glob patterns as in the .gitignore file.
? Please select the configuration action on the publish ignore. exit
? Specify the section to configure exit
$ amplify publish

This deployed SqAC to https://d3l0j9nusq7n6r.cloudfront.net. I had to add this to the app’s credentials at Google, and then to my app:

$ amplify update auth
Please note that certain attributes may not be overwritten if you choose to use defaults settings.

You have configured resources that might depend on this Cognito resource.  Updating this Cognito resource could have unintended side effects.

Using service: Cognito, provided by: awscloudformation
 What do you want to do? Add/Edit signin and signout redirect URIs
 Which redirect signin URIs do you want to edit? (Press <space> to select, <a> to toggle all, <i> to invert selection)
 Do you want to add redirect signin URIs? Yes
 Enter your new redirect signin URI: https://d3l0j9nusq7n6r.cloudfront.net/
? Do you want to add another redirect signin URI No
 Which redirect signout URIs do you want to edit? (Press <space> to select, <a> to toggle all, <i> to invert selection)
 Do you want to add redirect signout URIs? Yes
 Enter your new redirect signout URI: https://d3l0j9nusq7n6r.cloudfront.net/
? Do you want to add another redirect signout URI No
Successfully updated resource sqacauth locally

$ amplify push

But wait, this doesn’t work! 😱The Javascript library isn’t smart enough to choose between this CloudFront URL or the localhost one I already had. The aws-exports.js file contains both URLs comma-separated, but at runtime just the first one (localhost, since it was there first) is used. This utterly fails when running the app from CloudFront. See the issue report here. How this gap continues to exist, I do not understand. Surely nearly all front-end developers work first locally (localhost) before deploying to the cloud (CloudFront)? I added this hack to the Angular main.ts to handle the problem:

import awsconfig from './aws-exports';

// Choose an OAuth config based on environment
const redirectSignInOptions = awsconfig.oauth.redirectSignIn.split(',');
const redirect = environment.production
   ? redirectSignInOptions.find(s => s.startsWith('https'))
   : redirectSignInOptions.find(s => s.includes('localhost'));
awsconfig.oauth.redirectSignIn = redirect;
awsconfig.oauth.redirectSignOut = redirect;

Here’s another surprise: by default, amplify publish does not invalidate the CloudFront cache. 🙄 That means after we publish and reload in browser, we will not see our changes! (Unless we wait a day – not even clearing the browser cache will help.) To actually publish and test try out the changes, we must explicitly pass an option: amplify publish --invalidateCloudFront. Yes, this is documented here and of course I won’t remember this next time I publish and so I added a script to my package.json for it.

Pull Request: Part 7 – Add Google Auth Provider

Guest Access

One of my requirements when migrating to AWS was to add guest access. I’ve had some complaints from folks who wanted to try out my app, but were turned off by having to authenticate with Google or Facebook in order to do anything. One way of appeasing this concern was to use a Cognito User Pool, so that users don’t need to link a social account. More impactful though is to allow basic functionality without creating an account at all. In SqAC, I called this a “local account” and provided some warnings about data loss without cloud backup. However, this provides a means for someone to play with the app and decide if it is something they want to go farther with before providing an email address.

Most of my work consisted of changes to my client application itself. I was able to load public content from storage (AWS S3) without difficulty thanks to the work I did in part 4. User data is stored in the browser’s IndexedDB. When there’s a cloud account, this data is copied up to S3 as well; for local accounts this step is skipped. Everything was going great until I hit the last feature to test: search via AppSync. This failed with an exception thrown saying “no current user”. 🤔 This surprised me – I figured not including an @auth on my GraphQL type meant “no auth” and thus no need for a user. 🤷‍♂️Nope. A search brought me to some background and ultimately I stumbled right to the mass of Amplify documentation that tells us how to do public authentication. It says…

👉Note: Don’t do this! Read on! 👈
First, use API KEY authentication type:

$ amplify update api
? Please select from one of the below mentioned services: GraphQL
? Choose the default authorization type for the API API key
? Enter a description for the API key: Public access
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.

The following types do not have '@auth' enabled. Consider using @auth with @model
  - Collection
Learn more about @auth here: https://aws-amplify.github.io/docs/cli-toolchain/graphql#auth 

GraphQL schema compiled successfully.

Second, use auth mode API KEY and @auth(rules: [{allow: public}]) on the GraphQL type.

I was nervous about the 7 day (default) expiration. Upon an amplify push, the CLI showed me a value for the “GraphQL API KEY” and it is set in my aws-export.js. My app was able to successfully perform the GraphQL query without creating a Cognito user, but it was clear that this would stop working in a week. I could increase this to 365, but that only delays the problem. Amplify CLI issue 1450 has conversation of people struggling with this.

Back in that  Amplify documentation, it first says that API KEY must be used for public access, then gives a second example with IAM authentication for public access. Why not try that? 🤷‍♂️

I updated my GraphQL with @auth(rules: [{allow: public, provider: iam}]) and then on the CLI:

$ amplify update api
? Please select from one of the below mentioned services: GraphQL
? Choose the default authorization type for the API IAM
? Do you want to configure advanced settings for the GraphQL API No, I am done.

GraphQL schema compiled successfully.

$ amplify api gql-compile
$ amplify push

A bit of testing with a cloud account and a local (guest) account – and it’s working! 🎉

Call out to the Amplify team: This bit of documentation can be made clearer that there are two approaches, and how to choose between them.

Conclusion

As with everything Amplify (and honestly, doing new things in general) adding Google Sign-In and guest access was harder than it looked. It took some web searching and experimentation, but ultimately was easier than it would have been without Amplify.

Coming next time…

Cut over! The legacy app is still at https://sqac.fanello.net/, and this new one is at https://d3l0j9nusq7n6r.cloudfront.net. One has a slightly friendly name than the other. 😉

I just paid for another month of DigitalOcean hosting for the legacy app, and hope for it to be the last.

Until next time. 😎

Leave a Reply