The project that I'm working on requires me to add custom headers based on generated response body to all responses generated by my app. This works fine with after_action in my application controller, but I also need to add the custom header to the token responses generated with Doorkeeper. I setting base_controller to ApplicationController in doorkeeper configuration, but this did not cause my after_actions to be called. Are there some possible workarounds?
Turns out that it takes one to define a custom Doorkeeper::TokensController class and add an filter to it.
app/controllers/access_tokens_controller.rb:
class AccessTokensController < Doorkeeper::TokensController
include AbstractController::Callbacks
after_action :add_signature_to_response, only: [:create]
def add_signature_to_response
application = strategy.client.application
# ...
# response_based_on_application = ...
# ...
response.headers['custom-header'] = response_based_on_application
end
end
Next one needs to register that controller in doorkeeper configuration in config/initializers/doorkeeper.rb
# ...
use_doorkeeper scope: 'oauth2' do
# ...
controllers tokens: 'access_tokens'
# ...
end
# ...
Related
I am trying to create a small project for practising the API Key Authentication using the this tutorial. I have created a concern in following directory of the rails project.
app/controllers/concerns/api_key_authenticable.rb
module ApiKeyAuthenticatable
extend ActiveSupport::Concern
include ActionController::HttpAuthentication::Basic::ControllerMethods
include ActionController::HttpAuthentication::Token::ControllerMethods
attr_reader :current_api_key
attr_reader :current_bearer
# Use this to raise an error and automatically respond with a 401 HTTP status
# code when API key authentication fails
def authenticate_with_api_key!
#current_bearer = authenticate_or_request_with_http_token &method(:authenticator)
end
# Use this for optional API key authentication
def authenticate_with_api_key
#current_bearer = authenticate_with_http_token &method(:authenticator)
end
private
attr_writer :current_api_key
attr_writer :current_bearer
def authenticator(http_token, options)
#current_api_key = ApiKey.find_by token: http_token
current_api_key&.bearer
end
end
In my Controller, I am trying to include the concern like this
app/controllers/ApiKeysController.rb
class ApiKeysController < ApplicationController
include ApiKeyAuthenticatable
# Require token authentication for index
prepend_before_action :authenticate_with_api_key!, only: [:index]
# Optional token authentication for logout
prepend_before_action :authenticate_with_api_key, only: [:destroy]
def index
end
def create
end
def destroy
end
end
But when I run the server and visit the index action of the controller, I get the following error
NameError (uninitialized constant ApiKeysController::ApiKeyAuthenticatable):
app/controllers/ApiKeysController.rb:10:in `<class:ApiKeysController>'
app/controllers/ApiKeysController.rb:2:in `<main>'
Can anybody help me out to fix this issue?
Author here. Your concern’s filename is authenticable but your module is authenticatable. You’ll need to correct the typo in the concern filename.
Using devise gem to authenticate all users of an application.
I'm trying to implement Active Storage.
Let's say that all users must be authenticated as soon as they reach the app:
class ApplicationController < ActionController::Base
before_action :authenticate_user!
...
end
How to secure the Active Storage generated routes?
URL of an uploaded file can be accessed without having to authenticate first. The unauthenticated user can get the file url generated by Active Storage.
This is not a full answer but a starting point:
The gist: You would need to override the redirect controller.
The docs for activestorage/app/controllers/active_storage/blobs_controller.rb say:
If you need to enforce access protection beyond the
security-through-obscurity factor of the signed blob references,
you'll need to implement your own authenticated redirection
controller.
Also if you plan to use previews the docs for activestorage/app/models/active_storage/blob/representable.rb say
Active Storage provides one [controller action for previews], but you may want to create your own (for
example, if you need authentication).
Also you might find some relevant information in this rails github issue
Update:
Here is a minimal example that "should" work for preventing unauthorised access to the redirects when using the devise gem.
How the url, that the user will be redirected to if logged, is then secured is still another story I guess. By default they expire after 5 minutes but this could be set to a shorter period like 10 seconds (if you replace line 6 in example below with expires_in 10.seconds)
Create a file app/controllers/active_storage/blobs_controller.rb with the following code:
class ActiveStorage::BlobsController < ActiveStorage::BaseController
before_action :authenticate_user!
include ActiveStorage::SetBlob
def show
expires_in ActiveStorage::Blob.service.url_expires_in
redirect_to #blob.service_url(disposition: params[:disposition])
end
end
Please note that the only thing that changed from the original code is that the second line is added
before_action :authenticate_user!
Update 2:
Here is a concern that you can include in ActiveStorage::RepresentationsController and ActiveStorage::BlobsController to enable devise authentication for ActiveStorage
See gist is at https://gist.github.com/dommmel/4e41b204b97238e9aaf35939ae8e1666 also included here:
# Rails controller concern to enable Devise authentication for ActiveStorage.
# Put it in +app/controllers/concerns/blob_authenticatable.rb+ and include it when overriding
# +ActiveStorage::BlobsController+ and +ActiveStorage::RepresentationsController+.
#
# Optional configuration:
#
# Set the model that includes devise's database_authenticatable.
# Defaults to Devise.default_scope which defaults to the first
# devise role declared in your routes (usually :user)
#
# blob_authenticatable resource: :admin
#
# To specify how to determine if the current_user is allowed to access the
# blob, override the can_access_blob? method
#
# Minimal example:
#
# class ActiveStorage::BlobsController < ActiveStorage::BaseController
# include ActiveStorage::SetBlob
# include AdminOrUserAuthenticatable
#
# def show
# expires_in ActiveStorage::Blob.service.url_expires_in
# redirect_to #blob.service_url(disposition: params[:disposition])
# end
# end
#
# Complete example:
#
# class ActiveStorage::RepresentationsController < ActiveStorage::BaseController
# include ActiveStorage::SetBlob
# include AdminOrUserAuthenticatable
#
# blob_authenticatable resource: :admin
#
# def show
# expires_in ActiveStorage::Blob.service.url_expires_in
# redirect_to #blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
# end
#
# private
#
# def can_access_blob?(current_user)
# #blob.attachments.map(&:record).all? { |record| record.user == current_user }
# end
# end
module BlobAuthenticatable
extend ActiveSupport::Concern
included do
around_action :wrap_in_authentication
end
module ClassMethods
def auth_resource
#auth_resource || Devise.default_scope
end
private
def blob_authenticatable(resource:)
#auth_resource = resource
end
end
private
def wrap_in_authentication
is_signed_in_and_authorized = send("#{self.class.auth_resource}_signed_in?") \
& can_access_blob?(send("current_#{self.class.auth_resource}"))
if is_signed_in_and_authorized
yield
else
head :unauthorized
end
end
def can_access_blob?(_user)
true
end
end
If you want to implement authentication for all endpoints provided by active storage, you can override the ActiveStorage::BaseController based on the original implementation:
# app/controllers/active_storage/base_controller.rb
# frozen_string_literal: true
# The base class for all Active Storage controllers.
class ActiveStorage::BaseController < ActionController::Base
before_action :authenticate_user!
include ActiveStorage::SetCurrent
protect_from_forgery with: :exception
end
I am using apipie for API documentation.
The problem I am facing now is that the large amount of information to be provided for documentation is ruining my code structure in the controller.
How can I write the documentation outside the controller file,
so that the code structure will be readable?
You can find nice explanation on how to use apipie outside controller in this document https://ilyabylich.svbtle.com/apipie-amazing-tool-for-documenting-your-rails-api
To sum things up (example from link):
# app/docs/users_doc.rb
module UsersDoc
# we need the DSL, right?
extend Apipie::DSL::Concern
api :GET, '/users', 'List users'
def show
# Nothing here, it's just a stub
end
end
# app/controller/users_controller.rb
class UsersController < ApplicationController
include UsersDoc
def show
# Application code goes here
# and it overrides blank method
# from the module
end
end
super also needs to be included in the stubbed actions in the docs. In my app I created a controller that inherits from an existing controller defined in a gem, and if I don't include super in the stubbed action, I get unexpected behavior.
So:
# app/docs/users_doc.rb
module UsersDoc
# we need the DSL, right?
extend Apipie::DSL::Concern
api :GET, '/users', 'List users'
def show
super
end
end
# app/controller/users_controller.rb
class UsersController < ApplicationController
include UsersDoc
def show
# Application code goes here
end
end
I am using rails 4, devise for authentication and Pundit for authorization. I have restricted my application to check for authorization on every controller by below code.
class ApplicationController < ActionController::Base
include Pundit
after_action :verify_authorized
#.....
end
However, i want to skip authorization for two specific controllers in my application (they are open to public, users do not need to sign in). How can i achieve it without removing verify_authorized in ApplicationController ?
skip_after_action :verify_authorized
I'm working with Rails 5 and I wanted to skip authorization in just one action but not the whole controller. So, what you can do according to the documentation is to use skip_authorization feature in the controller action as shown below:
class Admin::DashboardController < Admin::BaseController
def index
#organizers = Organizer.count
#sponsors = Sponsor.count
#brochures = Brochure.count
skip_authorization
end
def sponsors_approve
# some statements...
end
def organizers_approve
# some statements...
end
end
In this controller the only one action to be skipped is index, the other ones must be authorized.
I hope it could be useful for somebody else.
I am working on a generic controller to provide default functionality to the RESTful actions, but I am having trouble when I need to add additional routes. The default behavior I am after renders all actions in either an index view, or a show view. My code is setup like this (lots of unrelated code is trimmed);
module RestController extend ActiveSupport::Concern
included do
before_action: setup_index_view,
only: [:index, :new, :create] << #additional_index_routes
before_action: setup_show_view,
only: [:show, :edit:, update, :destroy] << #additional_show_routes
end
def setup_rest_controller(_additional_index_routes,
_additional_show_routes)
#additional_index_routes = _additional_index_routes
#additional_show_routes = _additional_show_routes
end
def setup_index_view
# default behavior, sets #{collection_name} = {Model.collection}
# code trimmed
end
def setup_show_view
# default behavior, sets #{instance_name} = {Model.find(params[:id]}
# code trimmed
end
end
class Order < ApplicationController
def initialize
setup_rest_controller(
_additional_show_routes: [:quote, :contract, invoice_report])
end
def setup_index_view
#override default behavior to filter andpaginate collection
#orders = Orders.active.search(search_terms).paginate
end
#accept default behavior for setup_views
end
In development, this code is working fine (I admit that surprises me a little), but in production the additional routes are not running the setup_show_method. The only difference in the gem file is that development includes rspec-rails. Does anyone know why this code would behave differently?
EDIT
As soon as I hit post, the 'why' hit me. In development the code is reloaded at each request, and in production it is only loaded once...at first load #additional_show_routes is not set, and therefore no additional routes are added to the before_action call. New question then...how do I get the desired behavior?
Adding a call to before_action in the OrdersController overrides the one in RestController and breaks the default functionality.
Adding a call to before_action in setup_rest_controller throws a NoMethodError.
When you add a concern like this, is there a method like new that I can use instead of setup_rest_controller to set #additional_show_views earlier?
This feels like a hack, but it seems to get the desired result;
class OrdersController < ApplicationController
prepend_before_action Proc.new { send :setup_index_view},
only: [:new_restful_action]
def setup_index_view
#override default behavior to filter andp aginate collection
#orders = Orders.active.search(search_terms).paginate
end
#accept default behavior for setup_views
end
I also drop all references to #additional_{X}_routes in RestController. I don't know that prepend_ is required in the code as written above, but in my actual code there are other before_filters that need to run after #additional_{X}_routes...