Skip to main content

Spring Boot Adapter

The rampart-spring-boot-starter provides auto-configuration for integrating Rampart with Spring Boot applications. It builds on Spring Security's OAuth2 Resource Server support, adding Rampart-specific claim mapping, role extraction, and multi-tenancy support.

Installation

Maven

<dependency>
<groupId>com.rampart</groupId>
<artifactId>rampart-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>

Gradle

implementation 'com.rampart:rampart-spring-boot-starter:0.1.0'

The starter brings in spring-boot-starter-oauth2-resource-server and spring-boot-starter-security transitively.

Quick Start

1. Configure application.yml

rampart:
issuer-url: https://auth.example.com
audience: my-api
realm: default

This is all you need. The starter auto-discovers the OIDC endpoints via {issuer-url}/.well-known/openid-configuration and configures JWKS-based JWT verification.

2. Create a Security Configuration

package com.example.config;

import com.rampart.spring.RampartSecurityConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
RampartSecurityConfigurer rampart
) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/health", "/actuator/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
.with(rampart, configurer -> {});

return http.build();
}
}

3. Create a Controller

package com.example.controller;

import com.rampart.spring.RampartUser;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class ProfileController {

@GetMapping("/api/profile")
public Map<String, Object> profile(@AuthenticationPrincipal RampartUser user) {
return Map.of(
"userId", user.getId(),
"email", user.getEmail(),
"name", user.getName(),
"roles", user.getRoles()
);
}
}

Configuration Reference

application.yml

rampart:
# Required
issuer-url: https://auth.example.com
audience: my-api

# Optional
realm: default # Organization/realm
clock-tolerance: 5s # Clock skew tolerance (default: 5s)
jwks-cache-ttl: 10m # JWKS cache duration (default: 10m)
required-claims: # Claims that must be present
- email
role-claim: roles # JWT claim containing roles (default: roles)
role-prefix: ROLE_ # Spring Security role prefix (default: ROLE_)

Environment Variables

All properties can be set via environment variables:

RAMPART_ISSUER_URL=https://auth.example.com
RAMPART_AUDIENCE=my-api
RAMPART_REALM=default

SecurityFilterChain Configuration

Basic Setup

@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
RampartSecurityConfigurer rampart
) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/health").permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()
)
.with(rampart, configurer -> {});

return http.build();
}

With CORS

@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
RampartSecurityConfigurer rampart
) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/health").permitAll()
.requestMatchers("/api/**").authenticated()
)
.with(rampart, configurer -> {});

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:5173"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}

Role-Based Access in SecurityFilterChain

http.authorizeHttpRequests(auth -> auth
.requestMatchers("/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/billing/**").hasAnyRole("ADMIN", "BILLING")
.requestMatchers(HttpMethod.GET, "/api/**").authenticated()
.requestMatchers(HttpMethod.POST, "/api/**").hasAuthority("SCOPE_write")
.anyRequest().denyAll()
);

Method-Level Security with @PreAuthorize

Enable method security with @EnableMethodSecurity on your configuration class.

Role Checks

@RestController
@RequestMapping("/api/admin")
public class AdminController {

@GetMapping("/stats")
@PreAuthorize("hasRole('ADMIN')")
public Map<String, Object> getStats() {
return Map.of(
"totalUsers", 1234,
"activeToday", 567
);
}

@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('SUPER_ADMIN')")
public Map<String, String> deleteUser(@PathVariable String id) {
return Map.of("deleted", id);
}

@GetMapping("/reports")
@PreAuthorize("hasAnyRole('ADMIN', 'ANALYST')")
public Map<String, Object> getReports() {
return Map.of("reports", List.of());
}
}

Scope Checks

@PostMapping("/api/emails/send")
@PreAuthorize("hasAuthority('SCOPE_email:send')")
public Map<String, Boolean> sendEmail() {
return Map.of("sent", true);
}

Custom SpEL Expressions

// Only allow users to access their own data
@GetMapping("/api/users/{userId}/tasks")
@PreAuthorize("#userId == authentication.principal.id")
public List<Task> getUserTasks(@PathVariable String userId) {
return taskService.findByUserId(userId);
}

// Combine role and ownership checks
@PutMapping("/api/tasks/{taskId}")
@PreAuthorize("hasRole('ADMIN') or @taskService.isOwner(#taskId, authentication.principal.id)")
public Task updateTask(@PathVariable String taskId, @RequestBody TaskUpdate update) {
return taskService.update(taskId, update);
}

RampartUser Principal

The RampartUser object is available as the @AuthenticationPrincipal in any controller method:

