Here's a quick summary of everything we released in Q1 2024.

GraphQL

Authentication & Authorization

Implementing proper authentication and authorization is one of the most crucial aspects of securing your API. Let's take a look at how you can do it with GraphQL.

With great power comes great responsibility, and ensuring that your GraphQL API is secure is essential.

One of the most crucial aspects of securing your API is implementing proper authentication and authorization.

In this article, we will cover the basics of GraphQL authentication and authorization and provide best practices for securing your GraphQL API.

GraphQL Authentication

Authentication is the process of verifying the identity of a user. In the context of a GraphQL API, this typically involves verifying that the user has a valid token or credentials before allowing access to protected resources.

There are several common authentication methods used in GraphQL APIs, including:

  • HTTP Authentication
  • Custom Authentication
  • JSON Web Tokens (JWT) Authentication

HTTP authentication

For every request made to the API, HTTP authentication sends a username and password as part of the authentication process. After checking the credentials, the API requests the data and returns it if the credentials are genuine. Most GraphQL clients are capable of using this approach, which is reasonably easy to build.

However, HTTP authentication has some limitations. For example, it's not very secure because the credentials are sent with every request in plaintext. It can also be difficult to revoke or update credentials once they've been issued.

In GraphQL, HTTP authentication can be implemented using the Authorization header. The Authorization header is used to send authentication credentials with each request to the API. Here's an example of how to implement HTTP authentication in GraphQL using the Authorization header:

type Query {
me: User! @auth
}
directive @auth on FIELD_DEFINITION
type User {
id: ID!
name: String!
}
type AuthPayload {
token: String!
}
type Mutation {
login(email: String!, password: String!): AuthPayload!
}
schema {
query: Query
mutation: Mutation
}

In this example, we have a Query type with a single field, me, which returns a User object. We've added the @auth directive to the me field to indicate that this field requires authentication.

We've also defined a Mutation type with a login field, which takes an email and password as arguments and returns an AuthPayload object. The AuthPayload object contains a token field, a JWT that can be used for subsequent requests to the API.

The Client must include the Authorization header with the value Bearer <token> to authenticate a request, where <token> is the JWT returned by the login mutation. Let’s use the FetchAPI to include the Authorization header:

fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ query: '{ me { id name } }' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))

In this code, we're using the fetch API to make a POST request to the GraphQL API. We've included the Authorization header with the value Bearer <token>. The token variable should be set to the JWT returned by the login mutation.

Custom authentication

Custom authentication involves using a third-party authentication service, such as OAuth. It allows for greater flexibility and control over the authentication process. For example, you can implement multi-factor authentication or require users to authenticate using a specific method, such as biometric authentication.

However, custom authentication can be more complex and require additional resources, such as a dedicated authentication server.

Here's an example of how you might implement custom authentication in a GraphQL API using Node.js and the graphql-yoga library:

const { GraphQLServer } = require('graphql-yoga')
// Define a simple schema with a single query
const typeDefs = `
type Query {
hello: String!
}
`
// Define a resolver function for the query
const resolvers = {
Query: {
hello: (_, { name }) => `Hello ${name || 'World'}!`,
},
}
// Define a function to authenticate requests
function authenticate(req) {
// Implement your custom authentication logic here
// For example, you might check if the user is logged in and has the necessary permissions to access the requested data
// If the user is not authenticated, throw an error
if (!req.user) {
throw new Error('You must be logged in to access this resource')
}
}
// Create a new GraphQL server with custom authentication middleware
const server = new GraphQLServer({
typeDefs,
resolvers,
context: (req) => {
// Call the authenticate function to verify that the request is authenticated
authenticate(req)
// Add the authenticated user to the context object, so it can be accessed by the resolver functions
return {
user: req.user,
}
},
})
// Start the server on port 4000
server.start(() => console.log('Server is running on http://localhost:4000'))

In this code, we define a simple GraphQL schema with a single query that returns a greeting. We then define a resolver function for the query that uses the name argument to personalize the greeting.

The custom authenticate function that accepts the req object as a parameter is then defined. This function carries out the unique authentication logic we've developed, including determining whether the user is currently signed in and has the appropriate access rights to the requested data.

We then create a new GraphQL server using the GraphQLServer constructor from the graphql-yoga library. We pass in the schema and resolver functions and a context function that calls the authenticate function to verify that the request is authenticated. We also add the authenticated user to the context object, so the resolver functions can access it. Finally, we start the server on port 4000 using the server.start() method.

JWT authentication

