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 jwtand 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
endHow 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=testand add the following line
jwt_secret: "secret"Btw, don’t forget to generate secure keys using
rails secretLet’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 instanceNext, 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
###############
endWe 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
endand 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
endLet’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"}."
endIf 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
endNow, 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
endand 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
endNow, with
rake graphql:dump_schemaI’ll have updated schema in app/graphql/schema.graphql and specs will remind me to update it.