Into the Cloud Conclusion – Securing Applications

Introduction

Applications need to be developed primarily with security in mind.  A lot of things are involved but I will focus on two things in this blog post:

  1. We need to know which actors are using our system.
  2. We need to expose features only to users that need them, and have privilege to access them.

The first one deals with authentication – granting a user some sort of trusted credential (like an ID card). The second one deals with authorization – granting that credential a set of entitlements.

Architecture

In the diagram below, we will be authenticating users through Amazon Cognito. Cognito will be configured to authenticate through three methods:

  1. Using local amazon user pools (users signing up though our app)
  2. Using facebook (users signing in through their facebook login)
  3. Through google (users accessing the app with their google account).

Security architecture showing integration with Amazon Cognito using facebook and google identity providers

Standards

Like I stated in my earlier posts, it is important to design a security architecture that leverages standards so that it can be interoperable with other systems. We used the OpenID protocol. This made it easier to integrate using Angular Auth OIDC Client for the user interface and Spring Security Oauth2 library for the backend.

Authentication

How can we trust that the token passed to the backend service comes from the user instead of a malicious client? How do we identify the user from the token? How do we ensure that the right token is delivered to the client for use?  How do we ensure that users have a good user experience when logging in to the application? These are the questions you need to ask yourself when designing an authentication system.

Gaining access to the System and Single Sign On (SSO)

We implemented authorization code grant flow using (PKCE) on the angular user interface. The authorization code grant type flow redirects login to the authorization provider (Amazon Cognito in this case). The login and signup forms are served by Amazon Cognito. This means that the user’s sign in and signup experience can be customized by changing the Cognito configuration. A few things can be achieved:

  1. Sign-in though external identity providers like Facebook, Google and other OpenID providers
  2. Sign-In through SAML providers.
  3. Enabling multi factor authentication according to the security requirements of your application.
  4. Configuring the session properties like access token validity period, session time, validity of refresh tokens and so on.
  5. Determining what user info is needed
  6. Styling the sign up and login pages.

Signing in through external identity providers help you to login to multiple applications without signing in. For example, once I have logged in to my google account, by clicking the google login, I do not need to enter my user name and password again. This approach provides a seamless way for users to access applications.

One of the major security vulnerabilities of the authorization code grant flow is that the code returned by the authorization server can be intercepted and a malicious actor can use that code to obtain an access token on behalf of the user. This is because public clients are normally used with authorization grant type and no client secrets are used. A common way to mitigate this is using Proof of Key for Code Exchange (PKCE). Most OpenID clients (including angular-auth-oidc-client) and authorization providers (in this case Cognito) support this. This method prevents interception because:

  1. Before the redirect is made to the authorization provider, the client generates two cryptographically related tokens: the code challenge and the code verifier.
  2. Only the client knows these token pairs and their relationship can be validated using cryptographic algorithms.
  3. The client adds the code challenge to the authorization request and the authorization server keeps track of the code challenge by associating it with the request along with the generated authorization code.
  4. The client is required to pass the authorization code and the code verifier in order to obtain the access token. Since only the client knows the verifier, no malicious user can obtain the token without the verifier.
  5. The authorization server validates the relationship between the challenge and the verifier using the selected cryptographic algorithms.

Benefits of using OpenID

In this application, the front end and backend strictly integrates with Amazon Cognito using the OpenID protocol. Because of this:

  1. I can reuse established open id libraries without re-inventing the wheel
  2. I have a solid foundation on the security flow since the protocol is well documented
  3. I can switch authentication providers and use others like Keycloak or Okta without implementation code changes.

Proof of Identity

Once the client makes a token request with a valid authorization code and code verifier, Amazon Cognito issues three tokens:

  1. An access token
  2. An identity token
  3. A refresh token.

These tokens are cryptographically signed by Cognito using public-key cryptography as per the OpenID standard using the JWT specification. In this specification:

  1. The authorization server maintains a key pair. A private key (kept secret by the authorization server) that it uses to sign generated tokens and a public key that it exposes for resource servers to retrieve.
  2. The client attaches the access token to a request.
  3. The resource server validates the request by validating the signature of the token using the public key.

In short, proof of identity is established based on trust. If the resource server can confirm that the token comes from the authorization server, then it trusts that information. The token is a JWT token that contains claims that identify the client. One of those claims is the username claim that contains the user id of the user.

Authorization

Once the identity of the actor has been established, the system needs to know the capabilities of the actor. What is this user allowed to do on this system? This is implemented in different ways in different systems. There are two aspects of authorization:

  1. Obtaining the user entitlements
  2. Enforcing the user entitlements

