6
Level up your API

This chapter covers

  • How authentication and authorization work in serverless applications
  • Implementing authentication and authorization in your serverless application
  • Identifying your users through social identity providers

Authentication and authorization are one of many challenges you face when developing distributed applications. The challenge lies in distributing the authorized user, along with its permissions, across all application distributed services and properly integrating third-party authentications.

This chapter shows you how to implement authentication and authorization in your serverless application by enabling it for Aunt Maria’s customers and their pizza orders. You’ll learn the difference between authentication and authorization in a serverless environment and how to implement a web authorization mechanism using AWS Cognito. Then you’ll learn how to identify your users using a social provider—specifically, Facebook.

6.1 Serverless authentication and authorization

Aunt Maria and Pierre, her mobile developer whom you so fondly remember from the previous chapter, have informed you that your API call for pizza orders is showing all pizza orders to everyone, no matter who is asking. Only employees should be able to see all orders. Customers should be able to see only their own orders. Non-customers and non-employees should not be able to see any order.

Here’s how you’ll correct this issue:

  1. Enable your application users to authenticate themselves in two ways:
    • Via email
    • Via Facebook
  2. Create a user list for your API, and restrict each user to seeing only their own orders.

Based on your experience with Express.js or other more traditional applications, you probably want to implement authentication as a part of your API and keep your users in a database table. Though that option is feasible, we recommend another method for serverless.

Most applications need authorization, and it’s usually an email/password combination. Each authorization is implemented in a similar, if not identical, manner. Therefore, serverless providers have enabled almost literal plug-and-play authentication and authorization services to handle their vast serverless resources. In your case, Amazon has AWS Cognito—a user management and synchronization service that takes care of user authentication, authorization, access management, and user and data synchronization across services.

There are two main concepts in Amazon Cognito, each with a different responsibility:

  • User pools—A service responsible for identity management. Alongside that, it also comes with a possibility of an out-of-the-box authorization. Put simply, it’s a set of directories (user pools) for your users, with a capability of providing an authorization mechanism as well. For your front-end web and mobile application, you can implement the Cognito user pool authorization mechanism using AWS Cognito SDK.

    A user pool represents a single collection of users or a user directory.

  • Federated identities (also called identity pools)—A service responsible for handling authentication providers and providing temporary authorization to AWS resources. Federated identities provide
    • Integration with social identity providers (such as Facebook, Google, and OpenId) and your Cognito user pool’s authentication identity provider
    • Temporary access to your application’s AWS resources for authenticated users

    Federated identities are directories of a single user’s identities. Those identity pools keep track of each user logging in with different identity providers. To store actual user data, identity pools require Cognito user pools.

One of the key benefits of AWS Cognito is that it authorizes the requests before they hit your serverless application. It does that by setting the authorization on the API Gateway level. If the user is not authorized, it stops the requests before they hit your Lambda function and DynamoDB table, which can potentially save a lot of wasted time and money. Even though AWS Lambda is inexpensive, additional cost cutting is always welcome.

In the case of Aunt Maria’s pizzeria, you need to have both Congito identity pools and Cognito user pools. An identity pool will allow you to integrate Facebook login, and it will also give you a temporary access to your Cognito user pool without hardcoding your AWS access token and secret in the front-end and mobile applications. A user pool will manage the database of users that can order a pizza.

For Aunt Maria’s pizzeria, you need to enable your customers to authenticate via Facebook. As shown in figure 6.1, your Facebook authentication flow should have the following steps:

  1. Ask the user to log in via Facebook in the web or mobile application.
  2. When the Facebook access token is received, send that token to a Cognito identity pool that will grant the user temporary Cognito user pools access in the browser.
  3. Use a Cognito user pool to log in or register your user. After successful login or registration, the user pool will return a JWT token.
  4. Use that JWT token to contact the Pizza API when you want to create an order or list existing orders.
figure-6.1.eps

Figure 6.1 The responsibilities of user pools and identity pools

As shown in figure 6.2, the flow for authentication with email and password is similar:

  1. Ask the Cognito identity pool for a temporary access to Cognito user pools.
  2. Log in or register to the Cognito user pool using email and password. After successful login or registration, the user pool will return a JWT token.
  3. Use that JWT token to contact the Pizza API when you want to create an order or list existing orders.
figure-6.2.eps

Figure 6.2 A visual representation of Facebook authorization using identity pools and user pools for your serverless Pizza API

6.2 Creating user and identity pools

To implement the authentication flow, as described in the previous section, you need to create both user and identity pools.

