Using Grails REST for authentication in an AngularJS SPA

Introduction

I have played around with AngularJS for a few weeks now. I like how it provides for separation of concerns on the client-side that us server-side programmers prefer. I have built a couple of controllers that are retrieving data via REST from a Grails back-end domain. That is all well and good, but I want to protect access to the data, and most web applications have authentication so it is something good to figure out anyways.

I have made a few assumptions about Grails:

  • You are using Grails 2.3.x
  • You are using the asset-pipeline plugin
  • You are using Bower to manage AngularJS modules
  • You already have some AngularJS controllers defined which access data that you would like to protect

Configure Grails for authentication

For Grails, I am familiar with the Spring Security plugin, so I want to find a plugin along those lines. I found a nice plugin which provides “… authentication for REST APIs based on Spring Security …” using “… a token-based workflow.” Sounds exactly like what I need! It is a pretty easy plugin to set up.

  1. Install the plugin via a BuildConfig.groovy dependency
  2. Run grails s2-quickstart com.asoftwareguy.example.auth User Role to create the domain classes for authentication
  3. Create a class to hold the authentication tokens in the database:
    package com.asoftwareguy.example.auth
    
    class AuthenticationToken {
    
        String username
        String token
    }
    
    
  4. Add the proper configurations to Config.groovy:
    // Added by the Spring Security Core plugin:
    grails.plugin.springsecurity.userLookup.userDomainClassName = 'com.asoftwareguy.example.auth.User'
    grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'com.asoftwareguy.example.auth.UserRole'
    grails.plugin.springsecurity.authority.className = 'com.asoftwareguy.example.auth.Role'
    grails.plugin.springsecurity.securityConfigType = 'InterceptUrlMap'
    grails.plugin.springsecurity.interceptUrlMap = [
            '/':                    ['permitAll'],
            '/assets/**':           ['permitAll'],
            '/partials/**':         ['permitAll'],
            '/**':                  ['isFullyAuthenticated()']
    ]
    
    grails.plugin.springsecurity.rememberMe.persistent = false
    grails.plugin.springsecurity.rest.login.useJsonCredentials = true
    grails.plugin.springsecurity.rest.login.failureStatusCode = 401
    grails.plugin.springsecurity.rest.token.storage.useGorm = true
    grails.plugin.springsecurity.rest.token.storage.gorm.tokenDomainClassName = 'com.asoftwareguy.example.auth.AuthenticationToken'
    grails.plugin.springsecurity.rest.token.storage.gorm.tokenValuePropertyName = 'token'
    grails.plugin.springsecurity.rest.token.storage.gorm.usernamePropertyName = 'username'
    

    Notice that access to /partials and /assets is fully permitted; all other access requires authentication. Access to /api is allowed via the plugin itself.
    Also note the entry for grails.plugin.springsecurity.rest.login.failureStatusCode = 401. This is necessary because the AngularJS module uses HTTP status code 403 to verify that authentication is required to access a resource, which is different from an authentication error when attempting to authenticate. We override in Grails to use HTTP status code 401 to indicate that.

  5. Add a user entry or two into Bootstrap.groovy, so you have some credentials set up.
    class BootStrap {
    
        def init = { servletContext ->
    
            User user = new User(username: "test", password: "test123")
            user.save()
    
            Role roleUser = new Role(authority: "ROLE_USER")
            roleUser.save()
    
            new UserRole(user: user, role: roleUser).save()
        }
        def destroy = {
        }
    }
    
  6. Run a few tests against the server to check that when provided a JSON object with username and password to /api/login, the server returns a token that is used to later retrieve data.

Configure AngularJS for authentication