public class RampartUser {
public String getId(); // sub claim
public String getEmail(); // email claim
public String getName(); // name claim
public List<String> getRoles(); // roles claim
public String getScope(); // scope claim
public String getOrgId(); // org_id claim
public String getIssuer(); // iss claim
public String getAudience(); // aud claim

public boolean hasRole(String role);
public boolean hasAnyRole(String... roles);
public boolean hasScope(String scope);
public boolean hasAllScopes(String... scopes);
}

Full Working Example

application.yml

server:
port: 8080

rampart:
issuer-url: ${RAMPART_URL:https://auth.example.com}
audience: ${RAMPART_CLIENT_ID:task-api}
realm: default

logging:
level:
com.rampart: DEBUG

Security Configuration

package com.example.taskapi.config;

import com.rampart.spring.RampartSecurityConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
RampartSecurityConfigurer rampart
) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/health", "/actuator/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/tasks").hasAuthority("SCOPE_tasks:write")
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()
)
.with(rampart, configurer -> {});

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:5173"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}

Task Controller

package com.example.taskapi.controller;

import com.rampart.spring.RampartUser;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@RestController
public class TaskController {

private final Map<String, List<Map<String, String>>> tasks = new ConcurrentHashMap<>();

@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "ok");
}

@GetMapping("/api/tasks")
public Map<String, Object> listTasks(@AuthenticationPrincipal RampartUser user) {
List<Map<String, String>> userTasks = tasks.getOrDefault(user.getId(), List.of());
return Map.of("tasks", userTasks);
}

@PostMapping("/api/tasks")
@ResponseStatus(HttpStatus.CREATED)
public Map<String, String> createTask(
@AuthenticationPrincipal RampartUser user,
@RequestBody Map<String, String> body
) {
tasks.computeIfAbsent(user.getId(), k -> new ArrayList<>());

Map<String, String> task = Map.of(
"id", UUID.randomUUID().toString(),
"title", body.get("title"),
"assignee", user.getId()
);

tasks.get(user.getId()).add(task);
return task;
}

@GetMapping("/api/admin/stats")
@PreAuthorize("hasRole('ADMIN')")
public Map<String, Object> adminStats() {
long totalTasks = tasks.values().stream().mapToLong(List::size).sum();
return Map.of(
"totalUsers", tasks.size(),
"totalTasks", totalTasks
);
}

@GetMapping("/api/admin/tasks")
@PreAuthorize("hasRole('ADMIN')")
public Map<String, Object> adminListTasks() {
List<Map<String, String>> allTasks = tasks.values().stream()
.flatMap(List::stream)
.toList();
return Map.of("tasks", allTasks, "total", allTasks.size());
}
}

Application Entry Point

package com.example.taskapi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TaskApiApplication {
public static void main(String[] args) {
SpringApplication.run(TaskApiApplication.class, args);
}
}

pom.xml Dependencies

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.rampart</groupId>
<artifactId>rampart-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>

Run with:

RAMPART_URL=https://auth.example.com RAMPART_CLIENT_ID=task-api \
mvn spring-boot:run

Using Spring Security Without the Starter

If you prefer to use Spring Security's built-in OAuth2 Resource Server support directly (without the Rampart starter), configure it manually:

application.yml

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com
jwk-set-uri: https://auth.example.com/.well-known/jwks.json
audiences: my-api

Custom JWT Converter

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/health").permitAll()
.requestMatchers("/api/**").authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(rampartJwtConverter()))
);

return http.build();
}

@Bean
public JwtAuthenticationConverter rampartJwtConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthoritiesClaimName("roles");
authoritiesConverter.setAuthorityPrefix("ROLE_");

JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}

This approach uses only standard Spring dependencies and works with any OIDC-compliant provider, including Rampart.

Testing

Mock the RampartUser in Tests

@WebMvcTest(TaskController.class)
class TaskControllerTest {

@Autowired
private MockMvc mockMvc;

@Test
@WithMockRampartUser(id = "user-1", email = "test@example.com", roles = {"USER"})
void shouldReturnProfile() throws Exception {
mockMvc.perform(get("/api/profile"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.userId").value("user-1"))
.andExpect(jsonPath("$.email").value("test@example.com"));
}

@Test
@WithMockRampartUser(id = "user-1", roles = {"USER"})
void shouldDenyAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/stats"))
.andExpect(status().isForbidden());
}

@Test
@WithMockRampartUser(id = "admin-1", roles = {"ADMIN"})
void shouldAllowAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/stats"))
.andExpect(status().isOk());
}

@Test
void shouldRejectUnauthenticated() throws Exception {
mockMvc.perform(get("/api/profile"))
.andExpect(status().isUnauthorized());
}
}

The @WithMockRampartUser annotation is provided by the starter for test support. Add the test dependency:

<dependency>
<groupId>com.rampart</groupId>
<artifactId>rampart-spring-boot-starter-test</artifactId>
<version>0.1.0</version>
<scope>test</scope>
</dependency>