Start with a user pool. To create one, run the aws cognito-idp create-user-pool command from your terminal. The only required option for this command is the name for your new pool. In addition to the name, add the --username-attributes option, which specifies an email as a unique ID of your user pool. You may also want to customize the password policy by specifying the --policies option. The default password policy requires a mix of lowercase letters, uppercase letters, numbers, and special characters. The full command for creating a new user pool is shown in the next listing.

Listing 6.1 Creating a user pool

aws cognito-idp create-user-pool     ①  
    --pool-name Pizzeria     ②  
    --policies "PasswordPolicy={MinimumLength=8,RequireUppercase=false, RequireLowercase=false,
    RequireNumbers=false,RequireSymbols=false}"     ③  
    --username-attributes email     ④  
    --query UserPool.Id     ⑤  
    --output text

The output is the ID of your new user pool, because the query flag was provided. Keep this ID, because you’ll need it later.

Your user pool needs to have at least one client so you can connect it. You can create a client via the aws cognito-idp create-user-pool-client command, as shown in the listing 6.2. To create a client, you need to pass your user pool ID, which you received from the previous command, and your client name. You’ll test this setup with a simple web app, so you should create a client without a client secret (which means that you’ll need to create another client for Pierre’s mobile app in the future).

Listing 6.2 Creating a client for the user pool

aws cognito-idp create-user-pool-client     ①  
  --user-pool-id eu-central-1_userPoolId     ②  
  --client-name PizzeriaClient     ③  
  --no-generate-secret     ④  
  --query UserPoolClient.ClientId     ⑤  
  --output text

This command prints out the client ID; save it because you’ll need it in the next step.

Before implementing the Facebook authentication and permissions within your application, you need to visit the Facebook developer portal to create an application and obtain its ID.

The next step is to create an identity pool, which you can do using the aws cognito-identity create-identity-pool command from the AWS CLI, as shown in listing 6.3. To do so, provide the identity pool name, any supported login providers (in your case, Facebook), and a Cognito identity provider. For the cognito-identity-providers flag, you’ll need to provide the provider name and client ID and indicate whether you need a server-side token check. The provider name is in the following format: cognito-idp.<REGION>.amazonaws.com/<USER_POOL_ID>. The client ID is what you received from the previous command, and you don’t need server-side token validation, so that value is set to false.

Listing 6.3 Creating an identity pool

aws cognito-identity create-identity-pool     ①  
  --identity-pool-name Pizzeria     ②  
  --allow-unauthenticated-identities     ③  
  --supported-login-providers graph.facebook.com=266094173886660     ④  
  --cognito-identity-providers ProviderName=cognito-idp.eu-central-1.amazonaws.com/
  eu-central-1_qpPMn1Tip,ClientId=4q14u0qalmkangdkhieekqbjma, ServerSideTokenCheck=false   ⑤  
  --query IdentityPoolId     ⑥  
  --output text

After the identity pool is successfully created, you’ll need to create two roles and assign them to authenticated and unauthenticated users. If you need help with role creation, see https://aws.amazon.com/blogs/mobile/understanding-amazon-cognito-authentication-part-3-roles-and-policies/.

To set the roles, use the aws cognito-identity set-identity-pool-roles command, which expects the identity pool ID and roles for both authenticated and unauthenticated users, as shown in the following listing. Make sure you replace the <ROLE1_ARN> and <ROLE2_ARN> values with the ARNs of the two roles you just created.

Listing 6.4 Add roles to the identity pool

aws cognito-identity set-identity-pool-roles     ①  
  --identity-pool-id eu-central-1:2a3b45c6-1234-123d-1234-1e23fg45hij6     ②  
  --roles authenticated=<ROLE1_ARN>,unauthenticated=<ROLE2_ARN>    ③  

This command returns an empty response if it successfully executes.

6.2.1 Controlling API access with Cognito

Now that you have user and identity pools, it’s time to connect the authentication flow in your code.

Claudia, in combination with Claudia API Builder, supports all three authorization methods mentioned earlier: IAM roles, custom authorizers, and Cognito user pools. This book focuses on the last one, but the other two work in a similar way. For more information about them, see the official documentation for Claudia API Builder: https://github.com/claudiajs/claudia-api-builder/blob/master/docs/api.md#require-authorization.

To enable Cognito user pool authorization, you’ll need to register an authorizer using the registerAuthorizer method of the Claudia API Builder instance. This method requires two attributes: the authorizer name and an object with an array of Cognito user pool ARNs. The following is a simple example usage:

