UPDATE: After about two years of using this approach in Clearance, we removed the 403 Forbidden feature in Clearance. We discovered that setting the 403 status code turned out to be a bad user experience in some browsers such as Chrome on Windows machines. Philosophically, we decided we value user experience over technical purity.
Clearance tries to be fluent in HTTP. That means a few things:
- Know when to return which HTTP status codes.
- Know when to raise errors.
In layman’s terms:
Specifically for use when authentication is possible but has failed or not yet been provided.
The response is 401 Unauthorized out of the box with Clearance when:
- A user tries to sign in with bad credentials.
- A user without confirmed email tries to sign in.
If you protect an action with
before_filter :authenticate in your app,
Clearance will also return 401 Unauthorized when:
- A user who is not signed in tries to access that action.
In layman’s terms:
The request was a legal request, but the server is refusing to respond to it. Unlike a 401 Unauthorized response, authenticating will make no difference.
The response is 403 Forbidden out of the box with Clearance when:
- A user tries to confirm a user with confirmed email.
- A user tries to confirm a user without a token.
- A user tries to confirm a user without the correct token for an unconfirmed user.
- A user tries to edit a user’s password without a token.
- A user tries to update a user’s password without a token.
- A user tries to edit a user’s password without the correct token for the user.
- A user tries to update a user’s password without the correct token for the user.
These are legal requests by someone or something (maybe a malicious user) requesting actions in forbidden, exceptional ways. They are not available to any user, regardless of their authentication status. The server should refuse to respond to it.
Consider a typical edit, show, or destroy action:
def show @user = User.find(params[:id]) end
In the development and test environments, this will raise a
ActiveRecord::RecordNotFound error if a User does not exist for the given id.
In production, this will return 404 Not Found instead of 500 Internal Server
Rails does this by rescuing the
ActiveRecord::RecordNotFound error for
public requests (for example, staging or production environments). Inside
the rescue, it returns the logical status code,
:not_found. For local
(for example, development or test environments), the error is not rescued.
Rails provides similar functionality for other errors:
'ActionController::RoutingError' => :not_found, 'ActionController::UnknownAction' => :not_found, 'ActiveRecord::RecordNotFound' => :not_found, 'ActiveRecord::StaleObjectError' => :conflict, 'ActiveRecord::RecordInvalid' => :unprocessable_entity, 'ActiveRecord::RecordNotSaved' => :unprocessable_entity, 'ActionController::MethodNotAllowed' => :method_not_allowed, 'ActionController::NotImplemented' => :not_implemented, 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
This maps errors to HTTP status codes.
Clearance creates a custom error,
ActionController::Forbidden, and maps it to
:forbidden to match this convention
So when situations arise when 403 Forbidden is called for, Clearance simply does:
The effect is exactly like
ActiveRecord::RecordNotFound. In development and
test environments, the developer has the opportunity to investigate what is
going wrong. In staging and production, the app behaves like a good internet
citizen by responding with the correct HTTP status code.
Note: One could argue that Rails could provide a mapping like this for all HTTP status codes, or at least a few more of the most common ones. A patch for another day, perhaps.
These ideas were lifted from the good work coming out of the Merb community. The implementation was driven out through conversations with Tim Pope, Joe Ferris, Mike Burns, and Jason Morrison.