Securing Your GraphQL API: A Guide to Authentication and Authorization in Rails

Dhyey Sapara
Simform Engineering
5 min readJan 5, 2024

Elevate your GraphQL API’s security with robust authentication and fine-grained authorization in a Rails environment

Securing APIs is a crucial aspect of building robust applications. In this comprehensive three-part guide, we aimed to explore various methods to authenticate and authorize users in a GraphQL API built with Ruby on Rails.

In the first part, we saw what GraphQL is and understood the concept of queries. The second blog post in the series explored mutations.

Now, in this final part, we will learn to enhance the security of our application using authentication and authorization.

Authentication

Authentication is the process of verifying the identity of a user, system, or entity to ensure that they are who they claim to be. In the context of web development and APIs, authentication is a crucial aspect of ensuring that only authorized users have access to certain resources, functionalities, or data.

For our project, we are using Rails as an API, and our chosen method for authentication involves using JWT tokens.

Before we start with implementing authentication, we have to follow the below steps:

  1. Add username and password_digest columns to our users table, as we will be using them for authentication. Write the following command to generate the necessary migration file:
rails g migration AddColumnsToUsers password_digest username

And then, run rails db:migrate .

2. Install bcrypt gem in our gemfile and then add has_secure_password in our user.rb file. Also, add email and password validation as follows:

  PASSWORD_REGEXP = /\A
(?=.{8,})
(?=.*\d)
(?=.*[a-z])
(?=.*[A-Z])
(?=.*[[:^alnum:]])
/x

has_secure_password

validates :email, presence: true, uniqueness: true
validates :password, format: { with: PASSWORD_REGEXP, message: 'condition failed' }

before_save { self.email = email.downcase }

Here, we are checking email and username presence and password against our password policy regex.

Add some test data through the console, which we will use for testing.

For implementing JWT in our project, we will use ‘jwt’ gem. Add gem 'jwt' in our gemfile and then run bundle .

Let’s create SignIn Mutation using:

rails g graphql:mutation SignInMutation

Replace the content of the SignInMutation class with the following:

argument :username, String, required: true
argument :password, String, required: true

field :token, String, null: true
field :error, String, null: true

def resolve(username:, password:)
raise GraphQL::ExecutionError, "User already signed in" if context[:current_user]

hmac_secret = YOUR_SECRET_KEY
user = User.find_by(username: 'user-name')&.authenticate(password)

return { error: 'Username or Password is incorrect' } unless user
token = JWT.encode user.id, hmac_secret, 'HS256'
{
token: token,
error: ''
}
end

In the mutation part, the arguments define the fields that the mutation will take as inputs, while the field specifies the type of value the mutation will return. Let’s dissect the resolve method to gain a better understanding:

hmac_secret is used to store our hmac secret. Then, we will find the user using the authenticate method available by the bcrypt gem. If we don’t find the user, then we can return the error message. If the user is found, then we will generate the token using the JWT encode method; we provide the payload that we want to encrypt, then our secret key, and lastly, the algorithm that we want to sign our payload with.

Then, we will return the token and error values.

Now, the token that we will receive has to be passed in the header to check if the user is authenticated or not. We will do that by adding current_user method in “Application Controller”:

def current_user
hmac_secret = YOUR_SECRET_KEY
token = request.headers['Authorization'].to_s.split(' ').last
return unless token

user_id = JWT.decode token, hmac_secret, false, { algorithm: 'HS256' }
User.find(user_id[0])
end

To access the current_user method, we have to provide the method name to the context in the GraphQL Controller. Now our controller execute method would look like this:

def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
current_user:
}
result = RailsGraphqlSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
rescue StandardError => e
raise e unless Rails.env.development?

handle_error_in_development(e)
end

Tip:

When a hash has the same key and value, you can pass it using the key: As seen in the example above, if current_user is passed as both key and value of same method name, we can use it like current_user:.

Let’s implement this in GraphiQL.

Write the following query and execute it:

Upon successful authentication, you will receive a token as a response, and this token should be included in the header for subsequent requests. To access the current_user, include the token in the “Authorization” header.

Now, attempting to sign in again would result in raising an ExecutionError. Likewise, you can also check for its existence; if it doesn’t exist, you can raise an execution error to ensure that queries or mutations are executed only when the user is logged in.

Authorization

Authorization in system security is the process of giving the user permission to access a specific resource or function. This term is often used interchangeably with access control or client privilege.

For our project, I will be using an enum to declare the role and use it for authorization, but you can also use ‘cancancan’ or ‘pundit’ for authorization.

First, let’s add a role column in our users table and an enum in our user.rb:

rails g migration AddRoleToUsers role:integer

user.rb:

# remaining code
enum :role, %w[author admin]

For the authorization example, we will restrict the deletion of blogs to users with the “admin” role. However, before implementing this, we need to create a RoleType that must contain values of the enum. To generate the RoleType, use the following command:

rails g graphql:enum role

It creates a role_type.rb file in which the RoleType class inherits from BaseEnum class. Make the following changes to add values:

module Types
class RoleType < Types::BaseEnum
description "Role enum"

value "author",
value "admin"
end
end

Here, the value is used to give values for our user roles.

Append the following changes in blogDelete mutation resolve method:

raise GraphQL::ExecutionError, "Login to access" unless context[:current_user]
raise GraphQL::ExecutionError, "Only admins can delete the blogs" unless context[:current_user].admin?

Now, if you try to sign in with author login and try to execute mutation, it will raise the following exception:

Now, try to log in using admin, and you will be able to delete the blog.

This was the last part of our rails with GraphQL guide. Thank you for reading. I hope it was helpful to you.

Keep following the Simform Engineering publication for more insights and the latest trends in the development ecosystem.

Follow Us: Twitter | LinkedIn

--

--