(Quick Reference)

8 Authentication - Reference Documentation

Authors: Burt Beckwith, Beverley Talbott

Version: 2.0.0

8 Authentication

The Spring Security plugin supports several approaches to authentication.

The default approach stores users and roles in your database, and uses an HTML login form which prompts the user for a username and password. The plugin also supports other approaches as described in the sections below, as well as add-on plugins that provide external authentication providers such as LDAP and single sign-on using CAS

8.1 Basic and Digest Authentication

To use HTTP Basic Authentication in your application, set the useBasicAuth attribute to true. Also change the basic.realmName default value to one that suits your application, for example:

grails.plugin.springsecurity.useBasicAuth = true
grails.plugin.springsecurity.basic.realmName = "Ralph's Bait and Tackle"

PropertyDefaultDescription
useBasicAuthfalseWhether to use basic authentication.
basic.realmName'Grails Realm'Realm name displayed in the browser authentication popup.
basic. credentialsCharset'UTF-8'The character set used to decode Base64-encoded data

With this authentication in place, users are prompted with the standard browser login dialog instead of being redirected to a login page.

If you don't want all of your URLs guarded by Basic Auth, you can partition the URL patterns and apply Basic Auth to some, but regular form login to others. For example, if you have a web service that uses Basic Auth for /webservice/** URLs, you would configure that using the chainMap config attribute:

grails.plugin.springsecurity.filterChain.chainMap = [
   '/webservice/**': 'JOINED_FILTERS,-exceptionTranslationFilter',
   '/**': 'JOINED_FILTERS,-basicAuthenticationFilter,-basicExceptionTranslationFilter'
]

In this example we're using the JOINED_FILTERS keyword instead of explicitly listing the filter names. Specifying JOINED_FILTERS means to use all of the filters that were configured using the various config options. In each case we also specify that we want to exclude one or more filters by prefixing their names with -.