Both aspects can be taken care of by the same, or different authorization systems. Once practice I find useful is to de-couple authentication from authorization because you will, most likely, not use the same solution for both and you do not want to be tied down to a solution because you cannot isolate them in your logic.

In my case, I am only using Cognito for authentication. Cognito has a native integration with Amazon Verified Permissions which handles the above two aspects through authorization policy configuration and enforcement. Because I isolated both in my design, I am free to start with a much simpler authorization system using database based role-based-access control. In future, if I want to use something more elaborate like the Amazon Verified Permissions, I can easily integrate.

Like I said in the begining of the Authorization section, authorization behaviours fall into two categories:

  1. Those that handle 1 and 2 together, all you have to do is simply ask the authorization server the question “does the user have access to this resource?”
  2. Those that handle 1 and expect that you handle 2. They provide you with the list of the user’s entitlements and it is up to you to enforce that.

I am currently using the second approach, retrieving the user’s entitlement from the database.

Backend Authorization

Being a spring boot service, authentication and authorization are handled using spring security. I implemented a custom granted authority resolver. This is an implementation of spring’s Converter interface that converts a JWT token to a collection of granted authorities.

public class AppJwtGrantedAuthorityResolver implements Converter< Jwt , Collection<GrantedAuthority> > {

@Override
publicCollection<GrantedAuthority>convert(Jwtsource){
…
 }
}

}
 

Spring security already has APIs that help you to enforce access based on what entitlements the user has. So I can configure things like:

  1. Users can only have access to the GET ‘/hymns’ endpoint if they have the ‘hymns.view’ permission.
  2. Users can only have access to the POST ‘/hymns’ endpoint if they have the ‘hymns.create’ permission.

Front-end Authorization

Front-end authorization follows the same approach as backend but in this case, instead of restricting apis, we restrict the following:

  1. Access to pages on the app
  2. Visibility of components
  3. Functionality (enabling or disabling) of certain features or components.

These can be achieved in angular by:

  1. Implementing route guards
  2. Using entitlement based directives
  3. Using conditional statements applied to templates.

NOTE: It is advisable to implement both server and client size authorization.

Back to Amazon Verified Permissions

Amazon Verified Permissions is a user authorization service newly introduced by amazon that enables you to define flexible authorization rules that specify when and how much access a user has. These rules are defined using the open source Cedar language for specifying access control.

Like I said earlier in this post, as a resource server, you ask Amazon Verified Permissions if a user has access to the intended request. This service takes that authorization request, passes it though a set of rules you defined using the cedar language, and responds with an ALLOW or REJECT response.

This approach is extremely flexible and allows you to specify access rules based on any user and request criteria. Application teams can configure their access requirements in a manner that helps them efficiently manage access controls.

One drawback is that calls has to be made to the authorization API for every request and this can be costly and inefficient. This can be mitigated by:

  1. Caching the authorization decisions
  2. Batching authorization requests for multiple functionalities using the batch authorization API.

Secret Management

I cannot conclude without talking briefly about how to manage secrets. Database and private key credentials need to be stored securely to avoid malicious users gaining access to your system. The AWS Systems Manager provides a parameter store that enables you to store different parameters including secrets. This stores secrets as SecureStrings that are encrypted using the default AWS managed encryption key or a customer managed encryption key that you can manage. Encryption keys can be created using the Amazon Key Management Service.

Using the default AWS managed key is free, but you can only access the secret from within the account, using AWS APIs. If you need more flexibility and you want to share secrets with multiple AWS accounts, you will need to create a customer managed key, and of course, this is not free.

First Deployment Journey – Starting a Project with AWS

Background

For the past two weeks, I had two goals – to explore AWS and to launch my software development business. These two goals are similar in the sense that they involve delving into a deep abyss of unknowns with a lot of experiments. It has been an awesome journey so far and of course, I will keep you updated.

Overview

My first goal this past month was to kick of an online presence of my new business – Mariarch. This will involve launching a proof-of-concept introduction of an ambitious project, to curate all the catholic hymns in the world and give Catholics an easy way to discover and learn hymns as we travel around the world. (If this is something you are interested in, by the way, please drop me an email). The objective was to deploy an initial, simple version of this project with bare-minimum features. (You can check the application here)

Architecture

To tie this to my second goal, Mariarch Hymns (the name of the project), needed to be designed using AWS infrastructure.

Initial architecture

The simplest way to have implemented this was a single server architecture with a server side UI framework like freemarker or to serve single page architecture UI clients from within the same server. This will consume the least resources. But since I have the vision of creating mobile applications and exposing these APIs for collaboration with other teams, it made sense to isolate the frontend from the backend implementation using Rest APIs.

Cheap as Possible is The Goal

