(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"
Property | Default | Description |
---|
useBasicAuth | false | Whether 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.
Property | Default Value | Meaning |
---|
useDigestAuth | false | Whether 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. nonceValiditySeconds | 300 | How long a nonce stays valid. |
digest. passwordAlreadyEncoded | false | Whether you are managing the password hashing yourself. |
digest. createAuthenticatedToken | false | If 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. useCleartextPasswords | false | If 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() frompassword = springSecurityService.encodePassword(password)
topassword = 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.
Property | Default Value | Meaning |
---|
useX509 | false | Whether to support certificate-based logins |
x509.continueFilterChainOn UnsuccessfulAuthentication | true | Whether 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.checkForPrincipalChanges | false | Whether 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 PrincipalChange | true | Whether to invalidate the session if the principal changed (based on a checkForPrincipalChanges check). |
x509.subjectDnClosure | none | If set, the plugin's ClosureX509PrincipalExtractor class is used to extract information from the X.509 certificate using the specified closure |
x509. throwException WhenTokenRejected | false | If 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.
Property | Default Value | Meaning |
---|
rememberMe.cookieName | 'grails_remember_me' | remember-me cookie name; should be unique per application. |
rememberMe. alwaysRemember | false | If true , create a remember-me cookie even if no checkbox is on the form. |
rememberMe. tokenValiditySeconds | 1209600 (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.useSecureCookie | none | Whether 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. createSessionOnSuccess | true | Whether to create a session of one doesn't exist to ensure that the Authentication is stored for future requests |
rememberMe.persistent | false | If true , stores persistent login information in the database. |
rememberMe.persistentToken. domainClassName | none | Domain class used to manage persistent logins. |
rememberMe.persistentToken. seriesLength | 16 | Number of characters in the cookie's series attribute. |
rememberMe.persistentToken. tokenLength | 16 | Number of characters in the cookie's token attribute. |
atr.rememberMeClass | RememberMeAuthenticationToken | remember-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:
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