For the /webservice/** URLs, we want all filters except for the standard ExceptionTranslationFilter since we want to use just the one configured for Basic Auth. And for the /** URLs (everything else) we want everything except for the Basic Auth filter and its configured ExceptionTranslationFilter.

Digest Authentication is similar to Basic but is more secure because it does not send your password in obfuscated cleartext. Digest resembles Basic in practice - you get the same browser popup dialog when you authenticate. But because the credential transfer is genuinely hashed (instead of just Base64-encoded as with Basic authentication) you do not need SSL to guard your logins.

PropertyDefault ValueMeaning
useDigestAuthfalseWhether to use Digest authentication.
digest.realmName'Grails Realm'Realm name displayed in the browser popup
digest.key'changeme'Key used to build the nonce for authentication; it should be changed but that's not required.
digest. nonceValiditySeconds300How long a nonce stays valid.
digest. passwordAlreadyEncodedfalseWhether you are managing the password hashing yourself.
digest. createAuthenticatedTokenfalseIf true, creates an authenticated UsernamePasswordAuthenticationToken to avoid loading the user from the database twice. However, this process skips the isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired(), isEnabled() checks, so it is not advised.
digest. useCleartextPasswordsfalseIf true, a cleartext password encoder is used (not recommended). If false, passwords hashed by DigestAuthPasswordEncoder are stored in the database.

Digest authentication has a problem in that by default you store cleartext passwords in your database. This is because the browser hashes your password along with the username and Realm name, and this is compared to the password hashed using the same algorithm during authentication. The browser does not know about your MessageDigest algorithm or salt source, so to hash them the same way you need to load a cleartext password from the database.

The plugin does provide an alternative, although it has no configuration options (in particular the digest algorithm cannot be changed). If digest.useCleartextPasswords is false (the default), then the passwordEncoder bean is replaced with an instance of grails.plugin.springsecurity.authentication.encoding. DigestAuthPasswordEncoder. This encoder uses the same approach as the browser, that is, it combines your password along with your username and Realm name essentially as a salt, and hashes with MD5. MD5 is not recommended in general, but given the typical size of the salt it is reasonably safe to use.

The only required attribute is useDigestAuth, which you must set to true, but you probably also want to change the realm name:

grails.plugin.springsecurity.useDigestAuth = true
grails.plugin.springsecurity.digest.realmName = "Ralph's Bait and Tackle"

Digest authentication cannot be applied to a subset of URLs like Basic authentication can. This is due to the password encoding issues. So you cannot use the chainMap attribute here - all URLs will be guarded.

Note that since the Digest auth password encoder is different from the typical encoders you must to pass the username as the "salt" value. The generated User class uses springSecurityService which assumes you're not using a salt value. If you use the generated code in the User class to encode your password, change the dependency injection for springSecurityService with one for the passwordEncoder bean instead:
transient passwordEncoder

and change the code in encodePassword() from

password = springSecurityService.encodePassword(password)

to

password = passwordEncoder.encodePassword(password, username)

8.2 Certificate (X509) Login Authentication

Another authentication mechanism supported by Spring Security is certificate-based, or "mutual authentication". It requires HTTPS, and you must configure the server to require a client certificate (ordinarily only the server provides a certificate). Your username is extracted from the client certificate if it is valid, and you are "pre-authenticated". As long as a corresponding username exists in the database, your authentication succeeds and you are not asked for a password. Your Authentication contains the authorities associated with your username.

The table describes available configuration options.

PropertyDefault ValueMeaning
useX509falseWhether to support certificate-based logins
x509.continueFilterChainOn UnsuccessfulAuthenticationtrueWhether to proceed when an authentication attempt fails to allow other authentication mechanisms to process the request.
x509.subjectDnRegex'CN=(.*?)(?:,|$)'Regular expression (regex) for extracting the username from the certificate's subject name.
x509.checkForPrincipalChangesfalseWhether to re-extract the username from the certificate and check that it's still the current user when a valid Authentication already exists.
x509.invalidateSessionOn PrincipalChangetrueWhether to invalidate the session if the principal changed (based on a checkForPrincipalChanges check).
x509.subjectDnClosurenoneIf set, the plugin's ClosureX509PrincipalExtractor class is used to extract information from the X.509 certificate using the specified closure
x509. throwException WhenTokenRejectedfalseIf true thrown a BadCredentialsException

The details of configuring your server for SSL and configuring browser certificates are beyond the scope of this document. If you use Tomcat, see its SSL documentation. To get a test environment working, see the instructions in this discussion at Stack Overflow.

8.3 Remember-Me Cookie

Spring Security supports creating a remember-me cookie so that users are not required to log in with a username and password for each session. This is optional and is usually implemented as a checkbox on the login form; the default auth.gsp supplied by the plugin has this feature.

PropertyDefault ValueMeaning
rememberMe.cookieName'grails_remember_me'remember-me cookie name; should be unique per application.
rememberMe. alwaysRememberfalseIf true, create a remember-me cookie even if no checkbox is on the form.
rememberMe. tokenValiditySeconds1209600 (14 days)Max age of the cookie in seconds.
rememberMe.parameter'_spring_security_remember_me'Login form remember-me checkbox name.
rememberMe.key'grailsRocks'Value used to encode cookies; should be unique per application.
rememberMe.useSecureCookienoneWhether to use a secure cookie or not; if true a secure cookie is created, if false a non-secure cookie is created, and if not set, a secure cookie is created if the request used HTTPS
rememberMe. createSessionOnSuccesstrueWhether to create a session of one doesn't exist to ensure that the Authentication is stored for future requests
rememberMe.persistentfalseIf true, stores persistent login information in the database.
rememberMe.persistentToken. domainClassNamenoneDomain class used to manage persistent logins.
rememberMe.persistentToken. seriesLength16Number of characters in the cookie's series attribute.
rememberMe.persistentToken. tokenLength16Number of characters in the cookie's token attribute.
atr.rememberMeClassRememberMeAuthenticationTokenremember-me authentication class.

You are most likely to change these attributes:

  • rememberMe.cookieName. Purely aesthetic as most users will not look at their cookies, but you probably want the display name to be application-specific rather than "grails_remember_me".
  • rememberMe.key. Part of a salt when the cookie is hashed. Changing the default makes it harder to execute brute-force attacks.
  • rememberMe.tokenValiditySeconds. Default is two weeks; set it to what makes sense for your application.

Persistent Logins

The remember-me cookie is very secure, but for an even stronger solution you can use persistent logins that store the username in the database. See the Spring Security docs for a description of the implementation.

Persistent login is also useful for authentication schemes like OpenID and Facebook, where you do not manage passwords in your database, but most of the other user information is stored locally. Without a password you cannot use the standard cookie format, so persistent logins enable remember-me cookies in these scenarios.

To use this feature, run the s2-create-persistent-token script. This will create the domain class, and register its name in grails-app/conf/Config.groovy. It will also enable persistent logins by setting rememberMe.persistent to true.

8.4 Ajax Authentication

The typical pattern of using web site authentication to access restricted pages involves intercepting access requests for secure pages, redirecting to a login page (possibly off-site, for example when using a Single Sign-on implementation such as CAS), and redirecting back to the originally-requested page after a successful login. Each page can also have a login link to allow explicit logins at any time.

Another option is to also have a login link on each page and to use JavaScript to present a login form within the current page in a popup. The JavaScript code submits the authentication request and displays success or error messages as appropriate.

The plugin supports Ajax logins, but you need to create your own client-side code. There are only a few necessary changes, and of course the sample code here is pretty basic so you should enhance it for your needs.

The approach here involves editing your template page(s) to show "You're logged in as ..." text if logged in and a login link if not, along with a hidden login form that is shown using JavaScript.

This example uses jQuery and jqModal, a jQuery plugin that creates and manages dialogs and popups. Download jqModal.js and copy it to grails-app/assets/javascripts, and download jqModal.css and copy it to grails-app/assets/stylesheets.

Create grails-app/assets/javascripts/ajaxLogin.js and add this JavaScript code:

var onLogin;

$.ajaxSetup({ beforeSend: function(jqXHR, event) { if (event.url != $("#ajaxLoginForm").attr("action")) { // save the 'success' function for later use if // it wasn't triggered by an explicit login click onLogin = event.success; } }, statusCode: { // Set up a global Ajax error handler to handle 401 // unauthorized responses. If a 401 status code is // returned the user is no longer logged in (e.g. when // the session times out), so re-display the login form. 401: function() { showLogin(); } } });

function showLogin() { var ajaxLogin = $("#ajaxLogin"); ajaxLogin.css("text-align", "center"); ajaxLogin.jqmShow(); }

function logout(event) { event.preventDefault(); $.ajax({ url: $("#_logout").attr("href"), method: "POST", success: function(data, textStatus, jqXHR) { window.location = "/"; }, error: function(jqXHR, textStatus, errorThrown) { console.log("Logout error, textStatus: " + textStatus + ", errorThrown: " + errorThrown); } }); }

function authAjax() { $("#loginMessage").html("Sending request ...").show();

var form = $("#ajaxLoginForm"); $.ajax({ url: form.attr("action"), method: "POST", data: form.serialize(), dataType: "JSON", success: function(json, textStatus, jqXHR) { if (json.success) { form[0].reset(); $("#loginMessage").empty(); $("#ajaxLogin").jqmHide(); $("#loginLink").html( 'Logged in as ' + json.username + ' (<a href="' + $("#_logout").attr("href") + '" id="logout">Logout</a>)'); $("#logout").click(logout); if (onLogin) { // execute the saved event.success function onLogin(json, textStatus, jqXHR); } } else if (json.error) { $("#loginMessage").html('<span class="errorMessage">' + json.error + "</error>"); } else { $("#loginMessage").html(jqXHR.responseText); } }, error: function(jqXHR, textStatus, errorThrown) { if (jqXHR.status == 401 && jqXHR.getResponseHeader("Location")) { // the login request itself wasn't allowed, possibly because the // post url is incorrect and access was denied to it $("#loginMessage").html('<span class="errorMessage">' + 'Sorry, there was a problem with the login request</error>'); } else { var responseText = jqXHR.responseText; if (responseText) { var json = $.parseJSON(responseText); if (json.error) { $("#loginMessage").html('<span class="errorMessage">' + json.error + "</error>"); return; } } else { responseText = "Sorry, an error occurred (status: " + textStatus + ", error: " + errorThrown + ")"; } $("#loginMessage").html('<span class="errorMessage">' + responseText + "</error>"); } } }); }

$(function() { $("#ajaxLogin").jqm({ closeOnEsc: true }); $("#ajaxLogin").jqmAddClose("#cancelLogin"); $("#ajaxLoginForm").submit(function(event) { event.preventDefault(); authAjax(); }); $("#authAjax").click(authAjax); $("#logout").click(logout); });

and create grails-app/assets/stylesheets/ajaxLogin.css and add this CSS:

#ajaxLogin {
   padding:    0px;
   text-align: center;
   display:    none;
}

#ajaxLogin .inner { width: 400px; padding-bottom: 6px; margin: 60px auto; text-align: left; border: 1px solid #aab; background-color: #f0f0fa; -moz-box-shadow: 2px 2px 2px #eee; -webkit-box-shadow: 2px 2px 2px #eee; -khtml-box-shadow: 2px 2px 2px #eee; box-shadow: 2px 2px 2px #eee; }

#ajaxLogin .inner .fheader { padding: 18px 26px 14px 26px; background-color: #f7f7ff; margin: 0px 0 14px 0; color: #2e3741; font-size: 18px; font-weight: bold; }

#ajaxLogin .inner .cssform p { clear: left; margin: 0; padding: 4px 0 3px 0; padding-left: 105px; margin-bottom: 20px; height: 1%; }

#ajaxLogin .inner .cssform input[type="text"], #ajaxLogin .inner .cssform input[type="password"] { width: 150px; }

#ajaxLogin .inner .cssform label { font-weight: bold; float: left; text-align: right; margin-left: -105px; width: 150px; padding-top: 3px; padding-right: 10px; }

.ajaxLoginButton { background-color: #efefef; font-weight: bold; padding: 0.5em 1em; display: -moz-inline-stack; display: inline-block; vertical-align: middle; white-space: nowrap; overflow: visible; text-decoration: none; -moz-border-radius: 0.3em; -webkit-border-radius: 0.3em; border-radius: 0.3em; }

.ajaxLoginButton:hover, .ajaxLoginButton:focus { background-color: #999999; color: #ffffff; }

#ajaxLogin .inner .login_message { padding: 6px 25px 20px 25px; color: #c33; }

#ajaxLogin .inner .text_ { width: 120px; }

#ajaxLogin .inner .chk { height: 12px; }

.errorMessage { color: red; }

There's no need to register the JavaScript files in grails-app/assets/javascripts/application.js if you have this require_tree directive:

//= require_tree .

but you can explicitly include them if you want. Register the two CSS files in /grails-app/assets/stylesheets/application.css:

/*
 …
 *= require ajaxLogin
 *= require jqModal
 …
 */

