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)