Now that I have a back-end server accepting REST authentication requests and responding with a token I can use for later API calls, I need to set up my AngularJS application to use authentication. I settled on using the HTTP Auth Interceptor Module that is an AngularJS module.

    1. Change directory to grails-app/assets
    2. Run bower install angular-http-auth
    3. Add the require directive to your application manifest:
      //= require angular-http-auth/src/http-auth-interceptor
      
    4. Add the module declaration to your application:
      var exampleApp = angular.module('exampleApp', [
          'http-auth-interceptor',
          'ngRoute',
          'ui.bootstrap',
          'login',
          // others
      ]);
      
    5. Add a directive to your application to show the login form when required:
      exampleApp.directive('showLogin', function() {
          return {
              restrict: 'C',
              link: function(scope, element, attrs) {
                  var login = element.find('#login-holder');
                  var loginError = element.find('#login-error');
                  var main = element.find('#content');
                  var username = element.find('#username');
                  var password = element.find('#password');
      
                  login.hide();
                  loginError.hide();
      
                  scope.$on('event:auth-loginRequired', function() {
                      console.log('showing login form');
                      main.hide();
                      username.val('');
                      password.val('');
                      login.show();
                  });
                  scope.$on('event:auth-loginFailed', function() {
                      console.log('showing login error message');
                      username.val('');
                      password.val('');
                      loginError.show();
                  });
                  scope.$on('event:auth-loginConfirmed', function() {
                      console.log('hiding login form');
                      main.show();
                      login.hide();
                      username.val('');
                      password.val('');
                  });
              }
          }
      });
      function getLocalToken() {
         return localStorage["authToken"];
      }
      
      function getHttpConfig() {
          return {
              headers: {
                  'X-Auth-Token': getLocalToken()
              }
          };
      }
      
      function getAuthenticateHttpConfig() {
          return {
              ignoreAuthModule: true
          };
      }
      

      Notice a couple of other functions defined here as well. These are used later by the login controller(s) to set and retrieve the authentication request header/token combination. Here is the example HTML markup:

      <!DOCTYPE html>
      <html ng-app="exampleApp" lang="en">
      <!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
      <!--[if IE 7 ]>    <html lang="en" class="no-js ie7"> <![endif]-->
      <!--[if IE 8 ]>    <html lang="en" class="no-js ie8"> <![endif]-->
      <!--[if IE 9 ]>    <html lang="en" class="no-js ie9"> <![endif]-->
      <!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
      	<head>
      		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
      		<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
      		<title><g:layoutTitle default="Example App"/></title>
              <meta name="viewport" content="width=device-width, initial-scale=1">
              <asset:link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/>
              <asset:stylesheet href="font-awesome/css/font-awesome.css" />
              <asset:stylesheet href="bootstrap-css/css/bootstrap.css" />
              <asset:javascript src="application.js"/>
      		<g:layoutHead/>
      	</head>
      	<body class="show-login">
              <div class="page-header container">
                  <div class="row">
                      <div class="span6">
                          Welcome to Example App!
                      </div>
                      <div class="span6" style="text-align: right;" ng-controller="logoutController">
                          Welcome, username.
                          <a href="" ng-click="logOut()">(Log out)</a>
                      </div>
                  </div>
                  <div class="row" style="text-align: right;">
      
                  </div>
              </div>
      
              <div id="login-holder" class="container" style="width: 300px;">
                  <div id="login-error" class="alert alert-error">
                      <button type="button" class="close" onclick="$('#login-error').hide();">&times;</button>
                      Username and/or password incorrect.
                  </div>
                  <div id="loginbox">
                      <div id="login-inner" ng-controller="loginController">
                          <form name="loginForm" role="form" ng-submit="logIn()" autocomplete="off">
                              <div class="form-group">
                                  <label for="username">Username</label>
                                  <input id="username" class="form-control" type="text" ng-model="authData.username"/>
                              </div>
                              <div class="form-group">
                                  <label for="password">Password</label>
                                  <input id="password" class="form-control" type="password" ng-model="authData.password"/>
                              </div>
                              <input type="submit" class="btn btn-primary" value="Login"/>
                          </form>
                      </div>
                      <div class="clear"></div>
                  </div>
              </div>
              <div id="content" class="container">
      		    <g:layoutBody/>
              </div>
      	</body>
      </html>
      
    6. Define your login controller(s):
      var login = angular.module('login', []);
      
      login.controller('loginController',
          function ($rootScope, $scope, $http, authService) {
              console.log('loginController called');
      
              $scope.logIn = function() {
                  console.log('logIn called')
      
                  $http.post('api/login', { username: $scope.authData.username, password: $scope.authData.password }, getAuthenticateHttpConfig).
                      success(function(data) {
                          console.log('authentication token: ' + data.token);
                          localStorage["authToken"] = data.token;
                          authService.loginConfirmed({}, function(config) {
                              if(!config.headers["X-Auth-Token"]) {
                                  console.log('X-Auth-Token not on original request; adding it');
                                  config.headers["X-Auth-Token"] = getLocalToken();
                              }
                              return config;
                          });
                      }).
                      error(function(data) {
                          console.log('login error: ' + data);
                          $rootScope.$broadcast('event:auth-loginFailed', data);
                      });
              }
          }
      );
      
      login.controller('logoutController',
          function ($scope, $http, $location) {
              console.log('logoutController called');
      
              $scope.logOut = function() {
                  console.log('logOut called');
      
                  $http.post('api/logout', {}, getHttpConfig()).
                      success(function() {
                          console.log('logout success');
                          localStorage.clear();
                          $location.path("/")
                      }).
                      error(function(data) {
                          console.log('logout error: ' + data);
                      });
              }
          }
      );
      
      console.log('login controllers load complete');
      