api.registerAuthorizer('MyCognitoAuth', {
  providerARNs: ['<COGNITO_USER_POOL_ARN>']
});

After the authorizer is registered, add an object with the cognitoAuthorizer key and the name you used to register your authorizer as a value, as a third argument to the route definition. Your route definition should look like this:

api.post('/protectedRoute', request => {
  return doSomething(request)
}, { cognitoAuthorizer: 'MyCognitoAuth' })

Apply the same to the routes in your api.js file. The routes will look similar to the ones shown in the following listing. All routes related to the orders will be protected with the Cognito authorizer, but routes for pizzas will stay public.

Listing 6.5 API with a custom authorizer

'use strict'
const Api = require('claudia-api-builder')
const api = new Api()
const getPizzas = require('./handlers/get-pizzas')
const createOrder = require('./handlers/create-order')
const updateOrder = require('./handlers/update-order')
const deleteOrder = require('./handlers/delete-order')
api.registerAuthorizer('userAuthentication', {    ①  
  providerARNs: [process.env.userPoolArn]    ②  
})

// Define routes
api.get('/', () => 'Welcome to Pizza API')

api.get('/pizzas', () => {
  return getPizzas()
})
api.get('/pizzas/{id}', (request) => {
  return getPizzas(request.pathParams.id)
}, {
  error: 404
})

api.post('/orders', (request) => {
  return createOrder(request)    ③  
}, {
  success: 201,
  error: 400,
  cognitoAuthorizer: 'userAuthentication'    ④  
})
api.put('/orders/{id}', (request) => {
  return updateOrder(request.pathParams.id, request.body)
}, {
  error: 400,
  cognitoAuthorizer: 'userAuthentication'    ④  
})
api.delete('/orders/{id}', (request) => {
  return deleteOrder(request.pathParams.id)
}, {
  error: 400,
  cognitoAuthorizer: 'userAuthentication'    ④  
})
api.post('delivery', (request) => {
  return updateDeliveryStatus(request.body)
}, {
  success: 200,
  error: 400,
  cognitoAuthorizer: 'userAuthentication'    ④  
})

module.exports = api

The last piece of the authorization puzzle is updating the route handler to use the authorizer.

For example, to update your create-order.js handler you need to do the following:

  • Update the handler to receive the full request object instead of just the body. You want to be able to read user data from the Cognito user pool; that information is provided in the request object, but outside of the body.
  • Get the user data from the authorizer. It is available in the request context, in the authorizer object, under the key named claims.
  • Update the code to get the user’s address from the request body, if provided, or the default address of the authorized user if an address is not provided in the body.
  • Save the Cognito username in the DynamoDB orders table.
figure-6.3.eps

Figure 6.3 A visual representation of how access to your API is controlled by API Gateway and Amazon Cognito user pools

Figure 6.3 shows how the API is restricted by API Gateway and Amazon Cognito user pools.

The updated create-order.js handler is shown in the following listing.

Listing 6.6 create-order.js handler with authorization

'use strict'
const AWS = require('aws-sdk')
const docClient = new AWS.DynamoDB.DocumentClient()
const rp = require('minimal-request-promise')
function createOrder(request) {    ①  
  console.log('Save an order', request.body)
  const userData = request.context.authorizer.claims    ②  
  console.log('User data', userData)    ②  

  let userAddress = request.body && request.body.address    ④  
  if (!userAddress) {    ⑤  
    userAddress = JSON.parse(userData.address).formatted
  }

  if (!request.body || !request.body.pizza || userAddress)
    throw new Error('To order pizza please provide pizza type and address where pizza should be delivered')

  return rp.post('https://fake-delivery-api.effortlessserverless.com/delivery', {
    headers: {
      Authorization: 'aunt-marias-pizzeria-1234567890',
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      pickupTime: '15.34pm',
      pickupAddress: 'Aunt Maria Pizzeria',
      deliveryAddress: userAddress,    ⑥  
      webhookUrl: 'https://g8fhlgccof.execute-api.eu-central-1.amazonaws.com/latest/delivery',
    })
  })
    .then(rawResponse => JSON.parse(rawResponse.body))
    .then(response => {
      return docClient.put({
        TableName: 'pizza-orders',
        Item: {
          cognitoUsername: userAddress['cognito:username'],    ⑦  
          orderId: response.deliveryId,
          pizza: request.body.pizza,
          address: userAddress,    ⑧  
          orderStatus: 'pending'
        }
      }).promise()
    })
    .then(res => {
      console.log('Order is saved!', res)

      return res
    })
    .catch(saveError => {
      console.log(`Oops, order is not saved :(`, saveError)

      throw saveError
    })
}

