Cognito Authentication Tutorial Part 1: Web Applications Without Amplify
AWS Cognito is a delightfully simple to implement solution for managed authentication. With recent updates to the console we have even given clients direct access to manage their own users. Most samples I have seen rely on using the amplify framework and often times these samples create their own login screen. The provided UI is good enough to not justify spending time on recreating the process inside of our apps in addition to the inherit security risk management hat comes along with that.
There are many ways to implement Cognito in your projects. At its core Cognito is an OpenID provider which means we will be utilizing it via its oAuth2 endpoints. This article is specifically for implementing it using their provided UI, handling the code to token exchange server side to protect our client secret, and passing the token to the client web application. The full code for this is on Github.
For this implementation I chose to use AWS Serverless Application Model (SAM) in order to bootstrap the components more quickly. The front end of this application is stored in S3, the api is handled via lambda, and everything is glued together using api gateway and domain mapping.
The client side for this demo is react but can be translated to your framework of choice with ease. The main module Authentication.js is not react specific so feel free to pull that alone into your project. I was also able to port the authentication module over to Elm and can provide that upon request.
The server side is node 14. I have left places in the code to integrate with other services if you desire. In a typical application the configuration for verification of the tokens is stored in other locations such as ssm or a database but for sake of simplicity it is hard coded here.
In this example we are going to implement an authorization code flow and use the provided login screen. In a nutshell the authorization code flow has the client request an authorization code which is returned somewhere. That code is then exchanged for a token by providing a secret. The token is validated and then used to make authenticated requests from there on as a bearer token.
Recommended by LinkedIn
The user initiates login, either by clicking a log in button or automatically by visiting the page. The client application then either redirects to or opens a new tab with the oAuth server's authorize endpoint. The url includes information such as what oAuth application (clientid) is making the request, the type of flow that is being requested (response_type), and where the authorization process should redirect to (redirect_uri) with the authorization code. The authorization code is then exchanged for a token which in this case is done server side. The request to exchange the code for a token contains the same clientid as well as a client secret that the oAuth server uses to verify the request is originating from a trusted source.
In this example we initiate the login process when the user clicks the "Login" button. A new tab is opened to the authorize endpoint in Cognito. When the correct credentials are provided or when the user is already logged in the tab navigates to our api with the authorization code. Our api, with the provided code, makes a fetch to Cognito on the token endpoint. Here it provides the client id, client secret, and the code. Cognito responds with an id token, access token, and a refresh token.
Additional steps are taken to verify the provided token via the signature. A fetch is performed to retrieve the public keys used for signing tokens. The appropriate token is selected and the signature is verified to be correct an valid. If we determine that the signature doesn't match we reject the login process.
After we have verified the token, we return it to the client embedded in a small webpage. A script embedded communicates back to our app via postmessage. From there the frontend stores the access token in a variable while the refresh token is stored in session storage. While the page is open (and the token remains valid) the access token can be passed to APIs via an Authorization Bearer header.
Additionally the refresh token stored in the session storage can be used to retrieve a new access token. The provided code detects when a refresh token exists in a session and attempts the exchange when the Authentication object is constructed.
In the next installment I hope to go into a deeper dive on setting up Cognito.
You can find the sample code in this Github repository