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_foris 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)