DEV Community

Cover image for Rails GraphQL Auth - JWT, Email & Security
Sulman Baig
Sulman Baig

Posted on • Originally published at sulmanweb.com

Rails GraphQL Auth - JWT, Email & Security

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:

  1. JWT-based token authentication
  2. Email verification workflow
  3. 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Creates a JWT token with user-specific claims
  2. Sets an expiration time (1 week for auth tokens)
  3. Signs the token with the application's secret key
  4. 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
Enter fullscreen mode Exit fullscreen mode

Key aspects of the service:

  1. Validates user credentials
  2. Generates an authentication token
  3. Logs the authentication event
  4. 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
Enter fullscreen mode Exit fullscreen mode

This mutation:

  1. Accepts username and password
  2. Calls the authentication service
  3. Returns token and user data on success
  4. 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
Enter fullscreen mode Exit fullscreen mode

The confirmation flow:

  1. Checks user existence and confirmation status
  2. Generates a secure confirmation token
  3. Creates and sends confirmation email
  4. 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
Enter fullscreen mode Exit fullscreen mode

This creates a confirmation email with:

  1. A secure, time-limited token
  2. A personalized confirmation URL
  3. 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
Enter fullscreen mode Exit fullscreen mode

This middleware:

  1. Verifies token presence and validity
  2. Maintains user context throughout requests
  3. Handles authentication errors consistently
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Our testing approach:

  1. Verifies token generation and validation
  2. Ensures proper error handling
  3. Checks audit logging
  4. 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)