Mar 23, 2018

Rails GraphQL Server Tips — Part 1, Authentication.

I decided to write a series of posts that show how I do certain things with graphql-ruby in server side Rails applications.

Today, I’ll be showing how I handle authentication. Understanding this article requires some knowledge of GraphQL, Rails & how to use devise (or some other authentication gem works)

For an introduction to GraphQL, you can read this post and these slides. To see how to create a simple GraphQL API with Rails, you can check this repo out.

An example rails application with everything in this article can be found at my github here.

All constructive feedback is welcome.

Devise

Devise is a pretty popular, pretty powerful gem for handling authentication in a Rails application. I’m not going to explain how to install/set-up devise, the documentation is pretty explanatory. A few things to note:

  • As usual, we’re generating a User model. You may leave out the :confirmable module (don’t forget to edit the migration if you do so).
  • There’s no real need to generate devise views for the purposes of this tutorial.
  • You’ll also need to install and setup this gem devise-token_authenticatable. This enables the user to sign in via an authentication token. This token can be given via a query string or HTTP Basic Authentication.

User Type

Here’s a look at the simple GraphQL UserType we’ll be working with.

 module Types
  UserType = GraphQL::ObjectType.define do
    name 'User'
    description 'Example User'
    
    field :lastName, !types.String, property: :last_name
    field :firstName, !types.String, property: :first_name
    field :email, !types.String
  end
end 

User Registration

Now, we create a mutation that allows the user to register/sign up. Before we do that however, let us define the UserInputType that will be passed as an argument to the Mutation.

 module Types
  module Input
    UserInputType = GraphQL::InputObjectType.define do
      name 'UserInputType'
      description 'Properties for registering a new User'

      argument :lastName, !types.String
      argument :firstName, !types.String
      argument :email, !types.String
      argument :password, !types.String
    end
  end
end 

And below, we have the mutation:

 module Mutations
  RegisterUser = GraphQL::Field.define do
    name 'RegisterUser'
    argument :registrationDetails, !Types::Input::UserInputType

    type Types::UserType

    resolve ->(_obj, args, _ctx) {
      input = Hash[args['registrationDetails'].to_h.map {|k, v| [k.to_s.underscore.to_sym, v]}]
      begin
        @user = User.create!(input)
      rescue ActiveRecord::RecordInvalid => invalid
        GraphQL::ExecutionError.new("Invalid Attributes for #{invalid.record.class.name}: #{invalid.record.errors.full_messages.join(', ')}")
      end
    }
  end
end 

There’s something to note about the code snippet above. Let’s take a look at the line below.

 input = Hash[args['registrationDetails'].to_h.map {|k, v| [k.to_s.underscore.to_sym, v]}] 

It is convention for GraphQL types, fields & arguments to be written in camelCase. On the other hand, ruby’s convention is snake_case for pretty much everything except class and module names.

This means we need to able to easily convert our camelCase argument hash to it’s snake_case version. That’s what that line does.

The rest of the snippet is pretty straightforward; it tries to create a user with the input and if for some reason it fails, throw a GraphQL::ExecutionError.

Sign In

 module Types
  AuthType = GraphQL::ObjectType.define do
    name 'AuthType'

    field :authenticationToken, !types.String, property: :authentication_token
  end
end 

Above is the type the SignIn mutation will return and below is the mutation itself.

 module Mutations
  SignIn = GraphQL::Field.define do
    name 'SignIn'
    argument :email, !types.String
    argument :password, !types.String

    type Types::AuthType

    resolve ->(_obj, args, _ctx) {
      @user = User.find_for_database_authentication(email: args[:email])
      if @user
        if @user.valid_password?(args[:password])
          authentication_token = @user.authentication_token
          return OpenStruct.new(authentication_token: authentication_token)
        else
          GraphQL::ExecutionError.new('Incorrect Email/Password')
        end
      else
        GraphQL::ExecutionError.new('User not registered on this application')
      end
    }
  end
end 

This is also pretty straightforward and uses methods that Devise automatically adds to the User model.

This is a simple step by step of how it works:

  1. Check that the user exists.
  2. If the user doesn’t exist, throw a GraphQL::ExecutionError.
  3. Otherwise, check if the password is valid.
  4. If the password is invalid, throw a GraphQL::ExecutionError.
  5. Otherwise, return the authentication_token.

Authorization

All that’s left to do now is to protect certain queries or mutations from being accessed when the user isn’t signed in.

When the client application calls the SignIn mutation and gets the token in return, it can now make further requests as being signed in by adding the token to the request header (Authorization: Bearer <token-is-here>).

You can test passing a header to your GraphQL request using Altair.

Because devise & devise-token_authenticatable handle the stress of checking the headers to validate the token and find which user is signed in, the rest is pretty easy to do.

First, you make sure to pass the current_user (which is a helper method devise provides with the user that’s currently signed in) into the context of all GraphQL query/mutation resolvers. This is done in your GraphqlController

 class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
      current_user: current_user
    }
    result = TixSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  end
... 

Secondly, we’re going to make a simple helper of our own and it will be called AuthorizeUser.

AuthorizeUser is a simple Ruby class that has a call method. It also needs to have a simple, custom initializer.

 module Resolvers
  module Helpers
    class AuthorizeUser
      def initialize(resolve_func)
        @resolve_func = resolve_func
      end

      def call(obj, args, ctx)
        if ctx[:current_user].blank?
          GraphQL::ExecutionError.new('User not signed in')
        else
          @resolve_func.call(obj, args, ctx)
        end
      end
    end
  end
end 

When AuthorizeUser.new(.... is being used, it expects a GraphQL resolver method/lambda as it’s argument which it then sets as an instance variable.

In it’s call method, it takes in the same exact arguments as a resolver method/lambda.

It first checks to see if the current_user we passed in the context exists. As usual, if it doesn’t exist, it throws a GraphQL::ExecutionError. However, if the current_user exists, it passes it’s arguments untouched into the call method of the @resolve_func that was passed in during initialization.

Think of this AuthorizeUser as a resolver function that wraps around another resolver function.

Say for example we had a mutation called CreateTicket that was something like this.

 module Mutations
  CreateTicket = GraphQL::Field.define do
    name 'CreateTicket'
    argument :ticket, Types::Input::TicketInputType
    type Types::TicketType

    resolve ->(_obj, args, _ctx) {
      # perform ticket creation
    }
  end
end 

If we wanted to make it such CreateTicket can only be used by a signed in user, this is how it’ll look, using the helper class above.

 module Mutations
  CreateTicket = GraphQL::Field.define do
    name 'CreateTicket'
    argument :ticket, Types::Input::TicketInputType
    type Types::TicketType

    resolve Resolvers::Helpers::AuthorizeUser.new(->(_obj, args, _ctx) {
      # perform ticket creation
    })
  end
end 

We simply just passed the resolver of CreateTicket as an argument to AuthorizeUser.new which will then execute the contents of the resolver only if current_user is present.

That’s it!

Remember, you can find sample code with everything we went through here.

Copied to clipboard!