icon-arrow icon-check icon-mail icon-phone icon-facebook icon-linkedin icon-youtube icon-twitter icon-cheveron icon-download icon-instagram play close icon-arrow-uturn icon-calendar icon-clock icon-search icon-chevron-process icon-skills icon-knowledge icon-kite icon-education icon-languages icon-tools icon-experience
Werken bij Whitehorses
Blog 30/07/2021

Using Spring Security to secure Apache Camel REST services

Access denied

When developing REST APIs, you often need to secure (some of) its methods. Maybe some methods will be made publicly available, some may require users to be authenticated, and others may even require certain authorization roles.

In this blog post we will have a look how Spring Security can be used to secure your Apache Camel REST APIs.

Integratiespecialist
Mike Heeren /
Integratiespecialist

Setting up a new Apache Camel project

To start we will generate a new Apache Camel/Spring Boot project. We will do this by generating a new project using the latest version (at the moment of writing, this is version 3.11.0) of the camel-archetype-spring-boot archetype.

Next, we will add the camel-servlet-starter dependency so we can setup the APIs using the Camel REST DSL.

<dependency>
    <groupId>org.apache.camel.springboot</groupId>
    <artifactId>camel-servlet-starter</artifactId>
</dependency>

Now we are able to specify a RouteBuilder and specify our REST API. For this example we will add 3 methods:

  • The public method will be available for everyone.
  • The secured method will only be available for authenticated users.
  • The admin method will only be available for users authorized as admin.
package nl.mikeheeren.camel.rest;

import org.apache.camel.builder.RouteBuilder;
import org.springframework.stereotype.Component;

@Component
public class RestRouteBuilder extends RouteBuilder {

    @Override
    public void configure() {
        rest()
            .get("/public")
                .description("The public API is available for everyone.")
                .route().routeId("public")
                    .setBody(constant("Hello world!"))
                .endRest()
            .get("/secured")
                .description("The secured API is only accessible for authenticated users.")
                .route().routeId("secured")
                    .setBody(constant("Hello user!"))
                .endRest()
            .get("/secured/admin")
                .description("The admin part of the API requires the user to be authorized with the ADMIN role.")
                .route().routeId("admin")
                    .setBody(constant("Hello admin!"))
                .endRest();
    }

}

Because we didn’t override any of the default configuration, the methods can be called via the following URLs:

Besides these custom methods, we also have the Spring Boot Actuator endpoint available:

Configuring Spring Security

Now that the REST API skeleton has been setup, it’s time to start actually securing the endpoints. First, we add the following dependency to our project:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Because we want to implement custom security logic, we add the following exclusions to the @SpringBootApplication annotation:

@SpringBootApplication(exclude = { SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class })

To implement the custom security logic, we extends the WebSecurityConfigurerAdapter class and annotate it with @Configuration and @EnableWebSecurity.

Because we don’t want Spring Security to generate a default user, we override the userDetailsService() method. In our example we will add 2 dummy users named “user” and “admin”.

Important: Don’t forget to also register the outcome of this method as a @Bean. Otherwise the users specified here are still not taken into account and Spring Security will still generate a default user!

The next step is to override the configure method. Here we specify the different authentication and authorization levels that are required for certain endpoints. Basically, what we configure in the below example is that all API calls should be authenticated, except:

  • Calls to the public method don’t need any authentication.
  • Calls to Actuator endpoint (and all deeper paths) explicitly require the ADMIN role.

Note: Here we also configure that we expect basic authentication headers to be used, and that we will not use any session cookies. So every separate request that is received by the API, will be (re)authenticated.

package nl.mikeheeren.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    public static final String ADMIN = "ADMIN";

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        return new InMemoryUserDetailsManager(            
            User.withUsername("admin").password(passwordEncoder.encode("s3cret1")).roles(ADMIN).build(),               
            User.withUsername("user").password(passwordEncoder.encode("s3cret2")).roles().build()
        );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/camel/public")
                    .permitAll()
                .antMatchers("/actuator/**")
                    .hasRole(ADMIN)
                .anyRequest()
                    .authenticated()
            .and().httpBasic()
            .and().sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

}

 

Custom Spring Security policies in Camel routes

With the above configuration, we basically have all desired security in place, except for the check for the admin role to be present when calling the admin method. Of course, this could easily be implemented by adding this endpoint to the antMatchers method that also checks the Actuator endpoint. However, in some cases it could be useful to do these kind of checks within the Camel route definition. So we will use a different approach to achieve this.

First we add another dependency to our project:

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-spring-security</artifactId>
</dependency>

Next, we need an instance of the AuthenticationManager to be available as a bean. Luckily, the WebSecurityConfigurerAdapter class already creates an instance for us. So the only thing we have to do, is to fetch this created instance (by extending the authenticationManager() method) and exposing it as a @Bean:

@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
}

Now that we have the AuthenticationManager available, we can also create a SpringSecurityAuthorizationPolicy. In our case, we want to check the role of an authenticated user, so we will use a RoleVoter in the policy decision manager:

@Bean
public Policy adminPolicy(AuthenticationManager authenticationManager) {
    RoleVoter roleVoter = new RoleVoter();
    SpringSecurityAuthorizationPolicy policy = new SpringSecurityAuthorizationPolicy();
    policy.setAuthenticationManager(authenticationManager);
    policy.setAccessDecisionManager(new UnanimousBased(List.of(roleVoter)));
    policy.setSpringSecurityAccessPolicy(new SpringSecurityAccessPolicy(roleVoter.getRolePrefix() + SecurityConfiguration.ADMIN));
    return policy;
}

The final step is to make the route actually use this policy. This can easily be done by adding the policy step to the Camel route:

.get("/secured/admin")
    .description("The admin part of the API requires the user to be authorized with the ADMIN role.")
    .route().routeId("admin")
        .policy("adminPolicy")
        .setBody(constant("Hello admin!"))
    .endRest();

A full example can be found on Bitbucket: https://bitbucket.org/whitehorsesbv/apache-camel-spring-security/

Summary

Spring Security can easily be setup to secure Camel REST APIs. All security logic on URL level can be configured by extending  the WebSecurityConfigurerAdapter class and overriding the configure method.

When you want even more control over the security context from within a Camel route, you can also implement SpringSecurityAuthorizationPolicy instances, and applying the created policy on the route.

Good luck securing your API methods!

Geen reacties

Geef jouw mening

Reactie plaatsen

Reactie toevoegen

Jouw e-mailadres wordt niet openbaar gemaakt.

Geen HTML

  • Geen HTML toegestaan.
  • Regels en alinea's worden automatisch gesplitst.
  • Web- en e-mailadressen worden automatisch naar links omgezet.
Integratiespecialist
Mike Heeren /
Integratiespecialist

Wil je deel uitmaken van een groep gedreven en ambitieuze integratiespecialisten? Stuur ons jouw cv!