JWT authentication involves using JSON Web Tokens (JWTs) to authenticate requests to the API. JWTs are a type of token containing claims or statements about the user or client making the request. These claims can include information such as the user's ID or role.

When a user or client logs in to the API, the API generates a JWT and returns it to the client. The client then includes the JWT with each subsequent request to the API. The API verifies the JWT and returns the requested data if the JWT is valid.

Here's an example of how to implement JWT authentication in a GraphQL API using Node.js and the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) library:

const jwt = require('jsonwebtoken');
// Secret key used to sign JWTs
const secretKey = 'mySecretKey';
// Function to generate a JWT
function generateToken(user) {
const token = jwt.sign({ id: user.id }, secretKey, { expiresIn: '1h' });
return token;
}
// Function to verify a JWT
function verifyToken(token) {
try {
const decoded = jwt.verify(token, secretKey);
return decoded;
} catch (err) {
throw new Error('Invalid token');
}
}
// Resolver for a protected query
function protectedQuery(_, args, context) {
// Verify the JWT in the request headers
const token = context.headers.authorization.split(' ')[1];
const decodedToken = verifyToken(token);
// Query the data using the user ID in the JWT
const userId = decodedToken.id;
const data = queryDatabase(userId);
return data;
}
// Resolver for a login mutation
function login(_, args) {
// Verify the user's credentials
const user = verifyUser(args.username, args.password);
// Generate a JWT for the user
const token = generateToken(user);
// Return the JWT to the client
return { token };
}

In this example, we define two functions for generating and verifying JWTs: generateToken() and verifyToken(). The generateToken() function takes a user object as input and returns a JWT signed with a secret key. The verifyToken() function takes a token as input and returns the decoded payload if the token is valid, or throws an error if the token is invalid.

We also define two resolvers: protectedQuery() and login(). The protectedQuery() resolver is used to query protected data that requires authentication. It first verifies the JWT in the request headers using the verifyToken() function, and then queries the data using the user ID in the JWT.

The login() resolver is used to authenticate users and generate a JWT for them. It first verifies the user's credentials using a hypothetical verifyUser() function, then generates a JWT using the generateToken() function and returns it to the client.

To use this example in your own GraphQL API, you would need to modify the resolvers to match your API's schema and data sources. You must also configure the GraphQL server to parse and validate JWTs in the request headers.

GraphQL Authorization

Authentication is the process of verifying a user's identity, while authorization is the process of granting access to resources based on the user's identity and the permissions they have. Once a user is authenticated, we need to ensure they have the necessary permissions to access the requested resources.

GraphQL provides a built-in way to implement authorization through directives. Directives are annotations that can be added to the schema definition to provide additional functionality. In this case, we'll use the @auth directive to restrict access to certain fields or types based on the user's permissions.

Let's look at how we can implement authorization in our GraphQL API.

Role-based authorization

A popular method for restricting access to resources in a GraphQL API is role-based authorisation. Each user is given a role under role-based authorisation, which establishes their level of access to resources.

A user with the role of "admin" might have access to all resources, whereas a user with the role of "guest" might only have access to a portion of the resources.

Here how to use the graphql-shield package to establish role-based authorization in a GraphQL API:

const { rule, shield, and, or, not } = require('graphql-shield');
// Define roles
const ADMIN = 'admin';
const USER = 'user';
const GUEST = 'guest';
// Define rules
const isAuthenticated = rule({ cache: 'contextual' })(async (parent, args, ctx, info) => {
// Check if user is authenticated
return ctx.user !== null;
});
const isAdmin = rule({ cache: 'contextual' })(async (parent, args, ctx, info) => {
// Check if user has admin role
return ctx.user.role === ADMIN;
});
const isUser = rule({ cache: 'contextual' })(async (parent, args, ctx, info) => {
// Check if user has user role
return ctx.user.role === USER;
});
const isGuest = rule({ cache: 'contextual' })(async (parent, args, ctx, info) => {
// Check if user has guest role
return ctx.user.role === GUEST;
});
// Define permissions
const permissions = shield({
Query: {
// Require authentication for all queries
'*': isAuthenticated,
// Allow all users to view public resources
publicResource: isGuest,
// Allow all authenticated users to view protected resources
protectedResource: isUser,
// Allow only admin users to view admin resources
adminResource: isAdmin
},
Mutation: {
// Require authentication for all mutations
'*': isAuthenticated,
// Allow all authenticated users to update their own profile
updateProfile: isUser,
// Allow only admin users to create new users
createUser: isAdmin
}
});
// Export permissions middleware
module.exports = permissions;

