How to secure REST services
Introduction
In this section, we will explain how Atoti endpoints are secured and how you can custom your security for those endpoints and your own.
Securing Custom REST Services
You can secure REST services by using a couple of DSLs provided by Atoti starters that may be extended or overwritten to implement your own security logic. Those DSLs are presented here and intended to be used by both the underlying Atoti Server of your session and your own endpoints.
Examples of a custom DSL
You can use the provided DSLs or your custom DSLs in the following ways, inside a configuration class:
Using the provided DSLs
@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.
Adding context to the provided DSLs
@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).
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 the provided DSLs
If you want to implement a custom authentication or authorization logic (e.g. using a custom authentication filter), you will need to do the following:
- Create the classes that will be used to implement your 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);
}
}
- Create a custom DSL that extends the base DSLs and overrides the methods you want to customize.
In this example, we create a new filtering logic needing an API token to be present in the request header. We also disable CSRF and enable CORS and we 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.
}
}
- 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.
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.