Spring Boot With (Pac4J) OAuth

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

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.

github thumbnail

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.

One thought on “Spring Boot With (Pac4J) OAuth

  1. chris marx says:

    So with this setup using pac4j and spring security, would you then be able to things like @AuthenticationPrincipal and @PreAuthorize/@Secured in controllers with support for roles?

    1. Aaron Hanson says:

      Yes. In a follow up post I started added in some of that behavior. https://objectpartners.com/2015/12/17/from-poc-to-mpa/ The project has a step-4 tag that you can checkout and look at the AdminController for using the @PreAuthorize annotation for roles. It does not load any role information however as part of the example but that can definitely be added.

Leave a Reply

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

*

*