Localization of Spring Security Error Messages in Spring Boot

No Comments

Spring Security is a framework for easily adding state-of-the-art authentication and authorization to Spring applications. When used in conjuction with Spring Boot, adding basic authentication to a web application is straightforward. Although Spring Boot will take care of configuring Spring Security, localization of Springs Security’s error messages is not autoconfigured. In this blog post I will show how to add localization to Spring Security and show some pitfalls while doing so.

Example

I’ve implemented a very simple application with basic authentication as an example for this blog post. The application has a @RestController handling GET requests to /hello, which returns a “Hello World” message. Furthermore I’ve configured Spring Security with basic authentication and set up an in-memory user with user name “user” and password “password”. All code can be found at GitHub.

When I start the application and try to access the hello resource without authentication, I get:

$> curl -i http://localhost:8080/hello
HTTP/1.1 401
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: JSESSIONID=3E1B7F48E35AC7FEF0A5A66CEAF843D5; Path=/; HttpOnly
WWW-Authenticate: Basic realm="Realm"
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 22 Aug 2017 06:00:37 GMT

{
  "timestamp":1503381637553,
  "status":401,
  "error":"Unauthorized",
  "message":"Full authentication is required to access this resource",
  "path":"/hello"
}

Trying to access the resource with wrong credentials also yields an error (headers omitted this time):

$> curl -u user:wrongPassword http://localhost:8080/hello
{
  "timestamp":1503381723115,
  "status":401,
  "error":"Unauthorized",
  "message":"Bad credentials",
  "path":"/hello"
}

Only if I provide correct authentication, I can access the resource:

$> curl -u user:password http://localhost:8080/hello
{
  "message": "Hello World!"
}

As you can see, the error messages returned are all in Englisch. Since I’m developing software for German customers, I sometimes want my application to return German error messages. Let’s see how we can achieve that.

Loading Messages

Localization in Spring relies heavily on the MessageSource facilities. A MessageSource is an abstraction over how to access messages for an application with support for parameterization and localization of those messages. A common way to define a MessageSource is to use a ResourceBundleMessageSource or a ReloadableResourceBundleMessageSource both resolving messages using Java resource bundles. To sum this up: If we want localized Spring Security error messages, we need to provide a message source, which loads the resource bundles defining Spring Security’s error messages.

The Spring Security documentation only describes how to do this with XML configuration. Since we’re writing a Spring Boot application, we want to configure everything using Java config:

@Bean
public MessageSource messageSource() {
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    messageSource.addBasenames("classpath:org/springframework/security/messages");
    return messageSource;
}

It is important to call the @Bean method “messageSource”. This will replace the existing message source with ID “messageSource” in the application context bootstrapped by Spring Boot, thereby using it throughout the application. If we don’t replace the existing message source and instead configure a new one (for example by calling my @Bean method “myMessageSource”), the configured messages will not be used.

After adding Spring Security’s error messages to my message source, I can retry my request:

$> curl -u user:wrongPassword http://localhost:8080
{
  "timestamp":1503383024741,
  "status":401,
  "error":"Unauthorized",
  "message":"Ungültige Anmeldedaten",
  "path":"/hello"
}

We’ve loaded the Spring Security error messages and the German message is now returned. Great!

Setting a default Locale

Well, almost great, but not completely. The problem is that now the server’s locale decides which language will be returned. This is because a MessageSource always needs a Locale parameter to resolve a message. If none can be found, Spring falls back to Locale.getDefault(). I have two problems with this:

  1. You never know how the system locale will be set in your production environment running docker containers on Kubernetes in AWS. My guess is that it will be English, but I won’t count on it.
  2. Far more people speak English than German.

For this reasons it’s better to use English as default language and let the client decide which language it can accept. The former can be configured by setting the default Locale, preferably in the messageSource method:

@Bean
public MessageSource messageSource() {
    Locale.setDefault(Locale.ENGLISH);
    ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
    messageSource.addBasenames("classpath:org/springframework/security/messages");
    return messageSource;
}

The latter is achieved by setting the Accept-Language header in the client. This changes the request to:

$> curl -u user:wrongPassword http://localhost:8080
{
  "timestamp":1503381723115,
  "status":401,
  "error":"Unauthorized",
  "message":"Bad credentials",
  "path":"/hello"
}

$> curl -u user:wrongPassword -H 'Accept-Language: de-DE' http://localhost:8080/hello
{
  "timestamp":1503383940064,
  "status":401,
  "error":"Unauthorized",
  "message":"Ungültige Anmeldedaten",
  "path":"/hello"
}

There are a lot of languages already shipped with Spring Security. If yours is missing or if you want to change some translations, you can easily do this by following these steps:

  1. copy the english message.properties file from Spring Security to your project.
  2. translate or change the messages you’re interested in.
  3. In your messageSource method, add the new properties file to your message source.

Shortcomings

Remember that we also tried to access the hello resource without any authentication in our first example? Let’s try that again using the Accept-Language header:

$> curl -H 'Accept-Language: de-DE' http://localhost:8080/hello
{
  "timestamp":1503383125672,
  "status":401,
  "error":"Unauthorized",
  "message":"Full authentication is required to access this resource",
  "path":"/hello"
}

Sad but true, this is a shortcoming of Spring Security’s localization. The reason is that the “Bad credentials” message is returned by AbstractUserDetailsAuthenticationProvider from spring-security-core, while the “Full authentication required” message is returned by ExceptionTranslationFilter from spring-security-web. spring-security-core ships with message bundles, and therefore can be localized using a MessageSource. The error messages in ExceptionTranslationFilter are hard coded with no way of using a MessageSource for localization. I think this should be configurable using resource bundles the same way as it is for the messages from spring-security-core. For this reason I created a pull request for Spring Security that will hopefully be merged soon.

Conclusion

By loading Spring Security message bundles using a MessageSource it is pretty easy to add localization to Spring Security error messages. One thing to look for is how the default system Locale is involved while determining the language for localization. My suggestion is to make sure the default is English and let the client decide explicitly using the Accept-Language header. Furthermore, there are still some messages in Spring Security that cannot be localized this way, but hopefully will be in the future.

Benedikt Ritter works as a Software Craftsman at codecentric AG in Solingen since September 2013. His joy for creating reliable software is not limited to coding at work: Benedikt is member of the Apache Software Foundation and Committer for the Apache Commons project.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Comment

Your email address will not be published. Required fields are marked *