In this tutorial you will learn how to setup auth for GraphQL queries & mutations using Knock.
Knock is a super easy to setup authentication gem for Rails applications based on JWTs. It's been my go-to for JWT authentication ever since I started using it because it just works out-of-the-box.
Setting up Knock
Add
gem 'knock'
to your application's Gemfile then runbundle install
to install the gem.Include the
Knock::Authenticable
module in yourApplicationController
class ApplicationController < ActionController::API include Knock::Authenticable end
At this point Knock has added a bunch of helper methods to
ApplicationController
and by inheritance, its descendants. You can callauthenticate_user
, provided byKnock::Authenticable
, as abefore_action
in any controller you want to protect. If a request contains a valid JWT in itsAuthorization
header you'll have access to the current user ascurrent_user
in your controller actions.class TransactionsController < ApplicationController before_action :authenticate_user def index respond json: current_user.transactions, status: 200 end end
You can use
Knock::AuthToken.new(payload: { id: user.id }).token
to generate a JWT for a user. To test that everything works, make a request to your protected controller withBearer #{JWT}
as it's Authorization header and you'll find thatcurrent_user
is correctly set to the user you created the JWT for.
FYI
Users aren't the only entities you can authenticate. When you call authenticate_user
as a before_action
, what you're actually telling Knock with the suffix to authenticate_
(in this case, user
) is that you want to authenticate a User
entity (read as ActiveRecord) for that controller.
Knock gets the JWT from the request's Authorization
header, decodes it and uses the id
from the decoded payload to find the User
for that token. It goes on to set a current_user
instance variable for you. If the user it tried to find with the id
from the decoded payload exists and the JWT isn't expired or invalid, calling current_user
will return it and if it doesn't, current_user
returns nil
.
This means that calling authenticate_business
as a before_action
will authenticate a Business
entity and set a current_business
instance variable, available to you in your controller's actions.
Getting Knock to work with GraphQL-Ruby
Since Knock already handles getting the JWT from the request, decoding the token, and finding the entity for that token, all that's left to do is getting GraphQL to know who current_user
is and that is where GraphQL's context
comes in.
Like the name suggests, GraphQL context allows us to inject helpful (application or request specific) "outside" information into the GraphQL execution flow. A common usecase for context is what we intend to do now –– passing the current user into the GraphQL request execution flow.
To add the current user to your GraphQL context, in graphql_controller.rb
, add current
to the context
hash defined in execute
:
class GraphqlController < ApplicationController
before_action :authenticate_user
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: current_user,
}
result = SwipeSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
rescue => e
raise e unless Rails.env.development?
handle_error_in_development e
end
end
In your query or mutation, you'll have access to the context
hash and you can retrieve your current user with context[:current_user]
. If current_user
is nil for a protected query/mutation, you can then throw an 'Unauthorized' GraphQL::ExecutionError
.
module Mutations
class ApproveBusiness < Mutations::BaseMutation
argument :id, Int, required: true
def resolve(id:)
raise GraphQL::ExecutionError.new("Unauthorized") if current_user.nil?
current_user.business.approve!
end
def current_user
context[:current_user]
end
end
end
At this point, you might think your auth flow is ready for use but when you pass an invalid JWT, instead of getting a GraphQL execution error telling you that you're unauthorized, you'll get a 401 Unauthorized
HEAD response because Knock is (rightfully) configured to work like that out-of-the-box.
Knock calls unauthorized_entity
when authorization fails and that's what returns the 401 Unauthorized
HEAD response that's preventing your own GraphQL execution error message being returned to your API's consumer. To fix, in your application_controller.rb
you should replace Knock's unauthorized_entity
method with one that does nothing thus allowing GraphQL to return a response.
# application_controller.rb
def unauthorized_entity(entity_name)
end
Cleaning up
Littering every single one of your protected queries/mutations with raise GraphQL::ExecutionError.new("Unauthorized") if context[:current_user].nil?
is bad practice because you should not be repeating yourself.
To cleanup, use a module to encapsulate the authorization flow. You can then include that module in the base query & mutation all your queries and mutations inherit from and have the module's methods available to all queries and mutations.
module Authorizable
def ensure_authorized!
instance_variable_set("@current_user", context[:current_user])
raise GraphQL::ExecutionError.new("Unauthorized") if @current_user.nil?
end
def current_user
@current_user
end
end
You can then refactor the ApproveBusiness
mutation above to:
module Mutations
class ApproveBusiness < Mutations::BaseMutation
def resolve
ensure_authorized!
current_user.business.approve!
end
end
end