security

Unlocking Security: A Deep Dive into the Local Authorization Server

OpenID Connect (OIDC) is the most widely used framework for enabling Single Sign-On (SSO) for web client applications. It is built on top of the OAuth2 framework, which offers authorization capabilities through “access tokens.” OIDC and leading service providers support OAuth2 and identity management vendors.

Spring supports OIDC and OAuth2 through Spring Security and Spring Boot auto-configuration. Spring developers can quickly start and add support for a third-party identity provider, e.g., their company’s centralized SSO system.

Those SSO systems are typically managed centrally and often require opening tickets to gain access. Even when self-service, they often require specialized knowledge to configure and manage. This can be cumbersome for the “inner loop” experience, where developers work on their machines, or when running integration tests in CI. In those scenarios, working with a centralized system often requires figuring out many details: how are credentials managed? How are environments provisioned and configured? How to simulate the prod environment with high enough fidelity?

Often, developers try to work out a standalone solution that they could run on their machine. The first option is to use a “packaged” product, which can be heavyweight and hard to configure because it is designed for production use. On the other end of the spectrum, some products are too lightweight to support production-like use cases. Developers can also “write their own”, as a last resort, using open-source libraries like Spring Authorization Server – but this is yet more code the team will have to maintain over time.

Introducing: Local Authorization Server

We know the pain points – we have felt them too! And we’ve come up with a solution: Tanzu Local Authorization Server. It offers a simple, lightweight solution for running an OAuth2 Authorization Server / OpenID Connect Provider locally, packaged as a standalone jar file. Easy to run, with useful defaults, and focused on local development, it does not come with all the trappings of a full-blown production-grade product. This relaxes many constraints, allowing for simple configuration, and just enough features to mint id_tokens and access_tokens that look like production tokens.

Created with Spring Developers in mind, it makes the onboard experience simple – copy and paste the default configuration in a Boot project’s properties file, and the app is good to go. It integrates seamlessly with Testcontainers for integration tests, or with the experimental Spring Boot Testjars project.

Under the hood, it’s still “just” an OIDC Provider and OAuth2 Auth Server, compatible with every language and most OIDC/OAuth2 libraries, and is not limited to Spring Boot.

Getting started

Tanzu Spring Enterprise customers can obtain the latest release on artifactory. The full list of features and the API can be found on the official documentation. To get started, simply run the jar with Java 17 or higher:

java -jar tanzu-local-authorization-server-<VERSION>.jar

Notice that, in the command line output, the default user info is printed:

🧑 You can log in with the following users:
---
username: user
password: password

As well as the default Spring Boot configuration that can be used in any Spring Boot project:

spring:  
  security:
    oauth2:
      client:
        registration:
          tanzu-local-authorization-server:
            client-id: default-client-id
            client-secret: default-client-secret
            client-name: Tanzu Local Authorization Server
            scope:
              - openid
              - email
              - profile
        provider:
          tanzu-local-authorization-server:
            issuer-uri: http://localhost:9000

Using Local Authorization Server in a Boot project

Local AuthServer is here to help connect an existing Spring project to an OIDC/OAuth2 provider. To demonstrate the use of Local AuthSever, there needs to be a “Client” Boot application, in the sense of an OIDC/OAuth2 client. A good place to start is to clone the existing sample from Github. It is also possible to bootstrap a project from Spring Initializr: the required dependencies are Spring Web and OAuth2 Client.

Starting from Spring Initializr produces the following build.gradle:

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.4'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

From there, simply add an application.yml file in src/main/resources, and add the config that was copied from the Local Authorization Server in the section above:

spring:
  security:
    oauth2:
      client:
        registration:
          tanzu-local-authorization-server:
            client-id: default-client-id
            client-secret: default-client-secret
            client-name: Tanzu Local Authorization Server
            scope:
              - openid
              - email
              - profile
        provider:
          tanzu-local-authorization-server:
            issuer-uri: http://localhost:9000

There needs to be at least a simple “landing page” so that the application shows content. Create a simple @Controller class, for example DemoController.java:

@RestController
class DemoController {

    @GetMapping("/")
    String index(@AuthenticationPrincipal OidcUser user) {
        return "Hello " + user.getEmail();
    }

}

The application can be run with ./gradlew bootRun, and should be available on http://localhost:8080 . Visiting this page will redirect visitors to the Auth Server, and they can log in using the credentials from the section above, user and password, and see a message on the website: Hello [email protected].

The client application is successfully connected to the SSO system.

Advanced use-case: Roles-Based Access Control (RBAC)

In real-world use cases, requirements often include authorization based on information about the logged-in user, for example using their roles or attributes. Authorizations can be implemented in many ways in Spring Security. In this example, provide a OidcUserService bean to map an id_token claim to user “roles”. These roles can then be used to lock down or open up access. For fine-grained configuration, provide a SecurityFilterChain bean:

