One of the most common uses cases for serverless is when it is used in web applications. And if we imagine a simple web app, the first thing that comes to mind is user authentication. User authentication is very important in serverless web applications because we don’t want to expose our endpoints to the external world and we want to know who and when did what.

In this article, we’ll create a very simple serverless web application that will be able to sign up and log in users. When the users are logged in, they will be shown a message that comes from the backend. We are going to create the client part with React and will also use AWS Amplify.

For the backend part, we are going to use Amazon Cognito for the authentication, API Gateway to provide an endpoint, and AWS Lambda to provide a simple backend. We will build everything as code. 

Amazon Cognito

Amazon Cognito is a managed service from AWS that provides simple and secure user sign up, sign in, and access control. Amazon Cognito has two principal parts: Cognito User Pools and Cognito Identity Pools.

Amazon Cognito User pools enable developers to easily add functionalities that allow users to sign up for and sign in to the app, thus serving as an identity provider to maintain a user directory. User pools can be used to handle user management, storing, recovering, and new account creation in different web and native applications. User pools also serve to give permissions for different resources to the users.

For example, when using an API Gateway secured with Cognito, the users will need to be registered into some specific user pool to be able to perform a successful request from the API gateway.

User pools have client applications, which have the required permissions to perform unauthenticated calls to APIs within your application. In most serverless applications, calling unauthenticated APIs is necessary, as the users will need to be able to create an account, log in, or even retrieve their password if forgotten. When the client is calling those APIs, a client ID and client secret are needed, and the developer must make sure that hose IDs and secrets are secure, allowing for unauthenticated calls to be made solely by authorized clients. 

Amazon Cognito’s federated identities, or identity pools, enable developers to create unique identities for users of the system (the ones created with the user pools). Identity pools allow users to get authenticated using different federated identity providers, such as Facebook or Google, and gain temporary credentials to access AWS services such as Dynamo, S3, or API Gateway. 

In summary, after a user is authenticated using a Cognito user pool, an IAM role is attached to the user, who can then use this to access different AWS resources via a defined policy containing a list of all AWS resources available.

The Backend

The backend you are going to build is very straightforward. It will have a simple Lambda that will be triggered with an API Gateway. We will also build all the infrastructure you need for creating an Amazon Cognito user pool and identity pool in this backend project. 

You will first need an AWS account and Serverless Framework properly installed and configured. Run the following command in your terminal, inside an empty directory in which you would like to initiate your project for a new Serverless Framework: 

$ sls create --template aws-nodejs --name serverless-static-site

This command creates the boilerplate for your project, creating the serverless.yml and handler.js files.

The first thing you are going to do in the handler is create the Cognito infrastructure. Add Cloudformation notation at the end of the serverless.yml. You can do this at the bottom of the file after a property called “resources.”

You need to now define the Cognito user pool, so go ahead and simply give a name to the user pool and configure these two properties: AutoVerifiedAttributes and UsernameAttributes. The AutoVerifiedAttributes property will send an email to the new user with a confirmation code to validate the user email. And the UsernameAttributes property allows you to use an email address as a username. 

resources:
 Resources:
   CognitoUserPool:
     Type: AWS::Cognito::UserPool
     Properties:
       UserPoolName: ${self:custom.COGNITO_USER_POOL}
       AutoVerifiedAttributes:
         - email
       UsernameAttributes:
         - email

After this, you need to define the Cognito User Pool client application, which you can do by providing a name and making a reference to whichever user pool this client belongs.

CognitoUserPoolClient:
     Type: AWS::Cognito::UserPoolClient
     Properties:
       ClientName: ${self:custom.COGNITO_CLIENT}
       UserPoolId:
         Ref: CognitoUserPool

Now that you have the user pool and client, you can define the identity pool via a provider name and some specific properties.

CognitoIdentityPool:
     Type: AWS::Cognito::IdentityPool
     Properties:
       IdentityPoolName: ${self:custom.COGNITO_IDENTITY_POOL}
       AllowUnauthenticatedIdentities: false
       CognitoIdentityProviders:
         - ClientId:
             Ref: CognitoUserPoolClient
           ProviderName:
             Fn::GetAtt: [CognitoUserPool, ProviderName]

There are two roles that you need to define: authenticated and unauthenticated. Authenticated roles will be given to users when they sign in and will have a policy on what they can do. Unauthenticated roles are given to users who have not yet signed in and will only have a policy showing what they can do in your serverless applications when that happens.

CognitoIdentityPoolRoles:
     Type: AWS::Cognito::IdentityPoolRoleAttachment
     Properties:
       IdentityPoolId:
         Ref: CognitoIdentityPool
       Roles:
         authenticated:
           Fn::GetAtt: [CognitoAuthRole, Arn]
         unauthenticated:
           Fn::GetAtt: [CognitoUnauthRole, Arn]

The authenticated role has a name and a policy, which allows the user to invoke an API.  This policy is understandably very restrictive. The unauthenticated role looks very similar but cannot invoke any API.

