CSC/ECE 517 Fall 2024 - E2487. Reimplement authorization helper.rb
Introduction
Expertiza currently uses session-based authentication in its AuthorizationHelper module. However, the new back-end reimplementation adopts JSON Web Token (JWT) for authentication. This transition is necessary to make the system more scalable, stateless, and secure. JWT-based authentication will replace session-based methods, requiring updates to the AuthorizationHelper module and associated methods.
Background
The AuthorizationHelper module in Expertiza plays a critical role in managing user permissions and access control within the system. It provides methods to verify a user’s privileges based on their assigned roles, such as Super-Admin, Admin, Instructor, TA, or Student. These methods check if the user is authorized to perform actions like submitting assignments, reviewing work, or participating in quizzes. Additionally, it determines whether a user is involved in specific assignments, either as a participant, instructor, or through TA mappings.
Problem Statement
Expertiza currently uses session-based authentication in its [AuthorizationHelper] module. The [reimplementation-back-end] however, uses JWT (JSON Web Token) based authentication. This requires a reimplementation of an authorization concern module to accommodate JWT-based authentication
Concerns
A concern is a module that you extract to split the implementation of a class or module in coherent chunks, instead of having one big class body. Our new authorization module will be implemented as a concern which will work in tandem with the already-implemented [JWT Concern]
The following points need to be taken care of as per the requirement
- Role Management :
- Enable the system to process, verify, and decode roles from JWT Tokens.
- Use token role data to authenticate and authorize user actions.
- Privilege Verification :
- Update methods to validate user privileges using JWT claims.
- Testing and Documentation :
- Write comprehensive unit tests for all JWT-related methods.
- Include clear documentation and comments to aid future developers.
Implemented Changes
JWT Token Overview
A JSON Web Token (JWT) is a compact, URL-safe token format commonly used for securely transmitting information between parties. It is widely used in web applications for authentication and authorization, as it allows information to be verified and trusted. In the context of our reimplemented AuthorizationHelper module, JWT tokens will replace session-based authentication, enabling stateless and more secure access control.
A JWT Token consists of three main parts:
- Header: Contains metadata about the token, such as the type (JWT) and the hashing algorithm used (e.g., HS256 or RS256).
- Payload: Includes claims, which are statements about an entity (typically, the user) and additional data like the user’s role and permissions.
- Signature: A cryptographic signature generated by encoding the header and payload with a secret key or private key, ensuring data integrity.
These three sections are separated by dots (.), forming a token that looks like this: <Header>.<Payload>.<Signature>

Overall Flow

