Skip to content

Commit

Permalink
Improve the Active Storage "Authenticated Controllers" guide
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiculescu committed Jul 29, 2024
1 parent 077d6e8 commit 4d25ad2
Showing 1 changed file with 97 additions and 16 deletions.
113 changes: 97 additions & 16 deletions guides/source/active_storage_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -786,14 +786,34 @@ All Active Storage controllers are publicly accessible by default. The generated
URLs use a plain [`signed_id`][ActiveStorage::Blob#signed_id], making them hard to
guess but permanent. Anyone that knows the blob URL will be able to access it,
even if a `before_action` in your `ApplicationController` would otherwise
require a login. If your files require a higher level of protection, you can
implement your own authenticated controllers, based on the
[`ActiveStorage::Blobs::RedirectController`][],
[`ActiveStorage::Blobs::ProxyController`][],
[`ActiveStorage::Representations::RedirectController`][] and
[`ActiveStorage::Representations::ProxyController`][]
require a login.

To only allow an account to access their own logo you could do the following:
If you need to, there's a few ways to make this more secure.

#### Make URLs expire by default

By default, `config.active_storage.urls_expire_in` is `nil`. If you give it a value,
then URLs will not be permanent. This works for all URLs generated by Active Storage,
when using `link_to`, `url_for`, `image_tag`, and so on

```ruby
# config/application.rb
config.active_storage.urls_expire_in = 1.day
```

In this example, the URL will work when accessed immediately, but a day later, will stop working.
Calling `url_for` again will return a different URL.

```ruby
url_for(user.avatar)
# => https://www.example.com/rails/active_storage/blobs/redirect/:signed_id/my-avatar.png
```

#### Create dedicated routes for specific actions

You can make dedicated routes for application functionality, and add your own
authentication logic to these actions. For example, say you want to display the
current authenticated account's logo on every page.

```ruby
# config/routes.rb
Expand All @@ -805,9 +825,6 @@ end
```ruby
# app/controllers/logos_controller.rb
class LogosController < ApplicationController
# Through ApplicationController:
# include Authenticate, SetCurrentAccount
def show
redirect_to Current.account.logo.url
end
Expand All @@ -818,18 +835,82 @@ end
<%= image_tag account_logo_path %>
```

And then you should disable the Active Storage default routes with:
In `ApplicationController` you would run authentication logic, so users that are
not logged in are redirected away, and the current account is set.

#### Implement your own Active Storage controllers

By overriding `resolve_model_to_route`, you can provide your own routing method that
will take an Active Storage model and generate the appropriate route for it. Then you can
create your own routes, pointing to your own controllers. See [`activestorage/config/routes.rb`][routes.rb]
for the full set of routes that Rails generates.

```ruby
# config/application.rb
config.active_storage.resolve_model_to_route = :active_storage_authenticated_director
```

```ruby
# config/routes.rb
get "/storage/blobs/:signed_id/*filename" => "storage/blobs#show", as: :active_storage_authenticated_blob
get "/representations/redirect/:signed_blob_id/:variation_key/*filename" => "storage/representations_redirect#show", as: :active_storage_authenticated_representation
direct :active_storage_authenticated_director do |model, options|
expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
expires_at = options.delete(:expires_at)
if model.respond_to?(:signed_id)
route_for(
:active_storage_authenticated_blob,
model.signed_id(expires_in: expires_in, expires_at: expires_at),
model.filename,
options
)
else
route_for(
:active_storage_authenticated_representation,
model.blob.signed_id(expires_in: expires_in, expires_at: expires_at),
model.variation.key,
model.blob.filename,
options
)
end
end
```

This controller copies the implementation of `ActiveStorage::Blobs::RedirectController`, but
as it subclasses our `ApplicationController` it will include authentication checks. You could
take this even further by running user-specific permission checks based on the requested blob.

```ruby
# app/controllers/storage/blobs_controller.rb
class Storage::BlobsController < ApplicationController
include ActiveStorage::SetBlob
include ActiveStorage::SetCurrent
protect_from_forgery with: :exception
def show
expires_in(ActiveStorage.service_urls_expire_in)
redirect_to @blob.url(disposition: params[:disposition]), allow_other_host: true
end
end
```

With this approach, if you call `url_for(attachment)` or `image_tag(attachment.representation)`,
`active_storage_authenticated_director` will get used to generate a URL that will pass through your
secure `Storage::BlobsController`.


#### Disable Active Storage routes

To disable Active Storage routes entirely:

```ruby
# config/application.rb
config.active_storage.draw_routes = false
```

to prevent files being accessed with the publicly accessible URLs.
To link to an attachment or render a file you will need to generate the URL by hand.

[`ActiveStorage::Blobs::RedirectController`]: https://api.rubyonrails.org/classes/ActiveStorage/Blobs/RedirectController.html
[`ActiveStorage::Blobs::ProxyController`]: https://api.rubyonrails.org/classes/ActiveStorage/Blobs/ProxyController.html
[`ActiveStorage::Representations::RedirectController`]: https://api.rubyonrails.org/classes/ActiveStorage/Representations/RedirectController.html
[`ActiveStorage::Representations::ProxyController`]: https://api.rubyonrails.org/classes/ActiveStorage/Representations/ProxyController.html
[routes.rb]: https://github.com/rails/rails/blob/e0a9ef12b39a4871d8f143dae926931b3c3a0919/activestorage/config/routes.rb

Downloading Files
-----------------
Expand Down

0 comments on commit 4d25ad2

Please sign in to comment.