ruby-on-railsruby-grape

Rails +Grape API Authorization | How to validate token scope on a per-endpoint basis?


I am working on configuring security for my rails application. At this point the user can authenticate and get access tokens to submit to the API, and the API can validate those tokens before an endpoint is invoked. Now I would like to validate the scope of the token, but what scopes are required are really an endpoint specific matter.

The simplest way would be to create a validate_scope! method and just call that at the top of each request; but I'd really prefer to keep it out of the endpoint request handler and instead find a way to add this to, say, the request definition. Maybe something like this:

get 'endpoint', required_scopes: ['custom.scope'] do
  ...
end

Or perhaps something similar to the params block placed just before the endpoint.

params do
  ..
end
claims do
 requires scope  type: [String] do
  requires 'custom.scope'
 end
end
get 'endpoint' do
  ..
end

Are there any existing solutions for this, or perhaps some documentation that can point me in the right direction for writing my own such validation?

I also want to integrate with AWS Verified Permissions later, for which I'll want to setup a similar validation call for particular endpoints.


Solution

  • After digging into the doc + github code a bit, I was able to come up with a solution. First, you can add metadata to an endpoint trivially in one of two ways. The first way is to specify an options object that gets merged into the route's options object - at the top-level. This can be done like so:

    get 'my-endpoint', { 'custom': 'option' } do
      ...
    end
    

    Using the active route object within a before { ... } block, you can grab those options like so:

    before {
      custom = route.options.custom
      ... 
    }
    

    Alternatively, and perhaps preferably so as to avoid any potential collisions, you can use route_setting to add to the settings object (which seems to be empty be default).

    route_setting :custom, "value"
    get 'my-endpoint' do
      ...
    end
    

    You could then access the route settings like so:

    before {
      custom = route.settings[:custom]
      ...
    }
    

    With either approach (but I'll favor the later), you can now add various details for your authentication and authorization flows. For example, we can add a :public? setting used to skip authentication + authorization, and a :scopes parameter that we can use to further validate the token.

    route_setting :public?, true
    get 'my-public-route' do
      ...
    end
    
    route_setting :scopes, ['custom-scope'].to_set
    get 'my-authenticated-route' do
      ...
    end
    

    Then use a common before {...} block prior to mounting any api modules or endpoints and evaluate the active route object like so:

    before {
      next if route.settings[:public?]
    
      authenticate!
    
      if route.settings.key?(:scopes)
        error!(403, 'Forbidden') unless token_scopes().superset?(route.settings[:scopes])
      end
    
      # additional authorization 
    }