Methods Implemented
- has_required_role?(required_role):
def has_required_role?(required_role) required_role = Role.find_by_name(required_role) if required_role.is_a?(String) current_user&.role&.all_privileges_of?(required_role) || false end
This function checks if the current user has the required role or higher privileges. The role name is passed down as a parameter in required_role which is the minimum required role level for an action
For example:
has_required_role?('Administrator') # Checks if user is an admin or higher
has_required_role?(Role::INSTRUCTOR) # Checks if user is an instructor or higher
For exact role requirements, we also have:
- is_role?(required_role):
def is_role?(required_role) required_role = required_role.name if required_role.is_a?(Role) current_user&.role&.name == required_role end
which checks if the user has exactly the specified role. For example:
is_role?('Student') # True only if user is exactly a student
Finally, we have authorization methods which work with the ApplicationController as well as individual resource controllers:
# Authorize all actions def authorize unless all_actions_allowed? render json: { error: "You are not authorized to #{params[:action]} this #{params[:controller]}" }, status: :forbidden end end # Check if all actions are allowed def all_actions_allowed? return true if has_required_role?('Administrator') action_allowed? end # Base action_allowed? - allows everything by default # Controllers should override this method to implement their authorization logic def action_allowed? true end
The individual resource controllers can then override the method 'action_allowed?' to manage access. For example:
def action_allowed? has_required_role?('Instructor') end
Existing File Updates
This file was updated to include the new authorization module
class ApplicationController < ActionController::API include Authorization include JwtToken before_action :authorize end
This ensures that the ['has_required_role?'] method is available globally along with ['is_role?']. These methods can then be used to override 'action_allowed?' to override the base implementation and provide more granular control
To test the functionality, based on the old Expertiza, the method was also overridden in the courses_controller.rb file:
class Api::V1::CoursesController < ApplicationController before_action :set_course, only: %i[ show update destroy add_ta view_tas remove_ta copy ] rescue_from ActiveRecord::RecordNotFound, with: :course_not_found rescue_from ActionController::ParameterMissing, with: :parameter_missing def action_allowed? has_required_role?('Instructor') end ...
Outcomes
1. Modular Authentication and Authorization System
- Separate JwtToken and Authorization concerns with distinct responsibilities
- Integration through ApplicationController middleware
- Flexible role-based access control per controller
- Clean separation between authentication and authorization components
- Independent scaling and maintenance capability
2. Role-Based Access Control (RBAC)
- Hierarchical role system with administrator override
- Controller-specific role requirements through action-specific checks
- Granular permission control at the action level
- Fine-grained access control while maintaining system flexibility
- Custom role requirements for different resources
3. Secure Authentication Flow
- JWT-based stateless authentication
- Token verification precedes protected actions
- Automatic current user loading from verified tokens
- Protection against unauthorized access and token tampering
- Maintains stateless architecture for scalability
4. Standardized Error Handling
- Consistent HTTP status codes (401 for authentication, 403 for authorization)
- Clear error messages for debugging
- Uniform error response format across the application
- Improved debugging experience
- Standardized client-side error handling
4. Developer-Friendly Design
- Rails conventions and idioms throughout
- Clear, purpose-indicating method names
- Modular design with extension points
- Reduced learning curve for new developers
- Minimal configuration required for new controllers
Design Principles
The redesign of the `AuthorizationHelper` module for JWT-based authentication will follow well-established design principles to ensure the solution is secure, maintainable, and scalable. Below are the key principles to be applied and how they will shape this redesign:
1. Single Responsibility Principle (SRP)
- Definition: Each module or class should have one clear responsibility.
- Implementation:
- JwtToken concern handles only authentication
- Authorization concern manages only authorization rules
- Benefit: Changes to authentication logic don't affect authorization rules and vice versa
2. Open / Closed Principle
- Definition: Software entities should be open for extension but closed for modification
- Implementation:
# Base authorization in Authorization concern def action_allowed? true # Base implementation end # Extended in CoursesController without modifying base def action_allowed? has_required_role?('Instructor') end
- Benefit: New controllers can implement their own authorization rules without changing existing code
3. Inheritance and Polymorphism
- Definition: Ability to present the same interface for different underlying forms (implementations)
- Implementation:
- All controllers inherit from ApplicationController
- Each can override action_allowed? with its implementation
- System treats all action_allowed? calls polymorphically
- Benefit: Consistent interface while allowing specialized behavior
4. Composition
- Implementation:
class ApplicationController < ActionController::API include Authorization # Composition through modules include JwtToken end
- Benefit: More flexible than deep inheritance hierarchies, allows mixing in different behaviors
5. Don't Repeat Yourself (DRY)
- Definition: Every piece of knowledge should have a single, unambiguous representation
- Implementation:
- Common authentication logic in JwtToken
- Shared authorization behavior in Authorization
- Base controller provides default implementations
- Benefit: Reduces code duplication and maintenance overhead
Test Plan
Objectives
- Validate Individual Methods: Ensure each method in the Authorization concern performs as expected under various scenarios.
- Simulate Real-World Scenarios: Test the integration of the Authorization concern with controllers and the overall application workflow.
- Ensure Security Compliance: Verify that the module effectively handles unauthorized access, token tampering, and other security vulnerabilities.
- Facilitate Future Development: Provide clear and maintainable tests to aid ongoing development and debugging.
Unit Tests
Definition: Unit tests verify that individual methods work correctly in isolation under different scenarios.
Tests for has_required_role?
Checks Privileges When Role is a String:
- Input: required_role set to 'Administrator' as a string.
- Expected Output: Returns true if the user has privileges for the Administrator role or higher.
- Validation: Ensure the method identifies the role from the string and checks privileges correctly.
context 'when required_role is a string' do let(:admin_role) { instance_double('Role') } before do allow(Role).to receive(:find_by_name).with('Administrator').and_return(admin_role) end it 'finds the role and checks privileges' do expect(role).to receive(:all_privileges_of?).with(admin_role).and_return(true) expect(controller.has_required_role?('Administrator')).to be true end end
Directly Checks Privileges for Role Object:
- Input: required_role set to an instance of Role.
- Expected Output: Returns false if the user does not meet the required role privileges.
- Validation: Ensure the method directly uses the role object without additional lookups.
context 'when required_role is a Role object' do let(:instructor_role) { instance_double('Role') } it 'directly checks privileges' do expect(role).to receive(:all_privileges_of?).with(instructor_role).and_return(false) expect(controller.has_required_role?(instructor_role)).to be false end end
Returns False When User is Not Logged In:
- Input: current_user set to nil.
- Expected Output: Returns false.
- Validation: Ensure the method gracefully handles the absence of a logged-in user.
context 'when user is not logged in' do before do allow(controller).to receive(:current_user).and_return(nil) end it 'returns false' do expect(controller.has_required_role?('Administrator')).to be false end end
Returns False When User Has No Role:
- Input: current_user has nil for role.
- Expected Output: Returns false.
- Validation: Ensure the method handles cases where the user object exists but has no role assigned.
context 'when user has no role' do before do allow(user).to receive(:role).and_return(nil) end it 'returns false' do expect(controller.has_required_role?('Administrator')).to be false end end
Tests for is_role?
Checks Exact Role Match with a String:
- Input: role_name set to 'Student'.
- Expected Output: Returns true if the current user’s role is exactly 'Student'.
- Validation: Ensure the method matches the string representation of the role correctly.
context 'when role_name is a string' do before do allow(role).to receive(:name).and_return('Student') end it 'returns true when roles match' do expect(controller.is_role?('Student')).to be true end it 'returns false when roles do not match' do expect(controller.is_role?('Instructor')).to be false end end
Compares Role Names Using a Role Object:
- Input: role_name as a Role object with the name 'Student'.
- Expected Output: Returns true if the current user’s role name matches the role object name.
- Validation: Ensure the method extracts the role name and performs the comparison accurately.
context 'when role_name is a Role object' do let(:role_object) { instance_double('Role', name: 'Student') } before do allow(role).to receive(:name).and_return('Student') allow(role_object).to receive(:name).and_return('Student') allow(role_object).to receive(:is_a?).with(Role).and_return(true) end it 'compares using the role name' do expect(controller.is_role?(role_object)).to be true end end
Handles Missing User Gracefully:
- Input: current_user is nil.
- Expected Output: Returns false.
- Validation: Ensure the method behaves as expected when there is no logged-in user.
context 'when user is not logged in' do before do allow(controller).to receive(:current_user).and_return(nil) end it 'returns false' do expect(controller.is_role?('Student')).to be false end end
Handles User with No Role Gracefully:
- Input: current_user has nil for role.
- Expected Output: Returns false.
- Validation: Ensure the method handles cases where the user exists but has no role.
context 'when user has no role' do before do allow(user).to receive(:role).and_return(nil) end it 'returns false' do expect(controller.is_role?('Student')).to be false end end
Tests for all_actions_allowed?
Returns True for Users with Super Administrator Role:
- Input: current_user has the role 'Super Administrator'.
- Expected Output: Returns true.
- Validation: Ensure that users with sufficient privileges bypass further checks.
context 'when user has no role' do before do allow(user).to receive(:role).and_return(nil) end it 'returns false' do expect(controller.is_role?('Student')).to be false end end
Checks action_allowed? When User Lacks Administrator Role:
- Input: current_user does not have the role 'Super Administrator'.
- Expected Output: Returns false if action_allowed? also returns false.
- Validation: Ensure the method correctly delegates to action_allowed? when necessary.
context 'when the user does not have the Super Administrator role' do before do allow(controller).to receive(:has_required_role?).with('Super Administrator').and_return(false) allow(controller).to receive(:action_allowed?).and_return(false) end it 'checks action_allowed? and returns false' do expect(controller.all_actions_allowed?).to be false end end
Returns True When action_allowed? Returns True:
- Input: action_allowed? returns true, even if has_required_role? returns false.
- Expected Output: Returns true.
- Validation: Ensure the method respects the result of action_allowed?.
context 'when action_allowed? returns true' do before do allow(controller).to receive(:has_required_role?).with('Super Administrator').and_return(false) allow(controller).to receive(:action_allowed?).and_return(true) end it 'returns true' do expect(controller.all_actions_allowed?).to be true end end
Tests for authorize
Does Not Render Error for Authorized Users:
- Input: all_actions_allowed? returns true.
- Expected Output: No error response is rendered.
- Validation: Ensure the method bypasses rendering when the user is authorized.
context 'when all actions are allowed' do before do allow(controller).to receive(:all_actions_allowed?).and_return(true) end it 'does not render an error response' do expect(controller).not_to receive(:render) controller.authorize end end
Renders Forbidden Response for Unauthorized Actions:
- Input: all_actions_allowed? returns false.
- Expected Output: Renders a JSON response with an error message and a 403 Forbidden status.
- Validation: Ensure the method renders the appropriate error message and status for unauthorized users.
context 'when actions are not allowed' do before do allow(controller).to receive(:all_actions_allowed?).and_return(false) allow(controller.params).to receive(:[]).with(:action).and_return('edit') allow(controller.params).to receive(:[]).with(:controller).and_return('users') end it 'renders an error response with forbidden status' do expect(controller).to receive(:render).with( json: { error: "You are not authorized to edit this users" }, status: :forbidden ) controller.authorize end end
Tests for action_allowed?
Returns True by Default:
- Input: No specific implementation of action_allowed?.
- Expected Output: Returns true.
- Validation: Ensure the default implementation allows all actions.
it 'returns true by default' do expect(controller.action_allowed?).to be true end
Can Be Overridden for Custom Logic:
- Input: Subclass with a custom action_allowed? implementation that returns false.
- Expected Output: Returns false in the subclass.
- Validation: Ensure the method can be overridden to provide custom authorization logic.
it 'can be overridden in subclasses for custom logic' do class CustomController < ApplicationController include Authorization def action_allowed? false end end custom_controller = CustomController.new expect(custom_controller.action_allowed?).to be false end
Authorization Unit test stats
Integration Tests
Definition: Integration tests validate that the `AuthorizationHelper` module functions correctly within the context of the overall system.
Scenarios
Authenticated Access:
- Requests to endpoints with valid JWT tokens should succeed.
- Confirm correct permissions based on the user’s role.
Unauthorized Access:
- Requests with invalid or missing tokens should fail.
- Confirm proper error messages and HTTP status codes.
Role-Based Access Control:
- Ensure users with sufficient roles can access specific actions.
- Test granular access control for individual controllers.
Token Management:
- Validate handling of expired, tampered, or revoked tokens.
Endpoints to Test
ApplicationController:
- Validate authorize as a global before_action filter.
CoursesController:
- We have changed the tests for the course flow in order to test the logic for authorization. We have initialized the dummy data before the tests and are running the tests for the courses to validate this. The tests for TA related activities under the course flow would fail as it would require refactoring in the way the course logic is written with respect to its interaction with TA
before(:all) do # Create roles in hierarchy @super_admin = Role.find_or_create_by(name: 'Super Administrator') @admin = Role.find_or_create_by(name: 'Administrator', parent_id: @super_admin.id) @instructor = Role.find_or_create_by(name: 'Instructor', parent_id: @admin.id) @ta = Role.find_or_create_by(name: 'Teaching Assistant', parent_id: @instructor.id) @student = Role.find_or_create_by(name: 'Student', parent_id: @ta.id) end let(:prof) { User.create( name: "profa", password_digest: "password", role_id: @instructor.id, full_name: "Prof A", email: "testuser@example.com", mru_directory_path: "/home/testuser", ) } let(:token) { JsonWebToken.encode({id: prof.id}) } let(:Authorization) { "Bearer #{token}" }


Security Tests
Definition: Security tests focus on identifying vulnerabilities in the JWT authentication and authorization process.
Test Cases
- Expired Tokens: Verify that expired tokens are rejected with appropriate error codes.
- Tampered Tokens: Ensure tokens with altered signatures fail verification.
- Token Revocation: Test token invalidation during user logout or role changes.
- Injection Attacks: Validate inputs to ensure resilience against malicious payloads.
Pull Request
https://github.com/expertiza/reimplementation-back-end/pull/140/
Demo Video
https://go.ncsu.edu/oodd-final-oss-e2487
Team
Mentor
- Kashika Mallick (kmallick@ncsu.edu)
Members
- Shafa Hassan (shassa22@ncsu.edu)
- Archit Gupta (agupta85@ncsu.edu)
- Ansh Ganatra (aganatr@ncsu.edu)