Forbidden Kisses & HTTP Fluency in Clearance

Dan Croak

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.

401 Unauthorized

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.

403 Forbidden

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.

When to raise errors

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 Error.

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 requests (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:

raise ActionController::Forbidden

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.

Attribution

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.

Clearance is on GitHub.