Securing Your Serverless GraphQL API - Part I
Tags: aws, typescript, graphql, serverless
Published: 2023-06-20 20:48:54

In the modern digital era, our reliance on APIs has become more pronounced than ever. As developers, we are tasked with building secure, scalable systems, and a significant part of that responsibility involves safeguarding the APIs that power these systems.

Over the course of our 'From Zero to Serverless Hero' series, we've taken a deep dive into creating a serverless environment, setting up an Apollo server with TypeGraphQL, and implementing data resolvers using Node.js. Now, our journey brings us to a critical stage: securing our serverless GraphQL API.

Security is a non-negotiable aspect of any application. In this post, we'll explore the importance of securing APIs, examine different strategies, and explain what combination is a good choice for the beginning.

By the end of this article, you'll have a much clearer understanding of API security. However, due to the extensive nature of this topic, we will focus more on the theory in this blog post and cover most of the implementation part in the next blog post. Strap in and prepare to level up from being just a serverless enthusiast to becoming a serverless security hero!

## Table of Contents
* [Understanding API Security](#understanding-api-security)
* [API Security Strategies](#api-security-strategies)
* [Our Choice for Security](#our-choice)
* [Implementing Authentication in Your API](#implementing-authentication)
* [Testing the Security of Your API](#testing)
* [Conclusion](#conclusion)

## Understanding API Security <a name="understanding-api-security"></a>
API Security, at its core, involves implementing measures to protect the integrity of APIs from threats and attacks. APIs, or Application Programming Interfaces, are the conduits through which software applications communicate and exchange data. As such, they have become a prime target for malicious actors intent on exploiting vulnerabilities for data theft, denial of service attacks, and other nefarious purposes.

Securing your API is crucial because it serves as the gatekeeper to your application. If left unprotected, your API could serve as a weak point that attackers can exploit to access sensitive information. This risk is especially high in today's interconnected digital landscape, where APIs often interface with multiple external systems, each potentially introducing new vulnerabilities.

But what does it mean to secure an API? It involves several key aspects:

**Authentication**: This process verifies the identity of the client trying to access the API. It ensures that only recognized, authenticated users or systems are granted access.

**Authorization**: Once a user is authenticated, authorization determines what resources they can access and what operations they can perform. Not every user should have access to every part of your system.

**Encryption**: To safeguard data during transmission, APIs should use secure protocols such as HTTPS, which encrypts the data to prevent it from being intercepted and understood by unauthorized parties.

**Input Validation**: APIs should validate the input they receive to prevent attacks such as SQL injection or Cross-Site Scripting (XSS) that are designed to exploit poorly validated input.

**Rate Limiting**: This involves limiting the number of requests a client can make to the API within a specific timeframe, preventing DoS attacks where an attacker might aim to overwhelm an API with a flood of requests.

**Auditing and Logging**: Keeping a record of who accesses your API and what they do with it provides a trail that can be analyzed for suspicious activity. It also serves as a forensic tool if a breach does occur.
API security is not a 'set it and forget it' concept; rather, it is a continuous, proactive endeavor. It requires vigilance, regular updates, and a commitment to best practices in order to keep pace with evolving threats. In the following sections, we will discuss these aspects in greater detail, particularly focusing on our chosen strategy, user authentication.

## API Security Strategies <a name="api-security-strategies"></a>
There are numerous strategies and techniques at our disposal when it comes to securing APIs. The choice depends on a variety of factors, including the specific needs of the application, the sensitivity of the data being handled, and the nature of the clients that will be interacting with the API. Here, we'll outline a few commonly used API security strategies and briefly discuss their pros and cons.

### 1. API Keys
API keys are unique identifiers that are used to control access to an API. They are simple to implement and use. The client includes the API key in each request, and the server checks this key to decide whether or not to process the request.

**Pros**: Easy to use; straightforward implementation.

**Cons**: API keys are not inherently secure. If a key is stolen, it can be used to gain unauthorized access to the API. Also, API keys alone can't provide fine-grained control over what resources a client can access.

### 2. OAuth
OAuth is an open standard for access delegation. It allows users to grant third-party applications access to their information on other services, without sharing their passwords.

**Pros**: Highly secure; allows for fine-grained access control; widely adopted standard.

**Cons**: Can be complex to implement; involves a multi-step process for the user, which can affect the user experience.

### 3. JSON Web Tokens (JWT)
JWT is a standard for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

**Pros**: Enables stateless authentication; allows for fine-grained access control.

**Cons**: Requires careful management of the token lifecycle; vulnerable to theft if not properly secured.

### 4. Mutual TLS (mTLS)
mTLS is a version of the standard TLS protocol where both the client and the server authenticate each other. This mutual authentication provides an additional layer of trust.

**Pros**: Provides strong, mutual authentication.

**Cons**: Requires the distribution and management of client certificates, which can be complex.

### 5. GraphQL Shield
GraphQL Shield is a library specifically for the GraphQL server, offering an easy-to-use middleware-style API to create a permission layer for your application.

**Pros**: Easy integration with GraphQL; flexible rule-based access control.

**Cons**: Specific to GraphQL; requires an understanding of its rule and permission system.

Each of these strategies has its strengths and weaknesses, and there's no 'one-size-fits-all' answer. You might even use a combination of these strategies for different aspects of your API. In our case, we've chosen user authentication as our primary strategy, and we will use AWS Cognito to implement it. In the next sections, we'll discuss why we made this choice and how to get it set up.

## Our Choice for Security: User Authorization and Authentication via ApiKeys and OAuth <a name="our-choice"></a>
Striking the right balance between accessibility and security is essential when designing APIs. Some functionalities, such as browsing through a library, should be accessible to all users, even those who aren't logged in. However, other actions, particularly those that alter data or user preferences, need stricter control. To address these needs, we've opted for a dual approach for our serverless GraphQL API: ApiKeys for public endpoints and OAuth for protected operations.

### ApiKeys for Public Endpoints
ApiKeys provide a simple, effective way to regulate access to our API's public-facing endpoints. Users can engage with aspects of our service, such as browsing the library, without logging in. ApiKeys simplify user experience for new or casual users while offering control through unique keys issued to each client. By limiting the number of requests a client can make within a certain timeframe, we ensure these public endpoints are not exploited or overloaded.

### OAuth for Protected Operations
When it comes to sensitive operations like changing user settings or updating records, we need more robust protection. That's where OAuth comes in. OAuth is a widely used standard that allows for user-specific authorization and authentication. Using OAuth, we can confirm the identity of users, grant them access, and control what operations they can perform once logged in. This protects sensitive data and functionalities, enhancing our API's security.

### A Balanced Approach
This blend of ApiKeys and OAuth offers a balanced, secure, and user-friendly API. ApiKeys ensure public services are accessible, attracting potential users to our application. OAuth, meanwhile, protects sensitive operations, offering peace of mind for users and administrators alike. This approach effectively combines robust security with an optimal user experience. In the next sections, we'll explore how to implement these ApiKeys. The implementation of authorization with OAuth will be covered in the next blog post.

## Implementing Authentication in Your API with an API Key <a name="implementing-authentication"></a>

Let's start with the easy one: checking if the user provides a valid ApiKey. For this, we will check the headers in the context, and if the required `x-api-key` header is not provided or the apiKey is not in our whitelist, we will throw an error.

``` typescript
const API_KEYS = ['your-key'];

export const handler = startServerAndCreateLambdaHandler(
  server,
  handlers.createAPIGatewayProxyEventV2RequestHandler(),
  {
    context: async ({ event, context }) => {
      const apiKey = getHeader('x-api-key', event.headers);
      if (!apiKey || !API_KEYS.includes(apiKey)) {
        throw new GraphQLError(
          'You are not authorized to perform this action.',
          {
            extensions: { code: 'FORBIDDEN' },
          }
        );
      }
      return {
        lambdaEvent: event,
        lambdaContext: context,
      };
    },
  }
);
```
In the above example we add a list of api keys via a variable in the code. However, for easier configuration putting this into a database or at least making it configurable via environment variables would be the recommended option.

We also used a helper function in the above code, it will just get the header values from the header `helper/grapqhl.helper.ts`:
``` typescript
export function getHeader(
  headerName: string,
  headers: { [id: string]: unknown }
): string | undefined {
  const keys = Object.keys(headers);
  const key = keys.find((k) => k.toLowerCase() === headerName.toLowerCase());
  if (key) {
    return headers[key] as string;
  } else {
    return undefined;
  }
}

```

Add the above snippets to your API handler, build and deploy the code, and check how your API behaves now.

If you query your server now, you will get a 500 server response with the code UNAUTHENTICATED in the error message.

![Error For Unauthenticated Client](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yaolccm0gwgwy3cjrzps.png)
From now on you need to provide your apiKey with every call query and mutation you make. In the Apollo Playground, you are able to configure this header in a specific tab:

![Apollo Playground Header Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ptql1uwsuysvfn39bsb5.png)

As easy as this, now only people who know your apiKeys are able to communicate with your API.


## Testing the Security of Your API <a name="testing"></a>

After implementing the security measures for your API, it's critical to ensure that they work as expected. Testing the security of your API is a continuous process that helps identify potential vulnerabilities and ensure that the implemented security mechanisms function correctly.

**1. Unit Testing**: Start with unit tests for your authentication and authorization code. Ensure that only authenticated users can access protected routes, and are authorized to perform the operations they're trying to carry out.

**2. Integration Testing**: Conduct integration tests where you simulate a user's interaction with the API, performing various tasks. Check that the system correctly identifies users and grants them appropriate permissions.

**3. Penetration Testing**: Here, you simulate an attack on your system to identify potential vulnerabilities. Use automated tools, and, if possible, consider hiring a security firm that specializes in this type of testing. Test for common attack vectors such as SQL Injection, Cross-Site Scripting (XSS), and Cross-Site Request Forgery (CSRF).

**4. API Scanning**: Use API security scanners that can scan your APIs for vulnerabilities. These tools can help find issues that could be exploited by an attacker.

**5. Load Testing**: Overloading an API can reveal security vulnerabilities. Simulate high load situations to make sure that your rate limiting is effective and the system doesn't reveal sensitive information under stress.

**6. Regular Auditing**: Regularly review your security logs to identify any suspicious activity. Automated auditing tools can flag unusual behavior, making it easier to catch and respond to potential security threats.

**7. Dependency Checking**: Regularly check your dependencies for known vulnerabilities. Tools exist that can automatically check for and alert you to security issues in your dependencies.

**8. Security Training**: Keep your team up-to-date with the latest security best practices and common threats. A well-informed team is one of your best defenses against security breaches.

Remember, security isn't a one-time event but a continuous process of improvement. Regular testing, auditing, and updating your security measures are key to maintaining a robust, secure API. Stay proactive and vigilant in order to keep your API—and your users' data—safe and secure.

## Conclusion <a name="conclusion"></a>
In our journey from zero to serverless hero, understanding and implementing robust security measures is of paramount importance. In this post, we've explored the essentials of API security and strategies to safeguard our serverless GraphQL API.

We've elucidated why a mix of ApiKeys and OAuth provides a well-rounded security solution, allowing both public access to certain services and stringent protection for sensitive operations. This balance between openness and security forms the cornerstone of an effective, user-friendly, and secure API.

Furthermore, we've underscored the importance of continuous testing to validate the effectiveness of our security implementations, and to proactively uncover and patch potential vulnerabilities.

Implementing these strategies, however, is not the end. Security is a dynamic and continuous process. It requires regular evaluation and updates to keep up with the ever-evolving landscape of digital threats.

In the end, the key to effective API security lies not just in a one-time setup, but in continuous vigilance, regular testing, and an unwavering commitment to protecting your data and your users. Through this post, we hope to have equipped you with a good base of knowledge about API security. In the next blog post, we will head to the implementation part. So stay tuned.

_Hey there, dear readers! Just a quick heads-up: we're code whisperers, not Shakespearean poets, so we've enlisted the help of a snazzy AI buddy to jazz up our written word a bit. Don't fret, the information is top-notch, but if any phrases seem to twinkle with literary brilliance, credit our bot. Remember, behind every great blog post is a sleep-deprived developer and their trusty AI sidekick._