Authenticating your Angular / Rails App with JSON Web Tokens

<p><em>UPDATE: There have been some changes in the JWT Gem that make some of the below not work exactly right (it&rsquo;ll still be about 90% the same). Specifically, they added expiration support. See <a href="/2015/07/20/authentication-using-json-web-tokens-using-rails-and-react">my post on the same topic, but using React.js</a>. The server side code in this post will work just as well with Angular.</em></p> <p></p> <h2 id="overview">Overview</h2> <p>I&rsquo;m a big proponent of rolling your own authentication solution, especially if you&rsquo;re only doing simple username/password based logins (as opposed to logging in via an OAuth provider). I&rsquo;ve tried to use <a href="https://github.com/plataformatec/devise">Devise</a> on a number of Rails apps, but I always end up ripping it out. It&rsquo;s not because Devise is a bad gem, but because it always takes me more time to customize it to my liking than it does to just write everything myself. And the flexibility of a custom solution almost always comes in handy down the road. I have generally implemented it the same way that Ryan Bates does in <a href="http://railscasts.com/episodes/250-authentication-from-scratch-revised">this Railscasts episode</a>.</p> <p>But now that most of my greenfield projects are single page javascript apps, authentication has become slightly more complicated. While you can certainly continue doing traditional authentication with cookies and server-rendered views, my preference is to use a token-based approach. This has a number of benefits:</p> <ul> <li>The same authentication API can be used by all types of clients (web app, mobile app, etc).</li> <li>It is stateless, so the web server does not have to keep track of session information, which is good for scaling.</li> <li>Protected against CSRF (cross-site request forgery) attacks</li> <li>All of your views are rendered by the client, rather than a mix of server and client rendered views.</li> </ul> <p>A relatively new standard for accomplishing this is <a href="http://jwt.io/">JSON Web Tokens</a> (abbreviated to JWT). I won&rsquo;t dig into the details because there are plenty of good resources, but JWT is a way of digitally signing data to be transferred between two parties. The data is represented as an encoded JSON object. In a nutshell, these tokens are passed to the client upon successful authentication and then subsequently used in every HTTP request in order to verify the identity of the client.</p> <p></p> <h3 id="client-server-data-flow">Client/Server Data Flow</h3> <p>So the application flow will look something like this:</p> <ol> <li>Client sends username and password to server.</li> <li>If credentials are valid, the server generates a token that include&rsquo;s the user&rsquo;s ID inside the token payload. (Remember, this payload is not encrypted - the client can read it - so don&rsquo;t put anything you don&rsquo;t want the client to see)</li> <li>The token is returned to the client, who saves it somewhere for later use.</li> <li>When the client makes a request for protected data from the server, it includes the token in an HTTP header.</li> <li>Upon receiving a request for protected data, the server looks at the token and verifies that it was indeed generated for the user represented by the ID in the payload.</li> </ol> <h2 id="server-side-code">Server-Side Code</h2> <p>So let&rsquo;s start with the server-side code and assume you already have a basic user model. We&rsquo;ll first need some code to generate a JWT for a given user. So install the <a href="https://github.com/progrium/ruby-jwt">jwt gem</a> into your Gemfile. Next, I found there to be fair amount of logic around the JWT auth tokens, so I extracted it into a simple <code>AuthToken</code> class that takes care of encoding and decoding the tokens for us.</p> <div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">AuthToken</span> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="n">exp</span><span class="o">=</span><span class="mi">24</span><span class="p">.</span><span class="nf">hours</span><span class="p">.</span><span class="nf">from_now</span><span class="p">)</span> <span class="n">payload</span><span class="p">[</span><span class="ss">:exp</span><span class="p">]</span> <span class="o">=</span> <span class="n">exp</span><span class="p">.</span><span class="nf">to_i</span> <span class="no">JWT</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">,</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">secrets</span><span class="p">.</span><span class="nf">secret_key_base</span><span class="p">)</span> <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="n">payload</span> <span class="o">=</span> <span class="no">JWT</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">secrets</span><span class="p">.</span><span class="nf">secret_key_base</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="no">DecodedAuthToken</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span> <span class="k">rescue</span> <span class="kp">nil</span> <span class="c1"># It will raise an error if it is not a token that was generated with our secret key or if the user changes the contents of the payload</span> <span class="k">end</span> <span class="k">end</span> <span class="c1"># We could just return the payload as a hash, but having keys with indifferent access is always nice, plus we get an expired? method that will be useful later</span> <span class="k">class</span> <span class="nc">DecodedAuthToken</span> <span class="o">&lt;</span> <span class="no">HashWithIndifferentAccess</span> <span class="k">def</span> <span class="nf">expired?</span> <span class="nb">self</span><span class="p">[</span><span class="ss">:exp</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">to_i</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>And let&rsquo;s add a helper method to our <code>User</code> model that uses this class:</p> <div class="highlight"><pre class="highlight ruby"><code><span class="k">def</span> <span class="nf">generate_auth_token</span> <span class="n">payload</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">user_id: </span><span class="nb">self</span><span class="p">.</span><span class="nf">id</span> <span class="p">}</span> <span class="no">AuthToken</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span> <span class="k">end</span> </code></pre></div> <p>Ok, now we need to take care of that initial authentication request in our Client/Server Data flow. The client sends a username/password combination and the server sends back a new token. So go ahead and create a new controller called <code>AuthController</code> and add a new <code>post</code> route to <code>routes.rb</code>. You may also want to return some information about the current user inside the JSON response, but for now we&rsquo;ll just return the auth token.</p> <div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">AuthController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span> <span class="n">skip_before_action</span> <span class="ss">:authenticate_request</span> <span class="c1"># this will be implemented later</span> <span class="k">def</span> <span class="nf">authenticate</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by_credentials</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:username</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="c1"># you'll need to implement this</span> <span class="k">if</span> <span class="n">user</span> <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">auth_token: </span><span class="n">user</span><span class="p">.</span><span class="nf">generate_auth_token</span> <span class="p">}</span> <span class="k">else</span> <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid username or password'</span> <span class="p">},</span> <span class="ss">status: :unauthorized</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="c1"># in routes.rb:</span> <span class="n">post</span> <span class="s1">'auth'</span> <span class="o">=&gt;</span> <span class="s1">'auth#authenticate'</span> </code></pre></div> <p>Ok, now we need to add code to validate the token on subsequent requests. What we&rsquo;ll do first is implement a few helper methods in our <code>ApplicationController</code> that take care of decoding/validating the token and, based on the token payload, finding the current user. Then we&rsquo;ll tie them all together in a a before filter/action. If the token is properly decoded and the user found, the request can be continued. If not, we&rsquo;ll return a <code>401 Unauthorized</code> response.</p> <div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span> <span class="n">before_action</span> <span class="ss">:set_current_user</span><span class="p">,</span> <span class="ss">:authenticate_request</span> <span class="n">rescue_from</span> <span class="no">NotAuthenticatedError</span> <span class="k">do</span> <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s1">'Not Authorized'</span> <span class="p">},</span> <span class="ss">status: :unauthorized</span> <span class="k">end</span> <span class="n">rescue_from</span> <span class="no">AuthenticationTimeoutError</span> <span class="k">do</span> <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s1">'Auth token is expired'</span> <span class="p">},</span> <span class="ss">status: </span><span class="mi">419</span> <span class="c1"># unofficial timeout status code</span> <span class="k">end</span> <span class="kp">private</span> <span class="c1"># Based on the user_id inside the token payload, find the user.</span> <span class="k">def</span> <span class="nf">set_current_user</span> <span class="k">if</span> <span class="n">decoded_auth_token</span> <span class="vi">@current_user</span> <span class="o">||=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">decoded_auth_token</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">])</span> <span class="k">end</span> <span class="k">end</span> <span class="c1"># Check to make sure the current user was set and the token is not expired</span> <span class="k">def</span> <span class="nf">authenticate_request</span> <span class="k">if</span> <span class="n">auth_token_expired?</span> <span class="nb">fail</span> <span class="no">AuthenticationTimeoutError</span> <span class="k">elsif</span> <span class="o">!</span><span class="vi">@current_user</span> <span class="nb">fail</span> <span class="no">NotAuthenticatedError</span> <span class="k">end</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">decoded_auth_token</span> <span class="vi">@decoded_auth_token</span> <span class="o">||=</span> <span class="no">AuthToken</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">http_auth_header_content</span><span class="p">)</span> <span class="k">end</span> <span class="k">def</span> <span class="nf">auth_token_expired?</span> <span class="n">decoded_auth_token</span> <span class="o">&amp;&amp;</span> <span class="n">decoded_auth_token</span><span class="p">.</span><span class="nf">expired?</span> <span class="k">end</span> <span class="c1"># JWT's are stored in the Authorization header using this format:</span> <span class="c1"># Bearer somerandomstring.encoded-payload.anotherrandomstring</span> <span class="k">def</span> <span class="nf">http_auth_header_content</span> <span class="k">return</span> <span class="vi">@http_auth_header_content</span> <span class="k">if</span> <span class="k">defined?</span> <span class="vi">@http_auth_header_content</span> <span class="vi">@http_auth_header_content</span> <span class="o">=</span> <span class="k">begin</span> <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'Authorization'</span><span class="p">].</span><span class="nf">present?</span> <span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'Authorization'</span><span class="p">].</span><span class="nf">split</span><span class="p">(</span><span class="s1">' '</span><span class="p">).</span><span class="nf">last</span> <span class="k">else</span> <span class="kp">nil</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> <span class="k">end</span> </code></pre></div> <p>You&rsquo;ll also need to define the 2 errors that are being rescued from:</p> <div class="highlight"><pre class="highlight ruby"><code><span class="k">class</span> <span class="nc">NotAuthenticatedError</span> <span class="o">&lt;</span> <span class="no">StandardError</span> <span class="k">end</span> <span class="k">class</span> <span class="nc">AuthenticationTimeoutError</span> <span class="o">&lt;</span> <span class="no">StandardError</span> <span class="k">end</span> </code></pre></div> <h2 id="client-side-code">Client Side Code</h2> <p>That should take care of the server side. Next, we&rsquo;ll need to add support for these API&rsquo;s into our angular app. To do this, we&rsquo;ll need to implement two pieces of code: An AuthService that will handle logging in followed by an HTTP interceptor that will automatically attach our auth token to every http request and handle auth-related error responses.</p> <p>First, our AuthService. Note that there are two dependencies you&rsquo;ll need to implement. First, <code>AuthToken</code> is a simple service for storing the auth token in local storage while <code>AuthEvents</code> is a constant with a few auth/login related events so we&rsquo;re not using magic strings.</p> <div class="highlight"><pre class="highlight javascript"><code><span class="nx">app</span><span class="p">.</span><span class="nx">factory</span><span class="p">(</span><span class="dl">"</span><span class="s2">AuthService</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">$http</span><span class="p">,</span> <span class="nx">$q</span><span class="p">,</span> <span class="nx">$rootScope</span><span class="p">,</span> <span class="nx">AuthToken</span><span class="p">,</span> <span class="nx">AuthEvents</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="na">login</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">username</span><span class="p">,</span> <span class="nx">password</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">d</span> <span class="o">=</span> <span class="nx">$q</span><span class="p">.</span><span class="nx">defer</span><span class="p">();</span> <span class="nx">$http</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/auth</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">username</span><span class="p">:</span> <span class="nx">username</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="nx">password</span> <span class="p">}).</span><span class="nx">success</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">resp</span><span class="p">)</span> <span class="p">{</span> <span class="nx">AuthToken</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">resp</span><span class="p">.</span><span class="nx">auth_token</span><span class="p">);</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="nx">$broadcast</span><span class="p">(</span><span class="nx">AuthEvents</span><span class="p">.</span><span class="nx">loginSuccess</span><span class="p">);</span> <span class="nx">d</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="nx">resp</span><span class="p">.</span><span class="nx">user</span><span class="p">);</span> <span class="p">}).</span><span class="nx">error</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">resp</span><span class="p">)</span> <span class="p">{</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="nx">$broadcast</span><span class="p">(</span><span class="nx">AuthEvents</span><span class="p">.</span><span class="nx">loginFailed</span><span class="p">);</span> <span class="nx">d</span><span class="p">.</span><span class="nx">reject</span><span class="p">(</span><span class="nx">resp</span><span class="p">.</span><span class="nx">error</span><span class="p">);</span> <span class="p">});</span> <span class="k">return</span> <span class="nx">d</span><span class="p">.</span><span class="nx">promise</span><span class="p">;</span> <span class="p">}</span> <span class="p">};</span> <span class="p">});</span> </code></pre></div> <p>You&rsquo;ll need to implement a basic login form and controller that use this service.</p> <p>Next, let&rsquo;s add our two http interceptors. The first is quite simple. Just attach &ldquo;Bearer&rdquo; followed by the auth token. This is the standard format for adding a JWT to your http headers. The error interceptor is slightly more complicatated. First, we check to make sure this isn&rsquo;t our intial auth request because we want that to handle errors on its own. Then, we check to see if the response code matches any of our auth-related codes. If so, we broadcast an appropriate event.</p> <div class="highlight"><pre class="highlight javascript"><code><span class="nx">app</span><span class="p">.</span><span class="nx">factory</span><span class="p">(</span><span class="dl">"</span><span class="s2">AuthInterceptor</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">$q</span><span class="p">,</span> <span class="nx">$injector</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="c1">// This will be called on every outgoing http request</span> <span class="na">request</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">config</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">AuthToken</span> <span class="o">=</span> <span class="nx">$injector</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">AuthToken</span><span class="dl">"</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">AuthToken</span><span class="p">.</span><span class="kd">get</span><span class="p">();</span> <span class="nx">config</span><span class="p">.</span><span class="nx">headers</span> <span class="o">=</span> <span class="nx">config</span><span class="p">.</span><span class="nx">headers</span> <span class="o">||</span> <span class="p">{};</span> <span class="k">if</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span> <span class="nx">config</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="nx">Authorization</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Bearer </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">token</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">config</span> <span class="o">||</span> <span class="nx">$q</span><span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="nx">config</span><span class="p">);</span> <span class="p">},</span> <span class="c1">// This will be called on every incoming response that has en error status code</span> <span class="na">responseError</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">response</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">AuthEvents</span> <span class="o">=</span> <span class="nx">$injector</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">AuthEvents</span><span class="dl">'</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">matchesAuthenticatePath</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">config</span> <span class="o">&amp;&amp;</span> <span class="nx">response</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">url</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/auth</span><span class="dl">'</span><span class="p">));</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">matchesAuthenticatePath</span><span class="p">)</span> <span class="p">{</span> <span class="nx">$injector</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">$rootScope</span><span class="dl">'</span><span class="p">).</span><span class="nx">$broadcast</span><span class="p">({</span> <span class="mi">401</span><span class="p">:</span> <span class="nx">AuthEvents</span><span class="p">.</span><span class="nx">notAuthenticated</span><span class="p">,</span> <span class="mi">403</span><span class="p">:</span> <span class="nx">AuthEvents</span><span class="p">.</span><span class="nx">notAuthorized</span><span class="p">,</span> <span class="mi">419</span><span class="p">:</span> <span class="nx">AuthEvents</span><span class="p">.</span><span class="nx">sessionTimeout</span> <span class="p">}[</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">],</span> <span class="nx">response</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="nx">$q</span><span class="p">.</span><span class="nx">reject</span><span class="p">(</span><span class="nx">response</span><span class="p">);</span> <span class="p">}</span> <span class="p">};</span> <span class="p">});</span> <span class="nx">app</span><span class="p">.</span><span class="nx">config</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">$httpProvider</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">$httpProvider</span><span class="p">.</span><span class="nx">interceptors</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">"</span><span class="s2">AuthInterceptor</span><span class="dl">"</span><span class="p">);</span> <span class="p">});</span> <span class="c1">// Elsewhere....</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="nx">$on</span><span class="p">(</span><span class="nx">AuthEvents</span><span class="p">.</span><span class="nx">notAuthorized</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span> <span class="c1">// ... Take some action in response to a 401</span> <span class="p">});</span> </code></pre></div> <p>How you handle these auth error events will be up to you. The simplest solution is to just redirect the user to the login page. Or you may want to pop up a modal login form so that the user doesn&rsquo;t lose his or her work.</p> <p>Also, this naively assumes you&rsquo;ll have a long session length and the user won&rsquo;t mind logging in again at the end, even if they&rsquo;ve been actively using it the whole time. In my app, the session timeout length is just 60 minutes. So I implemented a timer that, every x minutes, requests to reissue the token (thus pushing back the expiration date) so long as there had been recent user activity. I may share this code in a future blog post, but I figured it was out of scope for the time being.</p> <p>I&rsquo;d love to hear your feedback because, again, this was roughly extracted from my application and I&rsquo;m not even sure it&rsquo;s the best implementation. So let me know on twitter, where I&rsquo;m <a href="http://twitter.com/adam_albrecht">@adam_albrecht</a>, if you find any bugs or ways to improve the code. Thanks!</p>