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
Usermodel. You may leave out the:confirmablemodule (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:
- Check that the user exists.
- If the user doesn’t exist, throw a
GraphQL::ExecutionError. - Otherwise, check if the password is valid.
- If the password is invalid, throw a
GraphQL::ExecutionError. - 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.