We'll need some GSP code to define the HTML, so create grails-app/views/includes/_ajaxLogin.gsp and add this:

<span id="logoutLink" style="display: none;">
<g:link elementId='_logout' controller='logout'>Logout</g:link>
</span>

<span id="loginLink" style="position: relative; margin-right: 30px; float: right"> <sec:ifLoggedIn> Logged in as <sec:username/> (<g:link elementId='logout' controller='logout'>Logout</g:link>) </sec:ifLoggedIn> <sec:ifNotLoggedIn> <a href="#" onclick="showLogin(); return false;">Login</a> </sec:ifNotLoggedIn> </span>

<div id="ajaxLogin" class="jqmWindow" style="z-index: 3000;"> <div class="inner"> <div class="fheader">Please Login..</div> <form action="${request.contextPath}/j_spring_security_check" method="POST" id="ajaxLoginForm" name="ajaxLoginForm" class="cssform" autocomplete="off"> <p> <label for="username">Username:</label> <input type="text" class="text_" name="j_username" id="username" /> </p> <p> <label for="password">Password</label> <input type="password" class="text_" name="j_password" id="password" /> </p> <p> <label for="remember_me">Remember me</label> <input type="checkbox" class="chk" id="remember_me" name="_spring_security_remember_me"/> </p> <p> <input type="submit" id="authAjax" name="authAjax" value="Login" class="ajaxLoginButton" /> <input type="button" id="cancelLogin" value="Cancel" class="ajaxLoginButton" /> </p> </form> <div style="display: none; text-align: left;" id="loginMessage"></div> </div> </div>