Fire up the application and try to navigate to one of your existing AngularJS controllers which requests protected data from the back-end server. If you have not already authenticated, the authentication module broadcasts a login required event, the directive we defined traps that event and overlays the screen with the login form. The login form stays active until you successfully authenticate, at which point the authentication module broadcasts a login confirmed event, and our directive traps that event and hides the login form, and the original data request is replayed with the authentication token as a request header.

Conclusion

The combination of AngularJS for a client with Grails REST APIs on the server is a powerful one with a lot of promise. Hopefully this will help you out if you are trying your hand at building an application along these lines. I have provided an example application using this approach on GitHub.

Advertisements
This entry was posted in AngularJS, Grails and tagged , , , , , . Bookmark the permalink.

37 Responses to Using Grails REST for authentication in an AngularJS SPA

  1. vasya10 says:

    Pretty neat! Interesting you used restrict: C, than an attribute (A). Probably C helps with older IE’s. Also line #26, i think you meant {{username}} ?

  2. asoftwareguy says:

    @vasya10 yeah, that part of the example isn’t fully implemented. Obviously, you would show/hide that div, and instead show a Log In link if not authenticated.

  3. Jim Johnson says:

    Loved the article. I have been looking for a good client-side example for the spring-security-rest plugin and with AngularJS is a bonus. It seems that your SPA is web focused. I’m working on an SPA/AngularJS/PhoneGap implementation. Need to think about the use of GSP in a mobile implementation. Thoughts?

  4. androidian says:

    Very interesting project. I am about to start doing an AngularJS based SPA running inside Cordova with the REST interface running on Grails.

    I ran your github project in IntelliJ. Instead of letting the user to login i am getting the events page. What could be wrong?

    • asoftwareguy says:

      @androidian did you try running through the application with a Javascript console such as Firebug open? There is some logging in the JS that may help you out.

      • androidian says:

        i had it in my browser – does it help?

        ravitas app load complete app.js?compile=false:110
        home controllers load complete home.js?compile=false:21
        login controllers load complete login.js?compile=false:55
        events controllers load complete events.js?compile=false:139
        gravitas manifest load complete. application.js?compile=false:13
        logoutController called login.js?compile=false:35
        loginController called login.js?compile=false:5
        listEventsController called events.js?compile=false:33
        loadEvents called events.js?compile=false:17
        GET http://localhost:8080/gravitas/data/events/ 404 (Not Found) angular.js?compile=false:8113
        loadEvents error: events.js?compile=false:25

      • asoftwareguy says:

        @androidian did you have the Grails app up and running?

  5. acveer says:

    Highly informative E. Its very cool that we can write an entire web application in AngularJS. The first thing that came to my mind is authentication as I learned about it last week. I will try to run your app from github, although NOT sure about Grails setup yet.

  6. bpavie says:

    Very interesting post, thanks a lot!

    I checked out the project from Github and if it works very well using Chrome, it do not using Firefox or Internet Explorer.

    Here are the logs for Firefox v28:
    Password fields present on an insecure (http://) page. This is a security risk that allows user login credentials to be stolen.[Learn More]
    reflow: 0.27ms
    “listEventsController called” events.js:33
    “loadEvents called” events.js:17
    GET http://localhost:8080/gravitas/data/events/ [HTTP/1.1 401 Unauthorized 158ms]
    no element found events:1
    “loadEvents error: ” events.js:25

    Here, you never see the login page

    For IE v8, I can see the login page but login will always fail.

    So right now, it is working only on Chrome (at least on Windows 7)

  7. Pingback: AngularJS login to Grails portal | Software notes

  8. I’m having a redirect loop. Any ideas?

    Thanks. :)

  9. Igor says:

    The example doesn’t work, I am getting an 500 below. Any ideas?

    http://localhost:8080/gravitas

    java.lang.IllegalArgumentException: ‘mediaType’ must not be empty
    org.springframework.util.Assert.hasLength(Assert.java:136)
    org.springframework.http.MediaType.parseMediaType(MediaType.java:687)
    org.springframework.http.MediaType$parseMediaType.call(Unknown Source)

  10. genuinefafa says:

    I’m trying to get the example working, but it throws an exception
    Message: ‘mediaType’ must not be empty

    Any ideas? I cannot even load the index page.

    • asoftwareguy says:

      My apologies. My last commit broke the application. I have reverted out those changes. Please update the sources and try again.

      • genuinefafa says:

        There is no need for apology. ;) You have 2 PR now. I think they will suit you just fine.
        ps: the project was still unstable due to changes in the -rest plugin.

  11. Ming says:

    Hello mate, your post is beneficial to me as I’m making the same attempt.

    Have you read: http://stackoverflow.com/a/12293017/2810746

    My biggest concern is performance issue.

    • Ming says:

      Just out of interest. Have you put this in a real project? How’s it going? Or did you do it just as a proof of concept?

      Thanks

      • asoftwareguy says:

        Gravitas in particular is just an example for the idea, and something with which to play around. At my company, we have one product in production that is using AngularJS client/Grails server, but it is an internal tool we use to manage configuration of some of our other production applications, so performance/latency issues as not as critical as it would be for an external end-user facing application.

  12. Mike Wes says:

    I got also problems with ‘GET http://localhost:8080/gravitas/data/events/ ‘ which resulted in a 401 and/or 403 message.
    The solution is to use expliciet grails version 2.3.11. Grails version 2.3.0 and 2.3.8 will fail even as using grails 2.4.3.

  13. edje95 says:

    I got also problems with ‘GET http://localhost:8080/gravitas/data/events/ ‘ which resulted in a 401 and/or 403 message.
    The solution is to use expliciet grails version 2.3.11. Grails version 2.3.0 and 2.3.8 will fail even as using grails 2.4.3.

  14. suryazi says:

    Excellent post for Grails+AngularJS+Spring Security. Though the application is running properly but there are problems in authentication the user:

    1. If wrong credentials are provided the login screen will reappear again and again even though right credentials are submitted later.

    2. If you logout and then you are able to login just by providing the username without password.

    3. You have to click Delete twice to remove an event.

    Are these problems pertain to plugin or the application itself.

    • asoftwareguy says:

      Some of these are known issues with bugs opened against the GitHub repo. Some of these are issues of which I was unaware. Feel free to add issues to GitHub and if you are so inclined, fix any with a PR :)

  15. Srt says:

    Very nice tutorial.. I cloned it from github and its working fine. And let me ask one doubt.. When i tried to send a post request using a REST Client to http://8080/gravitas/api/login with username and password as the request params i’m getting HTTP/1.1 400 Bad Request. What is the reason?

    • asoftwareguy says:

      Are you using query parameters or the body of the request? The API is expecting JSON containing the username/password.

      • srt says:

        I tried using POSTMAN Rest client , by passing a json file that contains usename (test@test.com) and password (test123) . It gives me 401 unauthorized error.
        Then i tried using passing the credentials via jquery ajax request . here is my code

        var arr = { username: ‘test@test.com’, password: ‘test123’};
        $.ajax({
        url: ‘http://localhost:8080/gravitas/api/login/’,
        type: ‘POST’,
        data: JSON.stringify(arr),
        contentType: ‘application/json; charset=utf-8’,
        dataType: ‘json’,
        async: false,
        success: function(msg) {
        alert(msg);
        }
        });
        it also gives 401 error in the javascript console.
        Could you please show me how to authenticate it using Jquery?

  16. asoftwareguy says:

    srt, using the POSTMAN client in Chrome, I was able to get a response from the server. See https://cloud.githubusercontent.com/assets/866865/6002003/ffa62fb2-aab4-11e4-963f-a0cca3d47384.png

  17. flotsch says:

    Hi, thanks for the great article. I have one question:

    Why is it necessary to set the config.headers in loginConfirmed?

    authService.loginConfirmed({}, function(config) {
    if(!config.headers[“X-Auth-Token”]) {
    console.log(‘X-Auth-Token not on original request; adding it’);
    config.headers[“X-Auth-Token”] = getLocalToken();
    }
    return config;
    });

    The token is already set with:

    localStorage[“authToken”] = data.token;

    and if you need the token it will be read with getHttpConfig()

    $http.get(‘/gravitas/data/events?max=99999’, getHttpConfig()).

    Thanks

    • asoftwareguy says:

      There is probably a better way to do this. What is happening is, for example, a user had previously logged in and was using the site, but then stepped away. Their authentication token in localStorage is still set. When they come back, say after 30 minutes, their session will have expired on the server, and they will be authenticated again and given a new token. This code just allows an easy way to update localStorage with the new token.

  18. Hi!, nice example. I have upgraded it to 2.4.4 and it works just fine. I’m trying to reach localhost:8080/gravitas/data/events/1 from POSTAM, I set the authorization header to Bearer *token* but I still get a 401. Do you know what I might be missing here? Thanks in advance!

  19. Miguel Curi says:

    I am new to angular but have used grails before. I am trying to make this work with Grails 3.0. I can’t find application.js or app.js in grails-app/assets/javascripts, should I create these files or are these file names something different depending on the name of your app?

    • asoftwareguy says:

      Are you using the new AngularJS profile to create your Grails 3.x app? If so, it may have a different pattern for naming the application.js file, I can’t remember. Since Gravitas is a Grails 2.x app, I just created the file and then reference it in grails-app/views/layouts/main.gsp

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s