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.
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.
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?
Yes. In a follow up post I started added in some of that behavior. https://stage.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.