And finally, update the grails-app/views/layouts/main.gsp layout to include _ajaxLogin.gsp, adding it after the <body> tag:

<html lang="en" class="no-js">
   <head>
      …
      <g:layoutHead/>
   </head>
   <body>
      <g:render template='/includes/ajaxLogin'/>
      …
      <g:layoutBody/>
   </body>
</html>

The important aspects of this code are:

  • There is a <span> positioned in the top-right that shows the username and a logout link when logged in, and a login link otherwise.
  • The form posts to the same URL as the regular form, /j_spring_security_check, and is mostly the same except for the addition of a "Cancel" button (you can also dismiss the dialog by clicking outside of it or with the escape key).
  • Error messages are displayed within the popup <div>.
  • Because there is no page redirect after successful login, the Javascript replaces the login link to give a visual indication that the user is logged in.
  • The Logout link also uses Ajax to submit a POST request to the standard logout url and redirect you to the index page after the request finishes.
    • Note that in the JavaScript logout function, you'll need to change the url in the success callback to the correct post-logout value, e.g. window.location = "/appname";

How Does Ajax login Work?

Most Ajax libraries include an X-Requested-With header that indicates that the request was made by XMLHttpRequest instead of being triggered by clicking a regular hyperlink or form submit button. The plugin uses this header to detect Ajax login requests, and uses subclasses of some of Spring Security's classes to use different redirect urls for Ajax requests than regular requests. Instead of showing full pages, LoginController has JSON-generating methods ajaxSuccess(), ajaxDenied(), and authfail() that generate JSON that the login Javascript code can use to appropriately display success or error messages.

To summarize, the typical flow would be

  • click the link to display the login form
  • enter authentication details and click Login
  • the form is submitted using an Ajax request
  • if the authentication succeeds:
    • a redirect to /login/ajaxSuccess occurs (this URL is configurable)
    • the rendered response is JSON and it contains two values, a boolean value success with the value true and a string value username with the authenticated user's login name
    • the client determines that the login was successful and updates the page to indicate the the user is logged in; this is necessary since there's no page redirect like there would be for a non-Ajax login
  • if the authentication fails:
    • a redirect to /login/authfail?ajax=true occurs (this URL is configurable)
    • the rendered response is JSON and it contains one value, a string value error with the displayable error message; this will be different depending on why the login was unsuccessful (bad username or password, account locked, etc.)
    • the client determines that the login was not successful and displays the error message
  • note that both a successful and an unsuccessful login will trigger the onSuccess Ajax callback; the onError callback will only be triggered if there's an exception or network issue