You can find the full serverless.yml with all the roles here.

CognitoAuthRole:
     Type: AWS::IAM::Role
     Properties:
       RoleName: ${self:custom.COGNITO_APP_AUTH_ROLE}
       Path: /
       AssumeRolePolicyDocument:
         Version: "2012-10-17"
         Statement:
           - Effect: "Allow"
             Principal:
               Federated: "cognito-identity.amazonaws.com"
             Action:
               - "sts:AssumeRoleWithWebIdentity"
             Condition:
               StringEquals:
                 "cognito-identity.amazonaws.com:aud":
                   Ref: CognitoIdentityPool
               "ForAnyValue:StringLike":
                 "cognito-identity.amazonaws.com:amr": authenticated
       Policies:
         - PolicyName: "CognitoAuthorizedPolicy"
           PolicyDocument:
             Version: "2012-10-17"
             Statement:
               - Effect: "Allow"
                 Action:
                   - "mobileanalytics:PutEvents"
                   - "cognito-sync:*"
                   - "cognito-identity:*"
                 Resource: "*"
               - Effect: "Allow"
                 Action:
                   - "execute-api:Invoke"
                 Resource: "*"

After defining all of this, you can move on to configuring your AWS Lambda. Your simple function will return a string message, and you can modify the code here to do whatever you need it to do. In the serverless.yml you can write something like this:

functions:
 hello:
   handler: handler.hello
   events:
    - http:
       path: hello
       method: get
       cors: true
       authorizer: aws_iam

Here you are defining a function called “hello” that will be triggered with an API gateway that has a method GET in the path /hello. Also, this function will return a response that supports CORS, so you can use it in your serverless web application. And the most interesting thing in the definition of the lambda is the “authorizer” property. This tells you that in order to trigger this lambda, the call has to be authenticated. 

Deploying and Verifying

After all this is done, you can go ahead and deploy the code and make sure that everything in the backend is working before getting to work on the client-side. To see the full project, GitHub has all the backend code here.

To make sure that the Cognito pools were created, you can go to the AWS console and look for the Cognito service. There you will see two options: one to manage the user pools and another for the identity pools. If you click on each of these buttons, you will see all the things that you just deployed.

"<yoastmark

Monitoring with Epsagon

If you want to add monitoring to your serverless applications, you can do it very simply. You can just add an external layer to all functions in order to implement monitoring with Epsagon. Take a look at this article to learn more about layers, and here below is an example of how your functions will look: 

functions:
 hello:
   handler: handler.hello
   events:
    - http:
       path: hello
       method: get
       cors: true
       authorizer: aws_iam
   layer:
    - arn: arn:aws:lambda:us-east-1:066549572091:layer:epsagon-node-layer:1

After you deploy the code in your Epsagon console, you can instrument this function and start seeing your serverless application traces. An architectural diagram of the application is also available.

Architectural Diagram Epsagon for serverless applications

Architectural Diagram by Epsagon

AWS Amplify

The client is going to use AWS Amplify, a library for building cloud-enabled applications, both web and native. Amplify has a lot of features, like authentication, analytics, GraphQL, storage, hosting, push notifications, and others. You can use AWS Amplify not only with Javascript but also with iOS, Android, and React Native. 

It’s a good idea to check the library out to get to know all of its features. You will be using it in your client for two things: user authentication and then to connect to the API Gateway. Amplify makes connecting your client serverless applications to the AWS cloud smooth and easy.

The Client

You will use React to build the client serverless applications and will get started with a starter application. The first thing you need to do is clone this GitHub repository and configure your application to connect to your backend. 

In the src folder, you need to create a file called config.js and populate it with all the relevant data from your just deployed backend. 

export default {
    apiGateway: {
        REGION: 'YOUR_API_GATEWAY_REGION',
        URL: 'YOUR_API_GATEWAY_URL'
    },
    cognito: {
        REGION: 'YOUR_COGNITO_REGION',
        USER_POOL_ID: 'YOUR_COGNITO_USER_POOL_ID',
        APP_CLIENT_ID: 'YOUR_COGNITO_APP_CLIENT_ID',
        IDENTITY_POOL_ID: 'YOUR_IDENTITY_POOL_ID'
    }
};

After you have configured your application, then you can add the AWS amplify library. Simply run: 

$ npm install amplify --save

Then you can modify the index.js to read all the information from the config.js into your serverless application. You need to configure the authentication module of Amplify and then the API one, so you can use the library in different parts of your serverless applications without needing to refer to specific data from your backend.  

import React from 'react';
import ReactDOM from 'react-dom';
import Amplify from 'aws-amplify';
import { BrowserRouter as Router } from 'react-router-dom';
import App from './App';
import config from './config';
import registerServiceWorker from './registerServiceWorker';
import './index.css';
Amplify.configure({
    Auth: {
        mandatorySignIn: true,
        region: config.cognito.REGION,
        userPoolId: config.cognito.USER_POOL_ID,
        identityPoolId: config.cognito.IDENTITY_POOL_ID,
        userPoolWebClientId: config.cognito.APP_CLIENT_ID
    },
    API: {
        endpoints: [
            {
                name: 'testApiCall',
                endpoint: config.apiGateway.URL,
                region: config.apiGateway.REGION
            }
        ]
    }
});
ReactDOM.render(
    <Router>
        <App />
    </Router>,
    document.getElementById('root')
);
registerServiceWorker();

