Leverage Spring Security to Test in Production

Spring Security is the “gold standard” in the Java world for integrating authentication and authorization into web applications. It supports standards, like SAML, OpenID Connect, and OAuth. It supports database backends and in-memory storage for user identity data.

Split is the “gold standard” for testing and measuring different experiences in web applications. And, Split enables you to test in production, with a small group of users, without having to redeploy your code. When the test is complete and the new functionality is ready to go live, you flip a switch in Split and – boom – there it is for everyone. Again, without having to redeploy. That is a massive time saver for your development and DevOps teams. (And it’ll make your PM pretty happy too!)

The marriage of Split and Spring Security is so easy that it just might blow your mind!

In this post, I’m going to walk you through building an application that demonstrates how to:

  1. Create a test using Split (called a treatment)
  2. Create an app with Spring Boot and Spring Security that leverages the treatment
  3. Set up the app in such a way that only a “beta test” group has access to new functionality
  4. Update the treatment so that all users get the new functionality
  5. NOT deploy the code more than once

Imagine a web app that’s in production. You want to add some new functionality that’s only available to a “beta test” group of users. When the beta test group gives you the thumbs up that the new functionality is good, you want to enable it for all users. And, you only want to deploy this app once. As a bonus, you want to be able to add new functionality in the future and beta test it in the same way, without having to update the part of the code responsible for managing the test. All of this is possible with Split and is what I am going to show you right now!

Get Started with Spring Boot

The Spring Initializr project makes getting started with Spring Boot a snap. For the example app, I’ll use the web, thymeleaf and spring security dependencies. You can customize things like package name and artifact ID. Then, you can download an archive of the complete project scaffolding, ready to be imported into the IDE of your choice.

If you’re a denizen of the terminal like me, you don’t even need the website. You can run the following:

mkdir split-spring-security-beta-test-example && cd split-spring-security-beta-test-example curl https://start.spring.io/starter.tgz \ -d dependencies=web,thymeleaf,security \ -d groupId=io.split.examples \ -d artifactId=split-spring-security-beta-test-example \ -d packageName=io.split.examples.betatestexample \ -d description="Leverage Spring Security to Beta Test new Features" \ | tar -xvf -
Code language: Swift (swift)

The important bit is the: dependencies=web,thymeleaf,security which tells Spring Initializr what dependencies to include in the pom.xml file.

NOTE: By default, Spring Boot now uses Java 11. Make sure you have the appropriate JDK installed on your machine. On Mac, I like to use sdkman to manage multiple JDKs.

Import the project into your IDE. I use IntelliJ. You can also find the completed project on GitHub.

At this point, you can actually fire up the project with:

mvn spring-boot:run
Code language: Arduino (arduino)

Spring Security has a “deny all unless otherwise specified” approach, which is a security best practice. You’ll notice in the output a default generated password for accessing this bare-bones application. Something like:

Using generated security password: fe23b3bf-fa77-4c60-9c28-4bf97e1c3300
Code language: PowerShell (powershell)

If you browse to http://localhost:8080, you’ll see a default login form:

The default username is: user. Using this and the password above, you can login. You’ll immediately see an error on successful login because we haven’t actually created an application yet. But, you can see that authentication is working out of the box.

Set up Some In-Memory Users and Groups

For the purposes of the demonstration, I am going to use a simple, in-memory database of users and groups. In real life, you’d connect Spring Security to an auth service or database for user identities.

I’ll add a SecurityConfig.java file to the project:

@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override public UserDetailsService userDetailsService() { InMemoryUserDetailsManager userManager = new InMemoryUserDetailsManager( newUser("linda", "12345678", "USER"), newUser("bob", "12345678", "USER"), newUser("tina", "12345678", "USER", "BETA_TESTER"), newUser("gene", "12345678", "USER", "BETA_TESTER"), newUser("louise", "12345678", "USER", "BETA_TESTER") ); return userManager; } private UserDetails newUser(String username, String password, String... roles) { return User .withUsername(username) .passwordEncoder( PasswordEncoderFactories.createDelegatingPasswordEncoder()::encode ) .password(password).roles(roles).build(); } }
Code language: Scala (scala)

The private newUser method uses Spring Security’s builder for creating users. It also uses Spring Security’s PasswordEncoderFactories, which defaults to bcrypt hashed passwords, which is a security best practice.

If you restart the application, you can now log in with these five users. Note: you’ll still get an error after login, as we’ve still not set up the application.