@Configuration
class DemoConfiguration {
    @Bean
    OidcUserService oidcUserService() {
        var oidcUserService = new OidcUserService();
        oidcUserService.setOidcUserMapper((oidcUserRequest, oidcUserInfo) -> {
            // Will map the "roles" claim from the `id_token` into user authorities (roles)
            var roles = oidcUserRequest.getIdToken().getClaimAsStringList("roles");
            var authorities = AuthorityUtils.createAuthorityList();
            if (roles != null) {
                roles.stream()
                        .map(r -> "ROLE_" + r)
                        .map(SimpleGrantedAuthority::new)
                        .forEach(authorities::add);
            }
            return new DefaultOidcUser(authorities, oidcUserRequest.getIdToken(), oidcUserInfo);
        });
        return oidcUserService;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(auth -> {
                    auth.requestMatchers("/admin/**").hasRole("admin");
                    auth.anyRequest().authenticated();
                })
                .oauth2Login(Customizer.withDefaults())
                .build();
    }
}

Add an /admin endpoint that can be consumed in the controller:

@RestController
class DemoController {

    @GetMapping("/")
    String index(@AuthenticationPrincipal OidcUser user) {
        return "Hello " + user.getEmail();
    }

    @GetMapping("/admin")
    String admin() {
        return "Hello, admin!";
    }

}

Configure Local Authorization Server, with two users: alice, who has an admin role granting her privileges in the client app, and bob with a regular, non-admin account:

tanzu:
  local-authorization-server:
    users:
      - username: alice
        password: alice-password
        attributes:
          # email is a standard OpenID claim, obtained with the email scope
          email: "[email protected]"
          # roles is a custom, application-specific claim
          roles:
            - viewer
            - editor
            - admin
      - username: bob
        password: bob-password
        attributes:
          email: [email protected]
          roles:
            - viewer
            - editor

Run the Auth Server using this configuration file:

java -jar tanzu-local-authorization-server-<VERSION>.jar --config=config.yml

Now access your client application’s newly created admin page at http://localhost:8080/admin : only alice should be able to access the page. Use incognito mode in your browser so switching users is easy and does not require implementing logout.

Testing: using with Testcontainers

To test the application logic against a running OIDC identity provider, the tests must run a web browser. One option is Selenium driving a real browser. However, Spring Boot also provides a more lightweight alternative, HtmlUnit, which emulates a real web browser in Java. 

The goal for these tests is to have a “real” user interaction, follow redirects, and ensure that our app is correctly wired to the AuthServer, gets and parses tokens, etc. We want to test our app with  Local Authorization Server, which we will package inside a container.

Start by importing the testcontainers dependencies in your build.gradle.kts file, as well as HtmlUnit:

testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("net.sourceforge.htmlunit:htmlunit")

Copy your local auth server to the appropriate directory, here test/resources/tanzu-local-authorization-server/authserver.jar, for example by running:

cp tanzu-local-authorization-server-*.jar test/resources/tanzu-local-authorization-server/authserver.jar

Configure Testcontainers tests, as in the sample TestcontainersLoginTests test file:

import java.io.IOException;

import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlButton;
import com.gargoylesoftware.htmlunit.html.HtmlInput;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.MountableFile;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationtests {

    @TestConfiguration
    static class TestcontainersConfiguration {
        @Bean
        @DynamicPropertySource
        GenericContainer<?> genericContainer(DynamicPropertyRegistry registry) {
            var tanzuAuthServer = new GenericContainer<>("bellsoft/liberica-openjre-alpine:21")
                    .withCopyFileToContainer(
                            // Point Testcontainers to the Tanzu-Local-Authorization-Server release
                            MountableFile.forClasspathResource("tanzu-local-authorization-server/authserver.jar"),
                            "/tanzu-local-authorization-server.jar")
                    .withCopyFileToContainer(MountableFile.forClasspathResource("tanzu-local-authorization-server/config.yml"), "/config.yml")
                    .withCommand("java", "-jar", "/tanzu-local-authorization-server.jar", "--config", "/config.yml")
                    .withExposedPorts(9000);

            registry.add(
                    "spring.security.oauth2.client.provider.tanzu-local-authorization-server.issuer-uri",
                    () -> "http://localhost:" + tanzuAuthServer.getFirstMappedPort()
            );
            return tanzuAuthServer;
        }
    }


    @Test
    void someTest() throws IOException {
        // ...
    }


}

From local development to Tanzu Platform

Local Authorization Server is built with the same technologies that underpin the Tanzu Platform bundled authorization server. It provides a powerful yet straightforward solution for local development environments and allows teams to seamlessly transition from local development to their production Tanzu Platform environments. The standalone jar format enables exceptional ease of use and portability, fitting naturally into CI pipelines and local setups, bridging inner and outer development loops. Developers can closely replicate production behaviors and obtain realistic ID and access tokens, significantly enhancing test reliability and minimizing discrepancies between development and production environments. With native compatibility across numerous languages and integration with powerful tools like Testcontainers, the Local Authorization Server enables an agile, inclusive development process, empowering teams to focus on innovation and building robust applications.

Give it a try

Head to the official documentation to learn more. Start using Local Authorization Server today to assess its suitability for various testing scenarios. We welcome your feedback!