Welcome back to my series how to develop GraphQL API for rails applications from scratch.
Authentication - signIn method
To authenticate our users, we need to add jwt gem to our Gemfile
$ bundle add jwt
and create app/models/auth_token.rb
class AuthToken
def self.key
Rails.application.credentials.jwt_secret
end
def self.token(user)
payload = {user_id: user.id}
JWT.encode(payload, key)
end
def self.verify(token)
result = JWT.decode(token, key)[0]
User.find_by(id: result["user_id"])
rescue JWT::VerificationError, JWT::DecodeError
nil
end
end
How does it work. We use jwt_secret
which is stored in rails credentials to create a token which stores user_id
and to find User
record by token.
Let’s create our jwt_secret
for development and test environments
$ rails credentials:edit --environment=development
$ rails credentials:edit --environment=test
and add the following line
jwt_secret: "secret"
Btw, don’t forget to generate secure keys using
$ rails secret
Let’s test it out.
AuthToken.key # "secret"
user = User.create(email: FFaker::Internet.email, password: SecureRandom.hex, first_name: FFaker::Name.first_name, last_name: FFaker::Name.last_name )
AuthToken.token(user) # "token-secret-value"
AuthToken.verify("token-secret-value") # returns User instance
Next, let’s implement current_user
method. We need to update app/controllers/graphql_controller.rb
class GraphQLController < ApplicationController
protect_from_forgery with: :null_session # <-- uncomment this line
def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: current_user # <-- add this line
}
result = GraphqlFromScratchSchema.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
############### Add this method
def current_user
return nil if request.headers["Authorization"].blank?
token = request.headers["Authorization"].split(" ").last
return nil if token.blank?
AuthToken.verify(token)
end
###############
end
We need to create our mutation in app/graphql/mutations/users/sign_in.rb
module Mutations::Users
class SignIn < Mutations::BaseMutation
graphql_name "signIn"
argument :email, String, required: true
argument :password, String, required: true
field :user, Types::UserType, null: true
field :token, String, null: true
def resolve(email:, password:)
user = User.find_by(email:)
errors = {}
if user&.authenticate(password)
context[:current_user] = user
token = AuthToken.token(user)
{token: AuthToken.token(user), user:, success: true}
else
user = nil
context[:current_user] = nil
raise GraphQL::ExecutionError, "Incorrect Email/Password"
end
end
end
end
and add it to app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :sign_up, mutation: Mutations::Users::SignUp
field :sign_in, mutation: Mutations::Users::SignIn # <-- add this line
end
end
Let’s try it in our GraphQL console
mutation {
signIn(input: { email: "test@example.com", password: "1234567890" }) {
user {
id
email
name
}
success
token
}
}
And you’ll see a result
{
"data": {
"signIn": {
"user": {
"id": "9a7fb463-2493-48f6-8641-509f58c9b47f",
"email": "test@example.com",
"name": "Alexey Poimtsev"
},
"success": true,
"token": "correct-token"
}
}
}
In case of wrong email/password
mutation {
signIn(input: { email: "test@example.com", password: "111" }) {
user {
id
email
name
}
success
token
}
}
You’ll see an error message
{
"data": {
"signIn": null
},
"errors": [
{
"message": "Incorrect Email/Password",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["signIn"]
}
]
}
Don’t forget to write specs spec/graphql/mutations/users/sign_in_spec.rb
require "rails_helper"
RSpec.describe "#signIn mutation" do
before do
@password = SecureRandom.hex
@user = FactoryBot.create(:user, email: "user@example.com", password: @password)
end
let(:mutation) do
<<~GQL
mutation signIn($email: String!, $password: String!) {
signIn(input: {
email: $email
password: $password
}) {
user {
id
email
name
}
token
}
}
GQL
end
it "is successful with correct email and password" do
result = GraphqlFromScratchSchema.execute(mutation, variables: {
email: "user@example.com",
password: @password
})
expect(result.dig("data", "signIn", "errors")).to be_nil
expect(result.dig("data", "signIn", "user", "email")).to eq("user@example.com")
expect(result.dig("data", "signIn", "user", "id")).to be_present
expect(result.dig("data", "signIn", "token")).to be_present
end
it "fails with wrong password" do
result = GraphqlFromScratchSchema.execute(mutation, variables: {
email: "user@example.com",
password: "wrong-password"
})
expect(result.dig("data", "signIn", "user", "id")).to be_nil
expect(result.dig("data", "signIn", "token")).to be_nil
expect(result.dig("errors", 0, "message")).to eq("Incorrect Email/Password")
end
end
Let’s break for a while to improve our code a bit.
Improvements - whoAmI method, inflections and GraphQL schema dump
Let’s add a helper method to easily test our authentication. Add following lines to app/graphql/types/query_type.rb
(you can replace test_field
method)
field :who_am_i, String, null: false,
description: "Who am I"
def who_am_i
"You've authenticated as #{context[:current_user].presence || "guest"}."
end
If you’re not authenticated
{
whoAmI
}
you’ll see
{
"data": {
"whoAmI": "You've authenticated as guest."
}
}
But if you use correct token
{
"Authorization": "correct-token"
}
You’ll see
{
"data": {
"whoAmI": "You've authenticated as Alexey Poimtsev."
}
}
Let’s play with inflections. Open file config/initializers/inflections.rb
and make in looks like
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym "RESTful"
inflect.acronym "GraphQL" # <-- add this line
end
Now, we can rename in app/controllers/graphql_controller.rb
class name GraphqlController
to GraphQLController
. Looks better, isn’t it? But don’t forget to rename every Graphql
string in class names to GraphQL
.
Let’s add rake task for schema dump. I’ve created lib/tasks/graphql.rake
with following code
namespace :graphql do
desc "Dump GraphQL schema"
task dump_schema: :environment do
# Get a string containing the definition in GraphQL IDL:
schema_defn = GraphQLFromScratchSchema.to_definition
# Choose a place to write the schema dump:
schema_path = "app/graphql/schema.graphql"
# Write the schema dump to that file:
File.write(Rails.root.join(schema_path), schema_defn)
puts "Updated #{schema_path}"
end
end
and added spec in spec/graphql/schema_spec.rb
require "rails_helper"
RSpec.describe "GraphQL schema" do
it "must be reflected in the .graphql file" do
current_defn = GraphQLFromScratchSchema.to_definition
printout_defn = File.read(Rails.root.join("app/graphql/schema.graphql"))
assert_equal(current_defn, printout_defn, "Update the printed schema with `bundle exec rake dump_schema`")
end
end
Now, with
$ rake graphql:dump_schema
I’ll have updated schema in app/graphql/schema.graphql
and specs will remind me to update it.