Notice that tina, gene, and louise all have the BETA_TESTER role. We’ll use this later to ensure that only they see the new functionality, while bob and linda still see the old functionality.

Add Authenticated Pages

We’re using the Thymeleaf templating engine in our project. This makes it easy for us to set up some pages while keeping the application small and simple.

First, we need a controller to direct incoming traffic to our application. Here’s HomeController.java:

@Controller public class HomeController { @GetMapping("/") public String home(@AuthenticationPrincipal UserDetails user, Model model) { model.addAttribute("username", user.getUsername()); model.addAttribute("roles", user.getAuthorities()); return "home"; } }
Code language: Java (java)

Spring Security automatically passes in the UserDetails object because of the @AuthenticationPrincipal annotation. Spring Web passes in a Model object automatically.

The code adds username and roles to the model and then directs Thymleaf to render the home template by returning it as a String.

Thymeleaf looks for templates by default in src/main/resources/templates. Here’s home.html:

<html xmlns:th="http://www.w3.org/1999/xhtml"> <head></head> <body> <h2> <span th:inline="text">Hello, [[${username}]]!</span> </h2> <p/> Here are your roles: <br/> <ul th:each="role : ${roles}"> <li th:inline="text">[[${role}]]</li> </ul> <hr/> <a href="/logout">Logout</a> </body> </html>
Code language: Django (django)

This template displays the username found in the model on line 5. And, it iterates over the list of roles using th:each on lines 10 – 12.

Notice that there’s a link to /logout at the bottom. Spring Security has default logout logic built in.

Fire up the app again and you should see that you can login as before and see the simple landing page. You should also be able to logout and login as someone else.

You may notice that although we defined the role as BETA_TESTER, it shows up as ROLE_BETA_TESTER. This is due to Spring Security’s ability to support multiple role types, each with its own prefix. ROLE_ underscore is the default.

At this point, the Spring Boot + Spring Security application is fully functional (such as it is).

Next, I talk about Split and then I bring Spring and Split together.

Get Started with Feature Flags

Getting setup with a free developer account for Split is as easy as 1, 2, 3:

  1. Go to: https://split.io, click: Free Account
  2. Fill out the registration form and click: SIGN UP
  3. Follow the link you receive in email and set a password.

Create the Treatment in Split

Treatments allow you to define settings and behaviors for what you want to test. For my example, we want to setup a treatment that will return on or off depending on whether or not you are part of the beta tester group.

To start, click DE in the upper left. Choose Admin Settings and API Keys. Copy the value for sdk Type in the prod-default Environment. You’ll need this in the Spring Boot app shortly.

Next, Click Splits on the left-hand side and click Create Split. Give it a Name. Leave the other defaults and click Create.

Next, click Add Rules on the Targeting Rules tab. Split automatically adds on and off treatment definitions and sets off as the default.

For the use-case in this example, we want to add a group for which the treatment will return a value of on.

Click Add Rule in the Set Targeting Rules section. Here, we want to have the treatment return “on” if the user is in the group of beta testers. To accomplish this, enter groups in the Add attribute field. From the Select matcher dropdown, choose Set > has any of and enter ROLE_BETA_TESTER in the field. Change the serve dropdown to on.

This now makes it read like an english sentence: “If the user has an attribute called groups and the groups list contains the value ROLE_BETA_TESTER, then serve ‘on’ for the treatment”

Click Save Changes

Click Confirm on the summary screen.

Integrate the Treatment with Spring Security

Edit the pom.xml file in the project. Add the following dependency:

<dependency> <groupId>io.split.client</groupId> <artifactId>java-client</artifactId> <version>4.0.1</version> </dependency>
Code language: Django (django)

This brings the Split Java SDK into scope for the project.

Next, add a configuration to the project to make the Split Java Client available to the application. Here’s SplitConfig.java:

@Configuration public class SplitConfig { @Value("#{ @environment['split.api-key'] }") private String splitApiKey; @Bean public SplitClient splitClient() throws Exception { SplitClientConfig config = SplitClientConfig.builder() .setBlockUntilReadyTimeout(1000) .enableDebug() .build(); SplitFactory splitFactory = SplitFactoryBuilder.build(splitApiKey, config); SplitClient client = splitFactory.client(); client.blockUntilReady(); return client; } }
Code language: Java (java)

Create a file under src/main/resources called application.yml and place the following in it:

