PII Data Protection using Spring Boot 3 + Spring Security
When developing enterprise applications there will come a time where certain PII data must be exposed only to privileged users and must be masked in a way to others.
This PII data can be Social Security Numbers, Addresses, Telephone numbers etc. Today I will show how to protect a tax id in a Customer API so that only privileged users can see the actual data and normal users will be seeing masked data.
For this demo I am using Spring Boot 3 for REST API and Spring Security for Authentication/Authorization. I am using custom Jackson Serialization capability along with a custom annotation to support this build.
Following is a domain object which is a Customer. This customer information can come from a Database, File or another REST API endpoint.
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Customer implements Serializable {
private String taxId;
private String firstName;
private String lastName;
public Customer() {}
public Customer(String taxId, String firstName, String lastName) {
this.taxId = taxId;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters/Setters
}
And we have our own REST API endpoint to support this Customer data.
@RestController
@RequestMapping("/api/1.0/customers")
public class CustomerController {
@GetMapping
public List<Customer> getCustomers() {
return Arrays.asList(new Customer("1234567890", "James", "Cordon"),
new Customer("0987654321", "Abraham", "Lincoln"));
}
}
Finally we have a Spring Security configuration with two users one with admin privilege and one with normal user privilege. Admin users should be able to see tax id without masking. We have configured basic authentication for demo purpose.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests().requestMatchers("/api/1.0/customers")
.authenticated()
.and()
.userDetailsService(users())
.httpBasic(withDefaults());
return http.build();
}
@Bean
public UserDetailsService users() {
// The builder will ensure the passwords are encoded before saving in memory
User.UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
Now the setup is done. Let’s see how to implement masking of PII data based on logged in user privilege. For this we need a new custom annotation to mark our PII data fields.
@JacksonAnnotationsInside
@JsonSerialize(using = ProtectDataSerializer.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ProtectData {
String[] allowedRoles() default {"ADMIN"};
}
By making use of this annotation we can write a custom Jackson Serializer. This serializer checks the role of the of logged in user before sending data to client.
public class ProtectDataSerializer extends StdSerializer<Object> implements ContextualSerializer {
private String[] allowedRoles;
public ProtectDataSerializer(String[] allowedRoles) {
this();
this.allowedRoles = allowedRoles;
}
public ProtectDataSerializer() {
super(Object.class);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Optional<ProtectData> annotation = Optional.ofNullable(property).map(prop -> prop.getAnnotation(ProtectData.class));
return new ProtectDataSerializer(annotation.map(ProtectData::allowedRoles).orElse(new String[] {"ADMIN"}));
}
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
String piiData = value.toString();
if (authentication != null) {
final List<String> allowedRolesList = Arrays.asList(this.allowedRoles);
long count = authentication.getAuthorities().stream().filter(ga -> allowedRolesList.contains(ga.getAuthority().substring(5))).count();
if (count == 0) {
piiData = piiData.replaceAll("\\w(?=\\w{4})", "x");
}
}
gen.writeString(piiData);
}
}
Now for the final step we need to annotation our PII field with our custom annotation.
@ProtectData
private String taxId;
That’s all that is required. Now when the customers endpoint is accessed by non admin user he will see the following with masked tax ids.
Also for an admin user the full PII data without any masking as below;
The complete source is available in Github.