Rails GraphQL Auth - JWT, Email & Security

Senior Software Engineer with 12 years of expertise in Ruby on Rails and Vue.js, specializing in health, e-commerce, staffing, and transport. Experienced in software development and version analysis.
Search for a command to run...

Senior Software Engineer with 12 years of expertise in Ruby on Rails and Vue.js, specializing in health, e-commerce, staffing, and transport. Experienced in software development and version analysis.
No comments yet. Be the first to comment.
Motivation After building several Shopify embedded apps, I've learned valuable lessons about what works (and what definitely doesn't) in the embedded app environment. Today, I'm sharing these insights to help you avoid common pitfalls and build bette...

As a developer working with Shopify's ecosystem, I recently built a multi-tenant SaaS application that synchronizes customer data between Shopify stores and external services. In this article, I'll share my experience and technical insights into crea...

As developers, we're always looking for ways to streamline our deployment process while maintaining security and reliability. Today, I'm excited to share my experience setting up an automated deployment pipeline for a Rails application using Dokku an...

The Challenge: Modernizing Rails Deployment When I recently needed to deploy my Rails 8 application with multiple databases, I faced a common dilemma: choosing between expensive managed solutions and complex self-managed servers. My requirements were...

The Cost-Benefit Realization As a Rails developer who recently migrated from Heroku to Dokku, I want to share my journey and the surprising benefits I discovered. This transition wasn't just about cost savingsβit opened up new possibilities for my de...

Building a secure and scalable authentication system is crucial for modern web applications. Today, I'll share insights from implementing a robust GraphQL authentication system in Rails, with detailed explanations of each component.
The authentication system consists of three main components:
Let's examine each component in detail.
The core of our authentication relies on JWT tokens. Let's break down the key components:
class User < ApplicationRecord
# Token generation for different purposes with specific lifetimes
generates_token_for :auth_token, expires_in: 1.week
generates_token_for :email_confirmation, expires_in: 8.hours
generates_token_for :password_reset, expires_in: 1.hour
end
This code uses a custom token generation system where:
generates_token_for is a macro that sets up token generation for specific purposesFor example, when we call user.generate_token_for(:auth_token), it:
module Users
class SignInService < ApplicationService
def call
return failure([USER_NOT_FOUND_MESSAGE]) unless user
# Generate authentication token
token = user.generate_token_for(:auth_token)
# Log the authentication event
log_event(user:, data: { username: user.username })
# Return success response with token and user data
success({ token:, user: })
end
private
def user
# Authenticate user using credentials
@user ||= User.authenticate_by(permitted_params)
end
end
end
Key aspects of the service:
module Mutations
class UserSignIn < BaseMutationWithErrors
# Define required input arguments
argument :password, String, required: true
argument :username, String, required: true
# Define return fields
field :token, String, null: true
field :user, Types::UserType, null: true
def resolve(**args)
result = Users::SignInService.call(args)
{
errors: result.errors,
success: result.success?,
token: result.success? ? result.data[:token] : nil,
user: result.success? ? result.data[:user] : nil
}
end
end
end
This mutation:
module Users
class SendConfirmationEmailService < ApplicationService
def call
return failure([USER_NOT_FOUND_ERROR]) unless user
return failure([USER_ALREADY_CONFIRMED_ERROR]) if user.confirmed?
send_confirmation_email
log_event(user:, data: { confirmation_sent: true })
success(CONFIRMATION_SENT_MSG)
end
private
def send_confirmation_email
# Generate confirmation email with secure token
email = Email.create_confirmation_email!(user:)
send_email(email)
end
end
end
The confirmation flow:
class Email < ApplicationRecord
def self.create_confirmation_email!(user:)
token = user.generate_token_for(:email_confirmation)
create!(
to_emails: [user.email],
template_id: Rails.application.credentials.dig(:sendgrid, :confirm_template_id),
substitutions: [{
"confirmation_url": "#{Settings.emails.confirm_url}?token=#{token}",
name: user.name
}]
)
end
end
This creates a confirmation email with:
module Queries
class BaseQuery < GraphQL::Schema::Resolver
def authenticate_user!
return if current_user
raise GraphQL::ExecutionError.new(
I18n.t('gql.errors.not_authenticated'),
extensions: { code: 'AUTHENTICATION_ERROR' }
)
end
def current_user
context[:current_user] || Current.user
end
end
end
This middleware:
class User < ApplicationRecord
# Regular expression for password validation
PASSWORD_FORMAT = /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>])[A-Za-z\d!@#$%^&*(),.?":{}|<>]{8,72}\z/
validates :password,
presence: true,
length: { minimum: 8, maximum: 72 },
format: { with: PASSWORD_FORMAT },
if: :password_required?
private
def password_required?
password_digest.nil? || password.present?
end
end
Password requirements:
RSpec.describe Users::SignInService do
describe '#call' do
context 'when credentials are valid' do
it 'generates an authentication token' do
result = service.call
expect(result.data[:token]).to be_present
expect(User.find_by_token_for(:auth_token, result.data[:token])).to eq(user)
end
it 'logs the authentication event' do
expect { service.call }.to change(AuditLog, :count).by(1)
end
end
context 'when credentials are invalid' do
it 'returns appropriate error messages' do
result = described_class.new(invalid_params).call
expect(result.errors).to include(I18n.t('services.users.sign_in.user_not_found'))
end
end
end
end
Our testing approach:
By implementing these patterns, we've created a secure, maintainable authentication system that:
The complete implementation demonstrates how these components work together in a production environment while maintaining security and user experience.
Happy Coding!