Skip to main content

How to secure REST services

Introduction

In this section, we will explain how Atoti endpoints are secured and how you can customize your security for endpoints.

Secure Custom REST Services

You can secure REST services by using a number of DSLs provided by Atoti starters that may be extended or overwritten to implement your own security logic. Those DSLs are presented here. Both the underlying Atoti Server of your session and your own endpoints use these DSLs.

Working with a custom DSL

Custom DSLs and the provided DSLs can be used to secure REST services in three ways.

  • The provided DSLs can be applied.
  • The provided DSLs can be applied with added context.
  • A custom DSL can be applied to override the provided DSLs.

Apply a provided DSL

@Bean
@Order(5)
public SecurityFilterChain swaggerSecurityFilterChain(
final HttpSecurity http, final HumanToMachineSecurityDsl dsl) throws Exception {
return http.with(dsl, Customizer.withDefaults())
.securityMatcher("/swagger.html")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.build();
}

This code defines a Spring SecurityFilterChain for the swagger endpoint. It uses the HumanToMachineSecurityDsl provided by default by Atoti starters and requires that the user is authenticated, any unauthenticated request will be redirected to the login page specified in the DSL default configuration.

Add context to a provided DSL

@Bean
@Order(6)
public SecurityFilterChain dataExportSecurityFilterChain(
final HttpSecurity http, final MachineToMachineSecurityDsl dsl) throws Exception {
http.with(
dsl,
d ->
d.setScope(
Set.of(
MachineToMachineSecurityDsl.Scope.SECURITY,
MachineToMachineSecurityDsl.Scope.CONTEXT)));
http.securityMatcher("/cube/data-export/download")
.authorizeHttpRequests(
auth ->
auth.requestMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated());
return http.build();
}

This code defines a Spring SecurityFilterChain for the data export endpoint. It configures the endpoint to allow all HTTP OPTIONS requests, and restricts all other requests to users with the USER or ADMIN roles, applying security and context scopes (see note below).

note

In the case of MachineToMachineSecurityDsl, three scopes exist:

  • MachineToMachineSecurityDsl.Scope.SECURITY: Used for endpoints requiring authentication.
  • MachineToMachineSecurityDsl.Scope.CONTEXT: Used for endpoints requiring the user context values (e.g. user restrictions).
  • MachineToMachineSecurityDsl.Scope.OBSERVABILITY: Used for public endpoints providing public information.

Using a custom DSL overriding a provided DSL

If you want to implement a custom authentication or authorization logic (e.g. using a custom authentication filter), you need to do the following:

  1. Create the classes that are used to implement the custom authentication logic.
Custom filter

/** Filter based on a fixed token. */
public class ApiTokenFilter extends OncePerRequestFilter {
private static final String BEARER_TYPE = "API-TOKEN";
@Nullable private final String expectedAuthorizationHeader;
private final SecurityContextRepository securityContextRepository =
new RequestAttributeSecurityContextRepository();
private final SecurityContextHolderStrategy securityContextHolderStrategy =
SecurityContextHolder.getContextHolderStrategy();
/** Creates a new filter. If {@code expectedToken} is {@code null}, the filter has no effect. */
public ApiTokenFilter(@Nullable final String expectedToken) {
this.expectedAuthorizationHeader =
expectedToken != null ? toAuthorizationHeader(expectedToken) : null;
}
public static String toAuthorizationHeader(final String token) {
return BEARER_TYPE + ' ' + Objects.requireNonNull(token);
}
@Override
protected void doFilterInternal(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (this.expectedAuthorizationHeader != null
&& this.expectedAuthorizationHeader.equals(authorizationHeader)) {
final Authentication auth =
UsernamePasswordAuthenticationToken.authenticated(
"API-Admin", null, List.of(new SimpleGrantedAuthority("ROLE_ADMIN")));
final SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(auth);
this.securityContextHolderStrategy.setContext(context);
this.securityContextRepository.saveContext(
SecurityContextHolder.getContext(), request, response);
}
filterChain.doFilter(request, response);
}
}

  1. Create a custom DSL that extends the base DSLs and overrides the existing methods.

The example below shows how to:

  • Create a new filtering logic that requires an API token to be present in the request header.
  • Disable CSRF and enable CORS.
  • Configure security to return a 401 Unauthorized status whenever an unauthenticated user tries to access a protected resource.

/**
* A base implementation of {@link MachineToMachineSecurityDsl} with opinionated defaults for
* Atoti Runtime.
*/
@RequiredArgsConstructor
public class CustomAtotiRuntimeMachineDsl extends MachineToMachineSecurityDsl {
private Set<Scope> scopes = Set.of(Scope.values());
private final ApiTokenFilter apiTokenFilter;
@Override
public MachineToMachineSecurityDsl setScope(final Set<Scope> scopes) {
this.scopes = scopes;
return this;
}
@Override
public final void init(final HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults()); // Disable CSRF and enable CORS.
http.addFilterAfter(
apiTokenFilter, SecurityContextHolderFilter.class); // Add custom API token filter.
http.exceptionHandling(
ex ->
ex.authenticationEntryPoint(
new HttpStatusEntryPoint(
HttpStatus.UNAUTHORIZED))); // Handle authentication errors.
}
}

  1. Register the custom DSL in the Spring context, and using it to secure some endpoints.

/** Spring configuration for custom security settings in Atoti Runtime. */
@Configuration
public class CustomSecurityConfiguration {
@Bean
@Primary // We specify this bean as primary to override the pre-existing
// MachineToMachineSecurityDsl beans.
public MachineToMachineSecurityDsl customAtotiRuntimeMachineDsl(
final ApiTokenFilter apiTokenFilter) {
return new CustomAtotiRuntimeMachineDsl(apiTokenFilter);
}
@Bean
@Order(1)
public SecurityFilterChain customSecurityFilterChain(
final HttpSecurity http, final MachineToMachineSecurityDsl dsl) throws Exception {
http.with(dsl, Customizer.withDefaults()); // Apply the custom MachineToMachineSecurityDsl.
http.securityMatcher("/api/**") // Match the security filter chain to the API endpoints.
.authorizeHttpRequests(
auth ->
auth.requestMatchers(HttpMethod.OPTIONS)
.permitAll()
.anyRequest()
.authenticated()); // Require authentication for all requests except OPTIONS
// requests.
return http.build(); // Build the security filter chain.
}
}

This code sets up a SecurityFilterChain that applies our custom DSL, matches only /api/** endpoints, permits all HTTP OPTIONS requests, and requires authentication (through our ApiTokenFilter) for all other API requests.

note

By adding @Primary to the @Bean annotation, we ensure that this custom DSL configuration takes precedence over any other security configurations in the application context, meaning that endpoints provided by Atoti using the MachineToMachineSecurityDsl will use this custom DSL.