Authorization with Angular.js and UI-Router

<p>While working on an angular.js application recently, I found myself needing some form of authorization logic (not to be confused with authentication / login). I needed to restrict content in my app based on a user&rsquo;s role as well as some other factors. At first, I created a single <code>AuthService</code> service that dealt with login, authorization, and session management. But this felt messy and violated the <a href="http://en.wikipedia.org/wiki/Single_responsibility_principle">Single Responsibility Principle</a>, so I decided to make something cleaner. My goal was for the API to look something like this:</p> <p></p> <p>(Warning: lots of coffeescript ahead!)</p> <div class="highlight"><pre class="highlight coffeescript"><code><span class="nx">LoginService</span><span class="p">.</span><span class="na">login</span><span class="p">(</span><span class="nx">email</span><span class="p">,</span> <span class="nx">password</span><span class="p">).</span><span class="na">then</span><span class="p">((</span><span class="nx">u</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nx">Session</span><span class="p">.</span><span class="na">setCurrentUser</span><span class="p">(</span><span class="nx">u</span><span class="p">)</span> <span class="p">)</span> <span class="c1"># ... Elsewhere ....</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">Session</span><span class="p">.</span><span class="na">getCurrentUser</span><span class="p">()</span> <span class="nx">authorizer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Authorizer</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="nx">authorizer</span><span class="p">.</span><span class="na">canAccess</span><span class="p">(</span><span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">viewAdminSettings</span><span class="p">)</span> <span class="c1"># returns a boolean</span> </code></pre></div> <p>By doing it this way, I was fairly sure I could split my formerly monolithic <code>AuthService</code> into 3 separate services that had no dependencies on one another. I won&rsquo;t go too detailed into the login and session services because they are fairly straight forward. <code>LoginService</code> has one method that simply makes an HTTP request with a username and password and, if successful, returns the user object. <code>Session</code> is a singleton service that, given a user, can create or destroy the current session. But my solution to Authorization was fairly interesting, so I thought I&rsquo;d share.</p> <p></p> <p>In the ruby world, I&rsquo;ve used both <a href="https://github.com/CanCanCommunity/cancancan">CanCan</a> and <a href="https://github.com/elabs/pundit">Pundit</a> and so I drew a lot of inspiration from them. But at the same time, I was mindful that client-side authorization is never as complex as server side. You should never need to use client side authorization to filter data (that should be done server side), but only to show/hide pages and pieces of content.</p> <p>So first, I created a constant containing a set of permissions:</p> <div class="highlight"><pre class="highlight coffeescript"><code><span class="nx">app</span><span class="p">.</span><span class="na">constant</span><span class="p">(</span><span class="s">'APP_PERMISSIONS'</span><span class="p">,</span> <span class="p">{</span> <span class="na">viewAdminSettings</span><span class="o">:</span> <span class="s">"viewAdminSettings"</span> <span class="na">editAdminSettings</span><span class="o">:</span> <span class="s">"editAdminSettings"</span> <span class="na">viewLibrary</span><span class="o">:</span> <span class="s">"viewLibrary"</span> <span class="na">editLibrary</span><span class="o">:</span> <span class="s">"editLibrary"</span> <span class="na">viewBusinessAssociates</span><span class="o">:</span> <span class="s">"viewBusinessAssociates"</span> <span class="na">editBusinessAssociates</span><span class="o">:</span> <span class="s">"editBusinessAssociates"</span> <span class="c1"># ...</span> <span class="p">})</span> </code></pre></div> <p>You&rsquo;ll notice that the keys and values are the same. I only made this an object rather than an array so I could refer to them with a dot syntax and wouldn&rsquo;t have magic strings floating around.</p> <p>Next, as a good programmer does, I added a test for the <code>Authorizer</code> service I was about to make.</p> <div class="highlight"><pre class="highlight coffeescript"><code><span class="nx">describe</span> <span class="s">"Authorizer"</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="nx">Authorizer</span> <span class="o">=</span> <span class="no">null</span> <span class="nx">APP_PERMISSIONS</span> <span class="o">=</span> <span class="no">null</span> <span class="nx">beforeEach</span><span class="p">(</span><span class="nx">angular</span><span class="p">.</span><span class="na">mock</span><span class="p">.</span><span class="na">module</span><span class="p">(</span><span class="s">'privacypro.auth'</span><span class="p">))</span> <span class="nx">beforeEach</span><span class="p">(</span><span class="nx">inject</span><span class="p">((</span><span class="nx">_Authorizer_</span><span class="p">,</span> <span class="nx">_APP_PERMISSIONS_</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nx">Authorizer</span> <span class="o">=</span> <span class="nx">_Authorizer_</span> <span class="nx">APP_PERMISSIONS</span> <span class="o">=</span> <span class="nx">_APP_PERMISSIONS_</span> <span class="k">return</span> <span class="c1"># always add return statements to injection blocks in Coffeescript.</span> <span class="p">))</span> <span class="nx">describe</span> <span class="s">"canAccess()"</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="nx">authorizer</span> <span class="o">=</span> <span class="no">null</span> <span class="nx">describe</span> <span class="s">"An admin user"</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">{</span> <span class="na">role</span><span class="o">:</span> <span class="s">"admin"</span> <span class="p">}</span> <span class="nx">beforeEach</span> <span class="o">-&gt;</span> <span class="nx">authorizer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Authorizer</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="nx">it</span> <span class="s">"can view the admin settings"</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">authorizer</span><span class="p">.</span><span class="na">canAccess</span><span class="p">(</span><span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">viewAdminSettings</span><span class="p">)).</span><span class="na">toBeTruthy</span><span class="p">()</span> <span class="nx">describe</span> <span class="s">"A normal user"</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="nx">user</span> <span class="o">=</span> <span class="p">{</span> <span class="na">role</span><span class="o">:</span> <span class="s">"normal"</span> <span class="p">}</span> <span class="nx">beforeEach</span> <span class="o">-&gt;</span> <span class="nx">authorizer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Authorizer</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="nx">it</span> <span class="s">"cannot view the admin settings"</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">authorizer</span><span class="p">.</span><span class="na">canAccess</span><span class="p">(</span><span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">viewAdminSettings</span><span class="p">)).</span><span class="na">toBeFalsy</span><span class="p">()</span> <span class="nx">it</span> <span class="s">"can view the library OR view the admin settings"</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="nx">expect</span><span class="p">(</span><span class="nx">authorizer</span><span class="p">.</span><span class="na">canAccess</span><span class="p">([</span><span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">viewLibrary</span><span class="p">,</span> <span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">viewAdminSettings</span><span class="p">])).</span><span class="na">toBeTruthy</span><span class="p">()</span> <span class="nx">it</span> <span class="s">"throws an error if passed a bad permission"</span><span class="p">,</span> <span class="o">-&gt;</span> <span class="nx">expect</span><span class="p">(</span><span class="o">-&gt;</span> <span class="nx">authorizer</span><span class="p">.</span><span class="na">canAccess</span><span class="p">(</span><span class="s">"foobar"</span><span class="p">)).</span><span class="na">toThrow</span><span class="p">()</span> </code></pre></div> <p>My specs included a number of examples and more complex scenarios, but you get the point. So next, I started work on my <code>Authorizer</code> service. This class can be as simple or complex as your authorization requirements demand. In reality, my code is quite a bit more complex than below, but you&rsquo;ll understand the basics from this example:</p> <div class="highlight"><pre class="highlight coffeescript"><code><span class="nx">app</span><span class="p">.</span><span class="na">service</span><span class="p">(</span><span class="s">"Authorizer"</span><span class="p">,</span> <span class="p">(</span><span class="nx">APP_PERMISSIONS</span><span class="p">,</span> <span class="nx">USER_ROLES</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="k">return</span> <span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="na">canAccess</span><span class="o">:</span> <span class="p">(</span><span class="nx">permissions</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nx">permissions</span> <span class="o">=</span> <span class="p">[</span><span class="nx">permissions</span><span class="p">]</span> <span class="nx">unless</span> <span class="nx">angular</span><span class="p">.</span><span class="na">isArray</span><span class="p">(</span><span class="nx">permissions</span><span class="p">)</span> <span class="k">for</span> <span class="nx">permission</span> <span class="o">in</span> <span class="nx">permissions</span> <span class="k">if</span> <span class="o">!</span><span class="nx">APP_PERMISSIONS</span><span class="p">[</span><span class="nx">permission</span><span class="p">]</span><span class="o">?</span> <span class="k">throw</span> <span class="s">"Bad permission value"</span> <span class="k">if</span> <span class="nx">user</span> <span class="o">&amp;&amp;</span> <span class="nx">user</span><span class="p">.</span><span class="na">role</span> <span class="k">switch</span> <span class="nx">permission</span> <span class="k">when</span> <span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">viewAdminSettings</span><span class="p">,</span> <span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">editAdminSettings</span> <span class="k">return</span> <span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="na">role</span> <span class="o">==</span> <span class="nx">USER_ROLES</span><span class="p">.</span><span class="na">admin</span><span class="p">)</span> <span class="k">when</span> <span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">editLibrary</span> <span class="k">return</span> <span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="na">role</span> <span class="o">==</span> <span class="nx">USER_ROLES</span><span class="p">.</span><span class="na">admin</span> <span class="o">||</span> <span class="nx">user</span><span class="p">.</span><span class="na">role</span> <span class="o">==</span> <span class="nx">USER_ROLES</span><span class="p">.</span><span class="na">normal</span><span class="p">)</span> <span class="c1"># etc...</span> <span class="k">else</span> <span class="k">return</span> <span class="no">false</span> <span class="no">false</span> <span class="p">}</span> <span class="p">)</span> </code></pre></div> <p>Now I&rsquo;m ready to add permissions to some of my routes. I always use <a href="https://github.com/angular-ui/ui-router">UI-Router</a>, but something similar can be done with <a href="https://docs.angularjs.org/api/ngRoute/service/$route">ngRoute</a>.</p> <div class="highlight"><pre class="highlight coffeescript"><code><span class="nx">$stateProvider</span> <span class="p">.</span><span class="na">state</span><span class="p">(</span><span class="s">"library"</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="o">:</span> <span class="s">"/library"</span><span class="p">,</span> <span class="na">templateUrl</span><span class="o">:</span> <span class="s">"..."</span> <span class="na">data</span><span class="o">:</span> <span class="p">{</span> <span class="na">permissions</span><span class="o">:</span> <span class="p">[</span><span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">viewLibrary</span><span class="p">]</span> <span class="p">}</span> <span class="p">})</span> <span class="p">.</span><span class="na">state</span><span class="p">(</span><span class="s">"admin"</span><span class="p">,</span> <span class="p">{</span> <span class="na">url</span><span class="o">:</span> <span class="s">"/admin"</span><span class="p">,</span> <span class="na">templateUrl</span><span class="o">:</span> <span class="s">"..."</span><span class="p">,</span> <span class="na">data</span><span class="o">:</span> <span class="p">{</span> <span class="na">permissions</span><span class="o">:</span> <span class="p">[</span><span class="nx">APP_PERMISSIONS</span><span class="p">.</span><span class="na">editAdminSettings</span><span class="p">]</span> <span class="p">}</span> <span class="p">})</span> </code></pre></div> <p>But how do I use this permission data and prevent a state from loading? To do this, you simply have to subscribe to ui-router&rsquo;s <code>$stateChangeStart</code> event and prevent it from propegating when necessary. Put this code inside of an angular run block.</p> <div class="highlight"><pre class="highlight coffeescript"><code><span class="nx">app</span><span class="p">.</span><span class="na">run</span><span class="p">((</span><span class="nx">$rootScope</span><span class="p">,</span> <span class="nx">Session</span><span class="p">,</span> <span class="nx">Authorizer</span><span class="p">,</span> <span class="nx">AUTH_EVENTS</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="na">$on</span> <span class="s">"$stateChangeStart"</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nx">permissions</span> <span class="o">=</span> <span class="k">if</span> <span class="nx">next</span> <span class="o">&amp;&amp;</span> <span class="nx">next</span><span class="p">.</span><span class="na">data</span> <span class="k">then</span> <span class="nx">next</span><span class="p">.</span><span class="na">data</span><span class="p">.</span><span class="na">permissions</span> <span class="k">else</span> <span class="no">null</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">Session</span><span class="p">.</span><span class="na">getCurrentUser</span><span class="p">()</span> <span class="nx">authenticator</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Authorizer</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="k">if</span> <span class="nx">permissions</span><span class="o">?</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">authenticator</span><span class="p">.</span><span class="na">canAccess</span><span class="p">(</span><span class="nx">permissions</span><span class="p">)</span> <span class="nx">event</span><span class="p">.</span><span class="na">preventDefault</span><span class="p">()</span> <span class="k">if</span> <span class="o">!</span><span class="nx">user</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="na">$broadcast</span> <span class="nx">AUTH_EVENTS</span><span class="p">.</span><span class="na">notAuthenticated</span> <span class="k">else</span> <span class="nx">$rootScope</span><span class="p">.</span><span class="na">$broadcast</span> <span class="nx">AUTH_EVENTS</span><span class="p">.</span><span class="na">notAuthorized</span> </code></pre></div> <p>Notes:</p> <ul> <li><code>AUTH_EVENTS</code> is another constant where I store various auth-related events.</li> <li>I did test this logic in my e2e tests via Protractor, but I don&rsquo;t want to make this post too extremely long.</li> </ul> <p>Finally, I needed to restrict individual pieces of content on the page. One easy way to do this would be to create a helper method on the scope and use it in tandem with ngIf like so:</p> <div class="highlight"><pre class="highlight html"><code><span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">''</span> <span class="na">ng-if=</span><span class="s">"canAccess('editAdminSettings')"</span><span class="nt">&gt;</span>Edit Admin Settings<span class="nt">&lt;/a&gt;</span> </code></pre></div> <p>But this is a bit messy and I hate to pollute the scope unless absolutely necessary. So instead, I created a custom directive that will work similarly:</p> <div class="highlight"><pre class="highlight html"><code><span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">''</span> <span class="na">ng-if-permission=</span><span class="s">"editAdminSettings"</span><span class="nt">&gt;</span>Edit Admin Settings<span class="nt">&lt;/a&gt;</span> </code></pre></div> <p>I was hoping to simply extend the <code>ngIf</code> directive with a specific set of logic, but I couldn&rsquo;t find a way to do this (if you know how to do this, please let me know!). So instead, I simply copied <a href="https://github.com/angular/angular.js/blob/master/src/ng/directive/ngIf.js">the ngIf source code</a>, changed the name to <code>ngIfPermission</code>, and made a few minor enhancements to the link function:</p> <div class="highlight"><pre class="highlight coffeescript"><code><span class="c1"># the beginning is the same besides the name</span> <span class="na">link</span><span class="o">:</span> <span class="p">(</span><span class="nx">$scope</span><span class="p">,</span> <span class="nx">$element</span><span class="p">,</span> <span class="nx">$attr</span><span class="p">,</span> <span class="nx">ctrl</span><span class="p">,</span> <span class="nx">$transclude</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nx">block</span> <span class="o">=</span> <span class="no">undefined</span> <span class="nx">childScope</span> <span class="o">=</span> <span class="no">undefined</span> <span class="nx">previousElements</span> <span class="o">=</span> <span class="no">undefined</span> <span class="c1"># There is no logic in the watch, so we can use $attr.$observe instead</span> <span class="nx">$attr</span><span class="p">.</span><span class="na">$observe</span> <span class="s">"ngIfPermission"</span><span class="p">,</span> <span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="c1"># Check if we can access the permission(s)</span> <span class="nx">permissions</span> <span class="o">=</span> <span class="nx">value</span><span class="p">.</span><span class="na">split</span><span class="p">(</span><span class="s">","</span><span class="p">)</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">Session</span><span class="p">.</span><span class="na">getCurrentUser</span><span class="p">()</span> <span class="nx">authenticator</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Authorizer</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="k">if</span> <span class="nx">authenticator</span><span class="p">.</span><span class="na">canAccess</span><span class="p">(</span><span class="nx">permissions</span><span class="p">)</span> <span class="nx">unless</span> <span class="nx">childScope</span> <span class="nx">$transclude</span> <span class="p">(</span><span class="nx">clone</span><span class="p">,</span> <span class="nx">newScope</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nx">childScope</span> <span class="o">=</span> <span class="nx">newScope</span> <span class="c1"># change the contents of the placeholder comment</span> <span class="nx">clone</span><span class="p">[</span><span class="nx">clone</span><span class="p">.</span><span class="na">length</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="na">createComment</span><span class="p">(</span><span class="s">" end ngIfPermission: "</span> <span class="o">+</span> <span class="nx">$attr</span><span class="p">.</span><span class="na">ngIfPermission</span> <span class="o">+</span> <span class="s">" "</span><span class="p">)</span> <span class="c1"># the rest is the same</span> <span class="p">)</span> </code></pre></div> <p>So now we have a way to restrict access to pages as well as individual pieces of content. Remember, client side authorization is no substitute for proper server-side authorization. Anybody who is half-way decent with the Chrome dev tools can figure out how to manipulate your API requests.</p> <p>I hope this helps you in your journey to create a complex and full-featured angular app. If you have any questions or have a suggestion on how to improve the code, let me know on twitter, where I&rsquo;m <a href="http://twitter.com/adam_albrecht">@adam_albrecht</a>. Thanks!</p>