giant robots smashing into other giant robots

Written by thoughtbot

dancroak

Forbidden kisses & HTTP fluency in Clearance

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:

<ul>
<li>Know when to return which <span class="caps">HTTP</span> status codes.</li>
    <li>Know when to raise errors.</li>
</ul>


<h2>401 Unauthorized</h2>


<p>In layman&#8217;s terms:</p>


<blockquote>
    <p>&#8220;Specifically for use when authentication is possible but has failed or not yet been provided.&#8221;</p>
</blockquote>


<p>The response is <a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_Error">401 Unauthorized</a> &#8220;out of the box&#8221; with Clearance when:</p>


<ul>
<li>A user tries to sign in with bad credentials.</li>
    <li>A user without confirmed email tries to sign in.</li>
</ul>


<p>If you protect an action with <code>before_filter :authenticate</code> in your app, Clearance will also return 401 Unauthorized when:</p>


<ul>
<li>A user who is not signed in tries to access that action.</li>
</ul>


<h2>403 Forbidden</h2>


<p>In layman&#8217;s terms:</p>


<blockquote>
    <p>&#8220;The request was a legal request, but the server is refusing to respond to it.<br />

Unlike a 401 Unauthorized response, authenticating will make no difference.”

<p><img src="http://ui.thoughtbot.com/assets/2009-2-22-forbdden_kiss.jpg" style="float:right;" />The response is <a href="http://en.wikipedia.org/wiki/HTTP_403">403 Forbidden</a> &#8220;out of the box&#8221; with Clearance when:</p>


<ul>
<li>A user tries to confirm a user with confirmed email.</li>
    <li>A user tries to confirm a user without a token.</li>
    <li>A user tries to confirm a user without the correct token for an unconfirmed user.</li>
    <li>A user tries to edit a user&#8217;s password without a token.</li>
    <li>A user tries to update a user&#8217;s password without a token.</li>
    <li>A user tries to edit a user&#8217;s password without the correct token for the user.</li>
    <li>A user tries to update a user&#8217;s password without the correct token for the user.</li>
</ul>


<p>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.</p>


<h2>When to raise errors</h2>


<p>Consider a typical edit, show, or destroy action:</p>


<pre><code>def show

@user = User.find(params[:id]) end

<p>In the development and test environments, this will raise a <code>ActiveRecord::RecordNotFound</code> 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.</p>


<p>Rails does this by rescuing the <code>ActiveRecord::RecordNotFound</code> error for <strong>public requests</strong> (for example, staging or production environments). Inside the rescue, it returns the logical status code, :not_found. For <a href="http://api.rubyonrails.org/classes/ActionController/Rescue.html#M000397"><strong>local requests</strong></a> (for example, development or test environments), the error is not rescued.</p>


<p>Rails provides similar functionality for other errors:</p>


<pre><code>'ActionController::RoutingError'             =&gt; :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

<p>This maps errors to <span class="caps">HTTP</span> status codes.</p>


<p>Clearance creates a custom error, <code>ActionController::Forbidden</code>, and maps it to <code>:forbidden</code> to match this convention.</p>


<p>So when situations arise when 403 Forbidden is called for, Clearance simply does:</p>


<pre><code>raise ActionController::Forbidden</code></pre>


<p>The effect is exactly like <code>ActiveRecord::RecordNotFound</code>. 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 <span class="caps">HTTP</span> status code.</p>


<p><em>Note:</em> One could argue that Rails could provide a mapping like this for all <span class="caps">HTTP</span> status codes, or at least a few more of the most common ones. A patch for another day, perhaps.</p>


<h2>Attribution</h2>


<p>These ideas were lifted from the good work coming out of the <a href="http://merbivore.com">Merb</a> community. The implementation was driven out through conversations with Tim Pope, Joe Ferris, Mike Burns, and Jason Morrison.</p>


<p><a href="http://github.com/thoughtbot/clearance">Clearance is on github</a>.</p>