In this example, we define three roles: admin, user, and guest. We then define four rules using the rule function from graphql-shield. The isAuthenticated rule checks if the user is authenticated, while the isAdmin, isUser, and isGuest rules check if the user has the corresponding role.

We then define permissions using the shield function from graphql-shield. The permissions object specifies which rules apply to each query and mutation. For example, we require authentication for all queries and mutations, and only allow admin users to create new users.

Finally, we export the permissions middleware, which can be used to protect your GraphQL schema. You can apply the permissions middleware to your schema using a middleware library such as express-graphql.

Attribute-based authorization

Attribute-based authorization (ABA) is a type of authorization mechanism that involves making access decisions based on the attributes of the user or client making the request. In GraphQL, ABA can be used to control access to specific fields or types based on the attributes of the requesting user or client.

Here's an example of how ABA can be implemented in a GraphQL API using the graphql-shield library:

const { rule, shield } = require('graphql-shield')
const isAuthenticated = rule({ cache: 'contextual' })(
async (parent, args, ctx, info) => {
return ctx.user !== null
},
)
const isAdmin = rule({ cache: 'contextual' })(
async (parent, args, ctx, info) => {
return ctx.user.role === 'admin'
},
)
const permissions = shield({
Query: {
// Only authenticated users can access the `me` query
me: isAuthenticated,
// Only admin users can access the `users` query
users: isAdmin,
},
Mutation: {
// Only authenticated users can access the `createPost` mutation
createPost: isAuthenticated,
// Only the author of a post can delete it
deletePost: rule({ cache: 'contextual' })(
async (parent, { id }, ctx, info) => {
const post = await getPostById(id)
return post.authorId === ctx.user.id
},
),
},
})

In this example, we're using graphql-shield to define rules that control access to specific queries and mutations in the GraphQL schema. We have two rules defined: isAuthenticated and isAdmin.

The isAuthenticated rule checks whether the user making the request is authenticated. If the user is authenticated, the rule returns true. Otherwise, it returns false. The isAdmin rule checks whether the user making the request has the admin role. If the user has the admin role, the rule returns true. Otherwise, it returns false.

We then use these rules to define the permissions for each query and mutation in the schema. For example, we use the isAuthenticated rule to restrict access to the query and the createPost mutation. We're using the isAdmin rule to restrict access to the users query. We're also using a custom rule to restrict access to the deletePost mutation. This rule checks whether the user making the request is the author of the post being deleted.

Custom authorization

Custom authorization in GraphQL involves implementing custom logic to determine whether a user or client can access a specific resource or perform a specific action. This can involve checking the user's role or permissions, validating input data, or implementing rate limiting to prevent abuse.

Here's an example of how custom authorization can be implemented in a GraphQL resolver:

const resolvers = {
Query: {
mySensitiveData: async (_, __, { user }) => {
if (!user || user.role !== 'admin') {
throw new Error('Unauthorized');
}
// Fetch and return sensitive data
const sensitiveData = await fetchSensitiveData();
return sensitiveData;
},
},
};

In this code, the resolver for the mySensitiveData field checks whether the user is authenticated and has the 'admin' role. If the user is not authenticated or doesn't have the correct role, the resolver throws an error indicating that the user is unauthorized.

Custom authorization can be implemented in a variety of ways, depending on the specific needs of your GraphQL API. For example, you might implement custom logic to check whether a user has permission to update a resource, or to limit the rate at which certain requests can be made.

You can check out this documentation on how to implement authorizations with Hygraph.

Best Practices for Securing a GraphQL API

There are various recommended practices you can adhere to so as to make sure your GraphQL API is secure, in addition to putting authentication and permission systems in place:

  • Encrypt sensitive data: Use encryption to protect sensitive data in transit and at rest.
  • Implement rate limiting: Limit the number of requests that can be made to the API in a given period to prevent abuse.
  • Validate input: Validate input to prevent injection attacks and other security vulnerabilities.
  • Keep the GraphQL schema simple: Limit introspection and expose what is necessary to clients.
  • Regularly audit the API: Perform security audits to identify and address vulnerabilities promptly.

Conclusion

Security is a continuous process; thus, it's crucial to routinely check your GraphQL API for potential flaws and patch them as soon as you find them.

In conclusion, building robust authentication and authorization mechanisms and adhering to security best practices are needed to secure a GraphQL API.

Now that you have a solid understanding of GraphQL authentication and authorization, you can apply these principles to your own API to ensure it's secure and protected.