After you modify the index.js, you need to add the session management in your application. These changes will happen in the App.js file, where you will add the navigation bar on the top, two buttons for “Sign up” and “Login,” and a “Logout” button that will be visible when the users are registered. You can see the App.js file with all the changes on GitHub here

App.js serverless applications

App.js

Now you have these buttons, and creating the actual pages for “Sign up” and “Login” via code is relatively simple. Let’s see how you use AWS Amplify on these pages as well. 

In the SignUp.js page, you will need to create a new user. This user will receive an email with a code to input on the site for verification and login. In the method handleSubmit, you can see how easy it is to create a new user via the AWS Amplify Auth module.

Just call the method signUp with the username, password, and whatever other attributes you need. 

handleSubmit = async event => {
        event.preventDefault();
        this.setState({ isLoading: true });
        try {
            const newUser = await Auth.signUp({
                username: this.state.email,
                password: this.state.password,
                attributes: {
                    email: this.state.email
                },
            });
            this.setState({
                newUser
            });
        } catch (e) {
            alert(e.message);
        }
        this.setState({ isLoading: false });
    };

Another method in the Signup.js page is the “handleConfirmationSubmit” method, which is called when the confirmation code is inputted by the user. Here the AWS Amplify Auth.confirmSignUp and signIn methods are called, the first to validate the email address of the user and the second to sign in the user after confirmation is successful. 

handleConfirmationSubmit = async event => {
        event.preventDefault();
        this.setState({ isLoading: true });
        try {
            await Auth.confirmSignUp(this.state.email, this.state.confirmationCode);
            await Auth.signIn(this.state.email, this.state.password);  
            this.props.userHasAuthenticated(true);
            this.props.history.push('/');
        } catch (e) {
            alert(e.message);
            this.setState({ isLoading: false });
        }
    };

 

You can see a similar use of the AWS Amplify Auth Signup method in the Login.js page, when a registered user wants to log in to your system. 

handleSubmit = async event => {
        event.preventDefault();
        this.setState({ isLoading: true });
        try {
            await Auth.signIn(this.state.email, this.state.password);
            this.props.userHasAuthenticated(true);
            this.props.history.push('/');
        } catch (e) {
            alert(e.message);
            this.setState({ isLoading: false });
        }
    };

After you have done everything necessary for the session management, you can go ahead and call your API. The API created in the backend part calls a lambda, which simply sends back a message to the client. You will then need a message displayed in the Home.js. For this, you will need to make some changes, the code for which can be seen here.  

As seen in the following snippet, the first thing you need to do when the page loads is to make sure that you have an authenticated user on the site. If so, then you will call the testApiCAll() method, which uses AWS Amplify to call the endpoint that we configured previously. Then you  just display that message on the screen.

...
    async componentDidMount() {
        if (!this.props.isAuthenticated) {
            return;
        }
        try {
            const testApiCall = await this.testApiCall();
            this.setState({ testApiCall });
        } catch (e) {
            alert(e);
        }
        this.setState({ isLoading: false });
    }
    testApiCall() {
        return API.get('testApiCall', '/hello');
    }
    renderTestAPI(testApiCall) {
        return testApiCall;
    }
    renderTest() {
        return (
            <div className="test">
                <PageHeader>Test API call</PageHeader>
                <ListGroup>{!this.state.isLoading && this.renderTestAPI(this.state.testApiCall)}</ListGroup>
            </div>
        );
    }
    render() {
        return <div className="Home">{this.props.isAuthenticated ? this.renderTest() : this.renderLander()}</div>;
    }
}

After everything is ready in the client, you can simply run:

$ npm start
AWS Lambda Message on serverless applications

Message sent by AWS Lambda

This will start your local server, where you can try to create a new user, validate the user email, then log in with the user, and see the message that the Lambda is sending back to the user. 

Conclusion

Serverless applications can be used for many different uses cases. Simple serverless web applications are probably the most common, as it is so easy to implement and doesn’t incur any cost if there are no users. Our example project here served as a dummy function, but just imagine that that function can, in fact, perform anything that you need it to do, from fetching data from a DynamoDB table or calling a third-party resource to execute complex calculations.

If you would like to see other example use cases for using serverless applications, check out these great tutorials on using SQS and Athena.

Continue reading:

Error Handling in AWS Lambda With Wrappers

How to Avoid Cost Pitfalls by Monitoring APIs in AWS Lambda

The 5 Best Use Cases for the Serverless Beginner

Why DevOps Engineers Love AWS Lambda

Kafka, RabbitMQ or Kinesis – A Tracing Solution Comparison