split: api-key: <your Split API Key>
Code language: Django (django)
Notice that it’s using the @Value annotation to pull in the Split API Key from the environment. This is a best practice. You should never hardcode an API Key into an application nor commit it in a git repo. In this case, application.yml is listed in the .gitignore file to ensure it’s not added to the git repo.

This is the key line that sets up the Split Client for use elsewhere in the code:

SplitFactory splitFactory = SplitFactoryBuilder.build(splitApiKey, config);
Code language: Arduino (arduino)

Let’s set up a new template called home-beta.html (It’s mostly copypasta from the original template and should be located in: src/main/resources/templates):

<html xmlns:th="http://www.w3.org/1999/xhtml"> <head></head> <body> <h1>WELCOME TO THE BETA EXPERIENCE</h1> <h2> <span th:inline="text">Hello, [[${username}]]!</span> </h2> <p/> Here are your roles: <br/> <ul th:each="role : ${roles}"> <li th:inline="text">[[${role}]]</li> </ul> <hr/> <a href="/logout">Logout</a> </body> </html>
Code language: Django (django)

The last piece of the puzzle is in our HomeController. I want to have the app render the new home-beta template if the authenticated user is in the ROLE_BETA_TESTER group. Here’s the updated controller:

@Controller public class HomeController { SplitClient splitClient; public HomeController(SplitClient splitClient) { this.splitClient = splitClient; } @GetMapping("/") public String home(@AuthenticationPrincipal UserDetails user, Model model) { model.addAttribute("username", user.getUsername()); model.addAttribute("roles", user.getAuthorities()); List<String> groups = user.getAuthorities().stream() .map(GrantedAuthority::getAuthority).collect(Collectors.toList()); String inBeta = splitClient.getTreatment( user.getUsername(), "BETA_UI_EXPERIENCE", Map.of("groups", groups) ); return "on".equals(inBeta) ? "home-beta" : "home"; } }
Code language: Arduino (arduino)

The first thing to notice is that I am injecting the Split Client using the constructor dependency injection of Spring.

The real magic happens with the splitClient.getTreatment call. The first parameter is the username provided by Spring Security for the authenticated user.

Note that in much of the documentation, this parameter is referred to as a key. Don’t confuse this with the API Key, which should NEVER be used as the first parameter to the getTreatment call.

The second parameter is the name of the treatment in Split that we want to target.

The third parameter sends a map where the key is groups and the value is the list of group names that the authenticated user belongs to. In the case of our user linda, this will be ROLE_USER. In the case of our user louise, this will be ROLE_USER and ROLE_BETA_TESTER.

The last line of the controller method now returns the home-beta template if the result of getTreatment is on and the home template otherwise.

Fire up the app and try to login as linda. You should see the same home template as before. Now, logout and login as louise. You should see the new beta template.

Make Your New Functionality Generally Available

Now that we have different treatments of our home template for regular users and beta testers, you may be wondering how to go about making the beta template available to everyone?

Split makes it easy-peasy. Go back to your Split definition for Spring_Security_App and switch the serve setting in the Set The Default Rule section from off to on. Save and Confirm the change.

Without restarting the Spring Boot application, logout and login as linda again. You should now see the same page that you saw for louise earlier.

Pretty cool, eh?

How to Repeat the Beta / Release Cycle

With this architecture in place, it’s now very easy to set up a new beta cycle. The steps would be something like this:

  1. Copy the home-beta.html template to home.html (now that it’s ready for production)
  2. Create a new home-beta.html template
  3. Set the default rule back to off in Split
  4. Redeploy the app
  5. Let your beta testers test the new experience
  6. When ready, set the default rule back to on in Split
  7. NO NEED TO REDEPLOY

You could do this over and over again and never touch the controller code. The only thing that’s changing are the templates and the settings in split.

This approach also lends itself to changing who is in your beta test program without having to change your code.

In a real application, you’d be working with a database or an Identity Management system where you could add and remove users to the BETA_TESTERS group. Those users would always see the latest and greatest beta while ordinary users would see only the current release.

I hope you’ve seen how useful it can be to set up different experiences for different users using Split and the native functionality built into Spring Security.

Learn More About Spring, Spring Security, and Testing in Production

If all this post did was wet your whistle, I’ve got good news… between the security experts at Okta and the testing aficionados at Split, we have you covered. Here are some other posts to check out:

You can find me talking about Java and security topics on Twitter @afitnerd and blogging on Okta’s developer blog. You can follow Split on YouTube, Twitter, and LinkedIn for more on progressive delivery, experimentation, and testing in production.