Avoiding a common spring annotation configuration mistake
December 7th, 2015
Making sure spring singletons are indeed singletons.
## Spring Boot With (Pac4J) OAuth
This article is going to run through setting up a relatively simple application that utilizes Spring Boot, Thymeleaf and Pac4J Spring Security. The source code of where we end up is available at https://github.com/aaronhanson/spring-boot-oauth-demo. This is somewhat of a port of the Pac4J Spring demo stripping out non-OAuth stuff and making it work with Spring Boot. For reference, I’m building with Java 8 and Gradle 2.8 while writing this.
You can following along and you should be able to checkout the **step-1** tag and run it.
$ git checkout step-1
$ ./gradlew bootRun
Fire up your browser and hit http://localhost:8080 and you should see the “Index Page”.
## Setting Up The Application
Spring Boot is fairly quick to setup, for our purposes let’s create a few directories to throw things into.
The root directory for our application code:
$ mkdir -p src/main/groovy/springboot/pac4j
For Thymeleaf things:
$ mkdir -p src/main/resources/templates
And let’s setup external configuration right away for our credentials and such that won’t be checked in.
$ touch application.properties
And we’ll need a simple build file to start with.
**build.gradle**
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2'
classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.6.RELEASE'
}
}
apply plugin: 'groovy'
apply plugin: 'spring-boot'
ext {
springBootVersion = '1.2.6.RELEASE'
groovyVersion = "2.4.3"
}
springBoot {
mainClass = "springboot.pac4j.SpringBootPac4jDemo"
}
repositories {
jcenter()
}
dependencies {
compile "org.codehaus.groovy:groovy-all:${groovyVersion}"
compile "org.springframework.boot:spring-boot-starter-web:${springBootVersion}"
compile "org.springframework.boot:spring-boot-starter-thymeleaf:${springBootVersion}"
}
Next we can create the main entry point class.
**src/main/groovy/springboot/pac4j/SpringBootPac4jDemo.groovy**
package springboot.pac4j
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
class SpringBootPac4jDemo {
public static void main(String[] args) {
SpringApplication.run(SpringBootPac4jDemo, args)
}
}
And while we’re at it, let’s create a boring index controller.
**src/main/groovy/springboot/pac4j/controller/IndexController.groovy**
package springboot.pac4j.controller
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
@Controller
class IndexController {
@RequestMapping("/")
String index() {
return "index"
}
}
**src/main/resources/templates/index.html**
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Pac4j Demo</title>
</head>
<body>
<h2>Index Page</h2>
</body>
</html>
## Thymeleaf Configuration
Next we’re going to work on the Thymeleaf configuration. Checkout the **step-2** tag of the project and you can see the changes.
$ git checkout step-2
In the application.properties we’re going to disable caching so when we make changes to our templates we’ll see them on refreshes.
Add the following:
spring.thymeleaf.cache=false
Go ahead and run the app again and make a change to the index.html template with your own message or whatever to make sure it works.
We’re also going to add the Spring Security extras to the build.gradle dependencies which will allow us to use familiar Spring Security expressions in the templates to restrict access to rending sections based on roles etc.
compile "org.thymeleaf.extras:thymeleaf-extras-springsecurity4:2.1.2.RELEASE"
To enable it in our application we just need to create a configuration class and a bean for the dialect.
**src/main/groovy/springboot/pac4j/config/ThymeleafConfig.grooy**
package springboot.pac4j.conf
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect
@Configuration
public class ThymeleafConfig {
@Bean
public SpringSecurityDialect springSecurityDialect() {
return new SpringSecurityDialect()
}
}
We’re also going to add in a Thymeleaf layout and tweak the index.html to use it to setup for having a logout button based on wether or not we’re logged in. I’m going to leave out all of that code but you can look at the *src/main/resource/templates* directory to check out the additions and modifications.
## Spring Security And Pac4J
Next we’ll add the dependencies for Spring Security and Pac4J to *build.gradle*. We’ll use **spring-boot-starter-security**, **spring-security-pac4j** and **pac4j-oauth**, since we’re just going to be concerned with OAuth for this app. I’m using a slightly older version of pac4j-oauth since the newer version changes some things up and wasn’t used in the Pac4J demo I’m porting this over from.
compile "org.springframework.boot:spring-boot-starter-security:${springBootVersion}"
compile("org.pac4j:spring-security-pac4j:1.3.0") {
exclude module: 'spring-security-web'
exclude module: 'spring-security-config'
}
compile group: 'org.pac4j', name: 'pac4j-oauth', version:'1.7.0'
Now that we have our dependencies in place let’s add a Pac4jConfig and SecurityConfig to the application.
The Pac4J configuration is fairly straight forward. Each client we want to support we can register a bean for and provider the appropriate security credentials from the OAuth provider. Then we need a **clients** bean that holds all of our clients and a **clientProvider** that we’ll need as the AuthenticationManager for the filter we’ll setup in the Security configuration.
**src/main/groovy/springboot/pac4j/config/Pac4jConfig.groovy**
package springboot.pac4j.conf
import org.pac4j.core.client.Clients
import org.pac4j.oauth.client.GitHubClient
import org.pac4j.springframework.security.authentication.ClientAuthenticationProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class Pac4jConfig {
@Value('${oauth.callback.url}')
String oauthCallbackUrl
@Value('${oauth.github.app.key}')
String githubKey
@Value('${oauth.github.app.secret}')
String githubSecret
@Bean
ClientAuthenticationProvider clientProvider() {
return new ClientAuthenticationProvider(clients: clients())
}
@Bean
GitHubClient gitHubClient() {
return new GitHubClient(githubKey, githubSecret)
}
@Bean
Clients clients() {
return new Clients(oauthCallbackUrl, gitHubClient())
}
}
We’ll also need to add the **application.properties** values needed for the Pac4J configuration class. As a habit, I like to use local.somedomain.com as a hosts entry for localhost. It’s useful for a variety of reasons but mostly because in this scenario some OAuth providers want a “real” domain for the redirect url. Normally this would be whatever your publicly exposed url. This value needs to match the value to you configure in GitHub for your application.
**application.properties**
oauth.callback.url=http://local.yourdomain.com:8080/callback
oauth.github.app.key=YOUR_GITHUB_CLIENT_ID
oauth.github.app.secret=YOUR_GITHUB_CLIENT_SECRET
Spring Security comes secured by default so if we tried to run the app now we wouldn’t be able to see anything, so let’s setup the security configuration. Since we’ll be using mostly annotation based security we need to use the EnableGlobalMethodSecurity annotation with the prePostEnabled and securedEnabled parameters. If we also wanted some predefined static rules we could add an addMatchers() call after the authorizeRequest() with the appropriate rules we wanted.
In order to get the OAuth clients to participate in the security chain we need to create a custom filter and wire it in. This is what the **clientFilter** is for. We’re not making it a bean since it’s a filter and then we’d have to exclude it from the normal request processing. But if you need it for other autowiring, there’s a way to handle that by disabling it. Check out this Stack Overflow question prevent-spring-boot-from-registering-a-servlet-filter for how.
**src/main/groovy/springboot/pac4j/config/SecurityConfig.groovy**
package springboot.pac4j.conf
import org.pac4j.core.client.Clients
import org.pac4j.springframework.security.authentication.ClientAuthenticationProvider
import org.pac4j.springframework.security.web.ClientAuthenticationFilter
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.builders.WebSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
ApplicationContext context
@Autowired
Clients clients
@Autowired
ClientAuthenticationProvider clientProvider
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers(
"/**/*.css",
"/**/*.png",
"/**/*.gif",
"/**/*.jpg",
"/**/*.ico",
"/**/*.js"
)
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.permitAll()
http.addFilterBefore(clientFilter(), UsernamePasswordAuthenticationFilter)
}
ClientAuthenticationFilter clientFilter() {
return new ClientAuthenticationFilter(
clients: clients,
sessionAuthenticationStrategy: sas(),
authenticationManager: clientProvider as AuthenticationManager
)
}
@Bean
SessionAuthenticationStrategy sas() {
return new SessionFixationProtectionStrategy()
}
}
We also want to create a login controller to handle generating the client authentication links for the view. These are the initial OAuth requests to sign the user in to the respective service. For the moment we’ll let it be dumb and not worry about if the user navigates here manually but is already logged in, but that should be considered in real scenarios.
**src/main/groovy/springboot/pac4j/controller/LoginController.groovy**
package springboot.pac4j.controller
import org.pac4j.core.client.BaseClient
import org.pac4j.core.client.Clients
import org.pac4j.core.context.J2EContext
import org.pac4j.core.context.WebContext
import org.pac4j.oauth.client.GitHubClient
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.RequestMapping
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Controller
class LoginController {
@Autowired
Clients clients
@RequestMapping("/login")
String login(HttpServletRequest request, HttpServletResponse response, Model model) {
final WebContext context = new J2EContext(request, response)
final GitHubClient gitHubClient = (GitHubClient) clients.findClient(GitHubClient)
model.addAttribute("gitHubAuthUrl", getClientLocation(gitHubClient, context))
return "login"
}
public String getClientLocation(BaseClient client, WebContext context) {
return client.getRedirectAction(context, false, false).getLocation()
}
}
At this point we should have a very basic application that forces the user to sign in with GitHub and redirects to the index page. And if they choose they can also logout.
## Additional OAuth providers
It’s actually fairly simple now to add other OAuth providers. Pac4J has a number of them ready to go and all you need to do is get the client id and secrets form the respective services and add another bean for each one you’d like to support. Checkout the **step-3** tag of the project for this part.
$ git checkout step-3
Let’s add Twitter and Google since I’ve got sample apps set up for each of them. Create a bean for each client and then add them to the **clients** bean creation.
**src/main/groovy/springboot/pac4j/config/Pac4jConfig.groovy**
package springboot.pac4j.conf
import org.pac4j.core.client.Clients
import org.pac4j.oauth.client.GitHubClient
import org.pac4j.oauth.client.Google2Client
import org.pac4j.oauth.client.TwitterClient
import org.pac4j.springframework.security.authentication.ClientAuthenticationProvider
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class Pac4jConfig {
@Value('${oauth.callback.url}')
String oauthCallbackUrl
@Value('${oauth.github.app.key}')
String githubKey
@Value('${oauth.github.app.secret}')
String githubSecret
@Value('${oauth.twitter.app.key}')
String twitterKey
@Value('${oauth.twitter.app.secret}')
String twitterSecret
@Value('${oauth.google.app.key}')
String googleKey
@Value('${oauth.google.app.secret}')
String googleSecret
@Bean
ClientAuthenticationProvider clientProvider() {
return new ClientAuthenticationProvider(clients: clients())
}
@Bean
TwitterClient twitterClient() {
return new TwitterClient(twitterKey, twitterSecret)
}
@Bean
Google2Client google2Client() {
return new Google2Client(googleKey, googleSecret)
}
@Bean
GitHubClient gitHubClient() {
return new GitHubClient(githubKey, githubSecret)
}
@Bean
Clients clients() {
return new Clients(oauthCallbackUrl, gitHubClient(), twitterClient(), google2Client())
}
}
Then we can update the **LoginController** and login template with the new client options.
**src/main/groovy/springboot/pac4j/controller/LoginController.groovy**
package springboot.pac4j.controller
import org.pac4j.core.client.BaseClient
import org.pac4j.core.client.Clients
import org.pac4j.core.context.J2EContext
import org.pac4j.core.context.WebContext
import org.pac4j.oauth.client.GitHubClient
import org.pac4j.oauth.client.Google2Client
import org.pac4j.oauth.client.TwitterClient
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.RequestMapping
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Controller
class LoginController {
@Autowired
Clients clients
@RequestMapping("/login")
String login(HttpServletRequest request, HttpServletResponse response, Model model) {
final WebContext context = new J2EContext(request, response)
final GitHubClient gitHubClient = (GitHubClient) clients.findClient(GitHubClient)
final Google2Client google2Client = (Google2Client) clients.findClient(Google2Client)
final TwitterClient twitterClient = (TwitterClient) clients.findClient(TwitterClient)
model.addAttribute("gitHubAuthUrl", getClientLocation(gitHubClient, context))
model.addAttribute("google2AuthUrl", getClientLocation(google2Client, context))
model.addAttribute("twitterAuthUrl", getClientLocation(twitterClient, context))
return "login"
}
public String getClientLocation(BaseClient client, WebContext context) {
return client.getRedirectAction(context, false, false).getLocation()
}
}
And add in our application.properties for the new providers
oauth.google.app.key=YOUR_GOOGLE_CLIENT_ID
oauth.google.app.secret=YOUR_GOOGLE_CLIENT_SECRET
oauth.twitter.app.key=YOUR_TWITTER_CONSUMER_KEY
oauth.twitter.app.secret=YOUR_TWITTER_CONSUMER_SECRET
Then we can update the login page to add the additional provider buttons and if we run the application now we should be able to login with Twitter, Google, or GitHub.
I think that’s enough for this post. In a follow up, we’ll take this from basic login to a slightly more realistic example with registration and roles.
Making sure spring singletons are indeed singletons.