module.exports = createOrder

After the code is updated, run claudia update to deploy your API. To test the working authorization, you’ll need to implement the login/signup flow. The back-end part of adding the authorization was easy. But most of the work, including the integration of your user and identity pools, should be done on the front-end side. This part of the application is beyond the scope of this book, but you can see the working example code with a how-to guide on GitHub at https://github.com/effortless-serverless/pizzeria-web-app.

Before you run the code from that repository, however, you can confirm that unauthorized users will be rejected by running the following curl command:

curl -o - -s -w ", status: %{http_code}
" 
  -H "Content-Type: application/json" 
  -X POST 
  -d '{"pizzaId":1,"address":"221B Baker Street"}'     ①  
  https://21cioselv9.execute-api.us-east-1.amazonaws.com/latest/orders

This command should return an error and a 401 HTTP status.

The back-end part for adding authorization is easy. Most of the work, including integration of your user and identity pools, should be done on the client-side. This part of the application is beyond the scope of this book, but you can see the working example code with the how-to guide on Github here: https://github.com/effortless-serverless/pizzeria-web-app.

6.3 Taste it!

Now that you know how authorizers work, it’s time to try it on your own.

6.3.1 Exercise

Your exercise for this chapter is to update the delete-order.js handler to allow users to delete only their orders.

Here are a few hints, in case you need them:

  • Authorization was added to the route in listing 6.5.
  • Although the deleteOrder function currently accepts only orderId, you’ll need to extend it to accept authorized user details, too.
  • The deleteOrder method should use cognito:username from the request.context.authorizer.claims object to check if the current user is the order owner.
  • If the user is not the owner, you should return an error.

In case you want an additional challenge, here are two more:

  • Update an order’s primary key to be a combination of the order ID and the owner’s Cognito username. Doing so would allow you to directly search for and delete only orders owned by the authorized user.
  • Modify the update-order.js handler in such a way as to allow users to update only their own orders.

6.3.2 Solution

First, you need to update your delete-order.js handler to accept both orderId and the authorized user data. You also want to get the order from the database and check if it belongs to the authorized user. The following listing shows the updated delete-order.js handler.

Listing 6.7 delete-order.js handler with authorization

'use strict'
const AWS = require('aws-sdk')
const docClient = new AWS.DynamoDB.DocumentClient()
const rp = require('minimal-request-promise')
function deleteOrder(orderId, userData) {    ①  
  return docClient.get({
    TableName: 'pizza-orders',
    Key: {
      orderId: orderId
    }
  }).promise()
    .then(result => result.Item)
    .then(item => {
      if (item.cognitoUsername !== userData['cognito:username'])    ②  
        throw new Error('Order is not owned by your user')    ③  

      if (item.orderStatus !== 'pending')
        throw new Error('Order status is not pending')

      return rp.delete(`https://fake-delivery-api.effortlessserverless.com/delivery/${orderId}`, {
        headers: {
          Authorization: 'aunt-marias-pizzeria-1234567890',
          'Content-type': 'application/json'
        }
      })
    })
    .then(() => {
      return docClient.delete({
        TableName: 'pizza-orders',
        Key: {
          orderId: orderId
        }
      }).promise()
    })
}

module.exports = deleteOrder

After updating your handler, you need to update the route to pass the correct data to the handler. The following listing shows an excerpt from api.js where the order ID and user data are passed to the delete-order.js handler. As you saw earlier, user data is available as claims in the request.context.authorizer object.

Listing 6.8 Updating the delete order route to pass user data to the handler

api.delete('/orders/{id}', (request) => {
  return deleteOrder(request.pathParams.id, request.context.authorizer.claims)    ①  
}, {
  error: 400,
  cognitoAuthorizer: 'userAuthentication'
})

Now that your code is updated, simply run claudia update to deploy it. After its completion, you can use the front-end code from the https://github.com/effortless-serverless/pizzeria-web-app repository to implement retrieving the authorization token on your front-end application or to test. If you try to delete an old order using that token, it won’t work because it was already created without authorization; the Cognito username won’t match.

Summary

  • You can authenticate users of your serverless application using Amazon Cognito.
  • For many user groups with different permissions, use Amazon Cognito identity pools.
  • Setting different authentication methods is easy; just remember that each authentication method has its own user pool.
  • Using Claudia, you can speed up your whole AWS Cognito authentication setup.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset