I often get asked questions about how to implement an authentication system for use in a frontend app relying wholly on GraphQL. There are divided opinions on whether you should keep authentication separate from the GraphQL API. To be honest, I don’t see a reason to do that. If you do authentication through a controller reached through a REST API or through the GraphQL Schema, it hardly makes any difference and if the frontend relies heavily on GraphQL, I don’t see a reason not to have the authentication part with it too.
The first thing to handling authentication through GraphQL is serve two different Schemas based on whether the user is making authenticated requests or a non-authenticated one. It is also the simplest one to take care of, so let’s get this out of the way.
If you are using graphql-ruby, you might already have something along the lines of the following in your GraphQLController
:
def execute_query
schema.execute(:document => document,
:variables => @variables,
:context => @context,
:operation_name => @operation_name)
.to_json
end
All we need is to provide that schema class based on whether the user is currently logged in. Something like this:
def schema
@schema ||= if Current.user
Schema::AuthenticatedSchema
else
Schema::PublicSchema
end
end
Just define the schema classes as separate classes inheriting from GraphQL::Schema
and you are done.
The first question we need to ask ourselves is what authentication is in terms of our app? For us, it is a mutation that creates a new UserSession
on the backend. So the PublicSchema
boils down to a single mutation:
module Schema
class PublicSchema < GraphQL::Schema
mutation Types::PublicMutationType
end
end
module Types
class PublicMutationType < BaseObject
description "Public Mutation API"
field :login, :mutation => Mutations::Login
end
end
You can add more operations if you need to support sign up, account recovery options etc. But once we can figure out the login part, the rest could be derived pretty easily.
module Mutations
class Login < Base
graphql_name "Login"
argument :email, String, :required => false
argument :password, String, :required => false
argument :remember_token, String, :required => false
field :session, UserSessionType, :null => true
field :errors, [String], :null => true
def resolve(args)
if args[:remember_token].present?
user = User.find_by(:persistence_token => args[:remember_token])
user_session = UserSession.new(user) if user.present?
else
user_session = UserSession.new(args.slice(:email, :password))
end
return { :errors => ["unauthorized"] } unless user_session.save
return unless user_session.user
{ :session => user_session, :name => user.name }
end
end
end
Here’s how it can be implemented easily. Note that we are using authlogic
as our authentication system. If you are using something else, only the resolve
implementation would change.