Skip to main content
Skip table of contents

Authorization Design Doc

Author: Alastair Dawson 

Introduction

Access to sensitive information and online services is crucial, ensuring robust user authentication is paramount. User authentication forms the first line of defense against unauthorized access, safeguarding both user data and organizational assets. This document serves as a comprehensive guide to understanding, implementing, and maintaining effective user authentication systems.

Goal

Separate the concern of authorization from the protected service.

Background

Endpoints are currently protected at a high level by authentication from an OIDC provider. Once a user logs in, services they have access to are included in the navigation on the front-end. Requests to those service endpoints are then rechecked against user attributes from the back-end service classes. 

The logic for navigation filtration lives in the User object and gets called by the UserSerializer. Some of that logic is shared by the back-end services, although many supplement it with additional checks on the user and other submitted parameters. This has lead to a bit of a fractured authorization story; business rules for authorization are not currently self documenting or easily discoverable. 

Although we have auth checks in the services themselves it's more secure, and efficient, to return an error before a controller action if called. This will allow services to more closely follow the single responsibility principle, reduce service code, and make test cases more succinct.

High level design

  • Extract the authorization methods from the User object and service classes into a policy per service. 

  • Add before_filters in service controllers to check the related policy file.

  • Update application controller to raise 403 Forbidden errors when the auth framework rejects a user.

  • Update UserSerializer to use the policy classes rather than the methods on User.

Specifics

Detailed Design

The Pundit gem provides a set of helpers that hook into Rails using regular Ruby classes. The standard usage of the Pundit is to depend on ActiveRecord based models to check authorization. As most vets-api resources are accessed through services we'll need to use the headless policies feature. Below is an example of updating the EVSS letters service to use pundit.

The policy file for all EVSS routes check the same policy method can?:

RUBY
EVSSPolicy = Struct.new(:user, :evss) do  
  def can?  
    user.edipi.present? && user.ssn.present? && user.participant_id.present?  
  end  
end

If some endpoints require different authorization, additional methods can be added:

RUBY
EVSSPolicy = Struct.new(:user, :evss) do  
  def can?  
    user.edipi.present? && user.ssn.present? && user.participant_id.present?  
  end  

  def can_update_ssn?  
    user.ssn.present?  
  end
end

Shared authorization can be defined in base controllers:

RUBY
module V0
  class EVSSController < ApplicationController
  
  private

  def authorize_user  
    authorize :evss, :can?  
  end
end

A child controller can use a before_action to add authorization for all endpoints:

RUBY
module V0  
  class LettersController < EVSSController  
    before_action :authorize_user

If a controller’s actions don’t need authorization we can use before_action filters to skip them:

RUBY
before_action :authorize_user, except:[:index]

If an action has different auth rules there are two options:

  1. Add another before_action filtered for that action

    • RUBY
      before_action :authorize_user, except:[:update_ssn]
      before_action :authorize_user_ssn, only:[:update_ssn]
  2. Call the pundit authorize method from within the action’s method

    • RUBY
      def update_ssn
        authorize :evss, :can_update_ssn?
      end

ApplicationController will need to be updated to catch Pundit::NotAuthorizedError errors:

RUBY
va_exception =
  case exception
  when Pundit::NotAuthorizedError
    Common::Exceptions::Forbidden.new('User does not have access to the requested resource')
  when ActionController::ParameterMissing
    Common::Exceptions::ParameterMissing.new(exception.param)

Alternatives and Future work

An alternative discussed at the Ad Hoc/va.gov day is to break authorization out into a micro service. As vets-api currently stands this may be premature optimization. If vets-api's services get broken out/extended with a Zapier like directory of VA APIs this may make more sense. Authorization could be part of a smart reverse proxy. The proxy would check that a user has access to the request endpoint. If the user is approved, the request is then forwarded on to the appropriate app/micro service.


JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.