AWS offers a reasonable free tier offering that I aim to leverage in this exploration and ensure that I spend as little as possible in this initial stage. While cost reduction is a goal, I will be designing in such a way that my application can evolve whenever its needed.

Avoiding Provider Lock-in

One of my core principles of design in this application is to avoid adopting solutions that will make change difficult. Because I am still in the exploratory phase, I need to be able to easily migrate away from AWS to another cloud provider like Azure or GCP if I ever decided that AWS does not meet my needs. In order to be free to do this, I had to ensure the following:

  1. Use open source frameworks and standards and tools where possible and avoid proprietary protocols. A good example is integrating my UI client with Amazon Cognito using the standard OpenID protocol as opposed to relying heavily on AWS Amplify.
  2. Integrating with secret management systems like AWS parameter store and AWS secret manager using a façade pattern to ensure that other secret providers can be easily integrated to the application system.

Software Stack

  1. Angular 18
  2. Java 21
  3. Spring boot 3.3.2
  4. Nginx
  5. AWS
  6. Mongo DB

Mongo V Dynamo DB

I’m pretty sure you are wondering why I chose Mongo DB as opposed to Dynamo DB since I am exploring AWS infrastructure. Dynamo DB is not suited for applications with complex queries to the database. It is a highly efficient key value store. Hopefully I will get into more detail if I have a need for the Dynamo. Fortunately, AWS partners with Mongo Atlas to provide managed Mongo Atlas databases in the same AWS region.

EC2 – The Core

At the heart of AWS offering are its Elastic Cloud Computing systems it calls EC2 (I’m thinking C2 means cloud computing). These are simply virtual machines that you can provision to run your services. AWS provides a host of configurations that allows you to pick the operating system, resource and graphics requirements that you need. (I will not get in to that since AWS has an extensive documentation on this). AWS also provides the serverless version, ECS Fargate, that enables you to focus on our application code rather than the virtual machine. On a level above this,  containers can be deployed through Docker, Amazon Elastic Kubernetes Service, or Amazon’s provision of the Openshift Red Hat Version. For this purpose, I used the simplest, the EC2 Amazon Linux virtual machine (the free tier configuration).

The Amazon Linux virtual machine is basically a Linux instance tailored for AWS cloud infrastructure. It provides tools to seamlessly integrate with the AWS ecosystem. You could do this on any other image version by installing the corresponding AWS tools.

EC2 instances generally run in their own Virtual Private Cloud. This means that you cannot communicate with EC2 instances unless you add rules to grant access to specific network interfaces like protocols and ports. One interesting thing to note is that while AWS provides a free tier offering for EC2, it doesn’t for VPCs and you need a VPC for every EC2 instance. This means that you will spend some money running EC2 instances since they need to run within a VPC even if you are on the free tier. I have attached my initial cost breakdown for the first 2 weeks of use of AWS. We see that while I’m on the free tier and all services I use are still free, I still need to pay for the VPC.

Cost breakdown showing VPC expenses.

Securing EC2 Instances

A VPC is defined by its ID and a subnet mask. Subnet masks are what is used to decide what IP addresses are used for that network. EC2 allows you to configure security groups that define who gets to access what interface of the virtual system. Security groups are reusable. They allow you to define multiple network access rules and apply these rules to different AWS components like other EC2 instances or load balancers. A network access rule comprises of the following:

  1. A type (e.g. HTTP, SSH, e.t.c)
  2. A network protocol (TCP, UDP)
  3. A port
  4. Source IPs (These can be defined by selecting VPC ids, or entering subnet masks or IP addresses).

As a rule of thumb, always deny by default, and grant access to components that need that access. Avoid wildcard rules in your security group.

Another aspect of securing EC2 instances is granting the instance permission to access other AWS services. In my application, I needed to retrieve deployment artefacts from AWS S3 in order to install them on the EC2 instance. To achieve this, the EC2 instance needs to be able to pull objects from S3. As discussed in the previous post, AWS provides a robust security architecture that ensures that all components are authenticated and authorized before they can access any AWS service. This includes EC2 instances.

IAM Roles enable AWS components to access other AWS services.  EC2 instances can be assigned AWS roles. This ensures that the credentials corresponding to the assigned roles are available for processes to use in that virtual environment. In IAM, you can configure a set of policies to be associated with an IAM role. Please check out the IAM documentation for more details.

Routing Traffic to EC2 Instances

Because EC2 instances run in their own sandbox, they cannot be accessed externally without certain measures. In the next post will go through three basic approaches:

  1. Simplest – Exposing the EC2 interface to the outside world
  2. Using an application load balancer
  3. Using an API gateway and a network load balancer.

Blog at WordPress.com.

Up ↑