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.
🏗️ Architecture Overview
The authentication system consists of three main components:
- JWT-based token authentication
- Email verification workflow
- Secure password management
Let's examine each component in detail.
🔑 JWT Authentication Implementation
Token Generation and Management
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 purposes - Each token type has its own expiration time
- Tokens are bound to specific user data for verification
For example, when we call user.generate_token_for(:auth_token)
, it:
- Creates a JWT token with user-specific claims
- Sets an expiration time (1 week for auth tokens)
- Signs the token with the application's secret key
- Returns the encoded token for client use
Authentication Service
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:
- Validates user credentials
- Generates an authentication token
- Logs the authentication event
- Returns a structured response
GraphQL Authentication Mutation
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:
- Accepts username and password
- Calls the authentication service
- Returns token and user data on success
- Handles errors gracefully
📧 Email Verification System
Confirmation Service Implementation
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:
- Checks user existence and confirmation status
- Generates a secure confirmation token
- Creates and sends confirmation email
- Logs the confirmation attempt
Email Token Generation
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:
- A secure, time-limited token
- A personalized confirmation URL
- User-specific template data
🔒 Security Implementation
Authentication Middleware
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:
- Verifies token presence and validity
- Maintains user context throughout requests
- Handles authentication errors consistently
- Provides access to current user data
Password Security
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:
- Minimum 8 characters
- Maximum 72 characters (bcrypt limitation)
- Must include lowercase and uppercase letters
- Must include numbers and special characters
- Validated only when necessary
🧪 Testing Strategy
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:
- Verifies token generation and validation
- Ensures proper error handling
- Checks audit logging
- Validates security constraints
Conclusion
By implementing these patterns, we've created a secure, maintainable authentication system that:
- Provides secure token-based authentication
- Handles email verification properly
- Maintains high security standards
- Scales well with application growth
The complete implementation demonstrates how these components work together in a production environment while maintaining security and user experience.
Happy Coding!
Originally published at https://sulmanweb.com.
Top comments (0)