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)