Implementing SSO with Amazon Cognito as Identity Provider (IdP) for the Tasks Service.
So far, we have implemented our Timer Service application using Amplify with Cognito integration for our authentication process. But this component is entirely coupled to our code base, which is a drawback if tomorrow we need to build another app that belongs to our business domain. So it’s better to deploy an Identity Provider (IdP) service that all our apps must integrate to validate the user session token. So, in this tutorial, our objective is to deploy an IdP using Amazon Cognito using Amplify as we did before, but in a standalone project.
I prefer to use Amplify instead of CloudFormation because we are more familiar with the Amplify CLI. Behind the scenes, Amplify uses CloudFormation to deploy the necessary resources on AWS. So you can see the created templates in the CloudFormation console if you want to use those templates in the future. Furthermore, we can customize our auth module in more detail using Amplify.
NOTE 1: You can download the IdP project’s code from my GitHub repository to review the latest changes. The Task Service source code is also available on my GitHub account. Likewise, you can pull the docker image for the API service (the backend service) from my DockerHub account and deploy it on your local environment using Docker Compose.
IMPORTANT: The last changes I made in this project are detailed in a new article, “Implementing a Multi-Account Environment with AWS.” So I suggest you go to the new one after reading this article to see the latest project improvements.
To complete this guide, you’ll need the following:
- An AWS account.
- AWS CLI (version 2).
- Amplify CLI.
- AWS Copilot CLI.
- GraalVM 22.1.0 with OpenJDK 17 (you can use the SDKMAN tool).
- Apache Maven 3.8 or superior.
- Docker and Docker Compose.
- IntelliJ or Eclipse IDE.
Amazon Cognito as Identity Provider (IdP)
You must create a new project. I don’t provide a Git repo for this purpose because this is a simple Node project, and after you deploy the IdP provider, you only will have an “amplify” directory. Still, for security reasons, I cannot share this directory. So it would be best if you created yours using Amplify:
$ amplify init
Then, you must add the authentication support:
$ amplify add auth
I share some of the parameters I used for this new project:
NOTE 2:Too enable Multifactor Authentication (MFA) for your IdP, you can read a tutorial about it.
Notice in the previous image that I configured an OAuth flow. I entered one page for the redirection of the user back to the app after a successful signed-in. The second redirect the user to the logout page after the session ends. The rest of the configurations are the same as we have used in the tutorials.
Once the configuration is done, push those changes to AWS:
$ amplify push
At the end of the command execution, you must see something like this:
Notice that Cognito provides a Hosted UI Endpoint at the end of the command execution. Copy the second endpoint and paste it into a new browser tab to see what happens:
As you can see, the Hosted UI endpoint validates the user’s credentials. We must also send some additional URL parameters required by the Cognito IdP.
IMPORTANT: The Hosted UI endpoint is not an OpenID Connect (OIDC). It is a web application managed by Cognito that we must use in our OAuth Flow. The OIDC endpoints configured by Cognito look like this:
So, for our configured Cognito User Pool, we can get the OIDC configuration using the standardized “.well-known/openid-configuration” resource:
This information is useful when configuring OIDC clients because they can discover the internal resources automatically and use them to interact with the OIDC server.
Now we know the differences between the 2 endpoints; the OIDC and the OAuth endpoints. We can move to the article’s next section to update our Timer Service App to use the Cognito Hosted UI.
Modifying the Timer Service App
The changes in this section are significant. We need to do some refactoring into the app. The good news is that I constructed the Timer Service App modularly, so the changes are more focused on the auth module.
First, deploy the Amplify project for the Timer Service on AWS. You can use the “run-scripts.sh” bash script inside the “hiperium-city-tasks” directory:
Choose option 1. Remember that our Timer Service from now doesn’t have an auth module configured with Amplify. We only create the Amplify project on AWS for later use. These are the configurations I used:
Then, we need to update the “environment.ts” file with the following “authConfig” declaration:
Notice that we’re using the “angular-oauth2-oidc” dependency. This new configuration helps us to initiate the OIDC client from our Ionic app. You can get all those parameters in the outputs section from the CloudFormation console in the IdP stack:
Don’t forget to declare the OIDC module in the “app.module.ts” file:
Then, we need to create an Angular service that initiates the OIDC client when rendering the application:
As we’re not using the Amplify-Cognito dependency in our project, the web pages and the reactive components are not required. So the new structure of our “auth” module is the following:
Notice that I created a new component called “home.” This component is the page used for the login and logout redirection in the OAuth Flow. Also, notice the decrease in the features used in the auth module. That’s because we’re directly centralizing the Auth component using the Cognito IdP Hosted UI.
Other significant updates in components like the AuthGuardservice and AuthInterceptorService now must use the AuthService for their internal operations.
Finally, the AppComponent is updated too to use the new AuthService. Indeed, the AppComponent initializes the AuthService in the constructor section and subscribes to an event triggered when a user is logged in to the application:
Now, it’s time to deploy our backend service using Docker Compose to validate these significant changes. If you don’t have the local API image built in your local environment, execute the following command:
$ docker compose -f utils/docker/docker-compose.yml build
Then, update the “dev.env” file with the new Cognito User Pool ID and execute the following command to start the local cluster:
$ docker compose -f utils/docker/docker-compose.yml up
Finally, open a new terminal tab to build and publish the Timer Service app locally. Execute the following commands in the Ionic project’s folder:
$ ionic buid
$ ionic serve
The last command opens a new browser tab with the home page of the Timer Service application:
Click on the “Login” button to be redirected to the Cognito Hosted UI login page, and enter the credentials of your user:
After validating your credentials, the Hosted UI redirects to the home page as we configured earlier:
Notice that the left menu is updated with the main menu loaded for the logged user account. If you click on the “Tasks” button, you will be redirected to the original tasks page:
So far, our configurations are working locally. In the next section, let’s deploy all these changes to AWS and host our Ionic/Angular app into Amplify.
Deploying to AWS
Now, we must deploy the backend service to AWS. Again, you can use the bash script for this purpose.
Backend Service (API)
Choose option 2 to deploy the required services into AWS:
NOTE 3: The backend service is deployed using the latest image version from the DockerHub website. If you want to build the image first before pushing it to the Amazon ECR service, you must update the “manifest.yml” file with the following content:
Amazon API Gateway
Now, it’s time to deploy our API Gateway. So, choose option 3 in our running bash script, and after a few minutes, the API Gateway appears as created in the CloudFormation console:
Updating Ionic Environment Values
So far, we have deployed the backend service on the Amazon ECS service and created a new Amazon API Gateway. But our Timer Service application doesn’t know the endpoints of these created services. So, choose option 4 in our running bash script to update the “environment.dev.ts” file with the corresponding endpoints.
Notice that the bash script also commits and pushes the changes made to this file to the Git repository. This activity is essential because the Amplify service uses those values to compile and publish the Timer Service App into a Hosted environment—more in the next section.
We must configure the hosting for our app using the Amplify service. So, choose option 5 of our running bash script and select the options marker as blue, as you will see in the following image:
This command opens a new browser tab in the Amplify service for the Timer Service project. Then click on the “Hosting environments” tab and select your Git provider:
In the next step, choose the Git repository and branch that Amplify must use to connect and pull the latest pushed changes. Also, Amplify configures a “Continuous Deployment” pipeline:
Next, select the environment and the IAM role used by Amplify to deploy the dependent resources on AWS:
The final step is to review the information entered:
After you click on the “Save and deploy” button, the Amplify service starts the pipeline using the last commit made in your Git repository:
Meanwhile, you can press an enter key in your terminal window to finish the last command. You will see a message with the created Amplify domain and the Git branch used to host your application on AWS:
But at this point, our pipeline fails. If you go to the Amplify console, you will see something like this:
And in the Frontend section, you must see the log errors produced:
I tried to find the node version used by Amplify to build our app, and it uses version 14. You can check this in the “Provision” tab:
The solution is to create a custom “amplify.yml” file in our project’s root directory to indicate the Node version that Amplify must use. But notice in the previous image that the latest version that Amplify can use is the 17 (until now). So our new file must contain the following:
NOTE 4: I’m using a different build command value: “npm run build-dev” Tha’s because we need to use the “environment.dev.ts” file we updated in the previous section. Remember that this file contains the value of the Hosted Amplify URL that our app needs for the OAuth Flow.
Let’s push this file to our Git repository to relaunch our pipeline:
After a few minutes, the pipeline must finish successfully:
We can check the logs to see if Amplify effectively uses the Node version we specified earlier. And it is:
So our pipeline is working as expected, and we can test if our app runs successfully on the Amplify Hosting.
FullStack App Testing
So now, we must use the provided URL by the Amplify Hosting service to access our application:
But there is a final step before logging into the app. Remember that we configured our IdP project using the OAuth Flow only for localhost:
And that was right because, at that point, we didn’t know the URL of the hosted application on Amplify. So we need to update the Idp project using the following command:
$ amplify update auth
And select the “Add/Edit signin and signout redirect URIs” option to add the URL of our hosted application. These are the values that I used:
NOTE 5: When we use our app in the Amplify-hosted environment, the redirection to the home page is blocked by Amplify. So for this configuration, you can notice in the previous image that I’m using the root URL for the redirection to work correctly on Amplify.
After that, push those changes to the Amplify service to take the changes:
$ amplify push
Then, go to the Cognito console to verify the changes we made:
So now, go to your Timer Service-hosted app and click on the “Login” button to access the Cognito IdP sign-in page:
After you enter your credentials, you must be redirected to the home page of the app, but this time in the Amplify-hosted environment:
Now you can navigate to the “Tasks” pages to manage the task’s timers as usual:
In the “Application” tab of the browser development tools, you can see some values of the user’s session:
If you have other apps that use the same OIDC server information, they don’t redirect you to the IdP sign-in page every time the app is rendered. That’s because we initiated the OIDC client at the app rendering time with our AuthService component:
And that’s it!! Now you have configured the Timer Service application to use an SSO, and it’s Cloud Native!!
I hope this tutorial was of interest. In my next article, I will talk about the CI/CI pipeline configuration, but this time on an AWS multi-account environment. So I’ll see you soon.