Skip to content

Commit

Permalink
implement api-key-based auth for rest endpoints (#910)
Browse files Browse the repository at this point in the history
  • Loading branch information
goekay committed Sep 29, 2022
1 parent 3a9fd91 commit 637e7da
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 11 deletions.
4 changes: 2 additions & 2 deletions src/main/java/de/rwth/idsg/steve/SteveAppContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@ private WebAppContext initWebApp() {

ctx.addEventListener(new ContextLoaderListener(springContext));
ctx.addServlet(web, CONFIG.getSpringMapping());
ctx.addServlet(cxf, CONFIG.getCxfMapping());
ctx.addServlet(cxf, CONFIG.getCxfMapping() + "/*");

if (CONFIG.getProfile().isProd()) {
// If PROD, add security filter
ctx.addFilter(
// The bean name is not arbitrary, but is as expected by Spring
new FilterHolder(new DelegatingFilterProxy(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)),
CONFIG.getSpringManagerMapping(),
CONFIG.getSpringMapping() + "*",
EnumSet.allOf(DispatcherType.class)
);
}
Expand Down
18 changes: 16 additions & 2 deletions src/main/java/de/rwth/idsg/steve/SteveConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ public enum SteveConfiguration {
// Root mapping for Spring
private final String springMapping = "/";
// Web frontend
private final String springManagerMapping = "/manager/*";
private final String springManagerMapping = "/manager";
// Mapping for CXF SOAP services
private final String cxfMapping = "/services/*";
private final String cxfMapping = "/services";
// Mapping for Web APIs
private final String apiMapping = "/api";
// Dummy service path
private final String routerEndpointPath = "/CentralSystemService";
// Time zone for the application and database connections
Expand All @@ -55,6 +57,7 @@ public enum SteveConfiguration {
private final ApplicationProfile profile;
private final Ocpp ocpp;
private final Auth auth;
private final WebApi webApi;
private final DB db;
private final Jetty jetty;

Expand Down Expand Up @@ -94,6 +97,11 @@ public enum SteveConfiguration {
.encodedPassword(encoder.encode(p.getString("auth.password")))
.build();

webApi = WebApi.builder()
.headerKey(p.getOptionalString("webapi.key"))
.headerValue(p.getOptionalString("webapi.value"))
.build();

ocpp = Ocpp.builder()
.autoRegisterUnknownStations(p.getOptionalBoolean("auto.register.unknown.stations"))
.wsSessionSelectStrategy(
Expand Down Expand Up @@ -182,6 +190,12 @@ public static class Auth {
private final String encodedPassword;
}

@Builder @Getter
public static class WebApi {
private final String headerKey;
private final String headerValue;
}

// OCPP-related configuration
@Builder @Getter
public static class Ocpp {
Expand Down
128 changes: 121 additions & 7 deletions src/main/java/de/rwth/idsg/steve/config/SecurityConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,48 @@
*/
package de.rwth.idsg.steve.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import de.rwth.idsg.steve.SteveProdCondition;
import de.rwth.idsg.steve.web.api.ApiControllerAdvice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DisabledException;
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.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static de.rwth.idsg.steve.SteveConfiguration.CONFIG;

/**
* @author Sevket Goekay <sevketgokay@gmail.com>
* @since 07.01.2015
*/
@Slf4j
@Configuration
@EnableWebSecurity
@Conditional(SteveProdCondition.class)
Expand Down Expand Up @@ -67,26 +88,119 @@ public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(webPageUser);
}

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers(
"/static/**",
CONFIG.getCxfMapping() + "/**"
);
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
final String prefix = "/manager/";
final String prefix = CONFIG.getSpringManagerMapping();

return http
.authorizeHttpRequests(
req -> req
.antMatchers("/static/**").permitAll()
.antMatchers(prefix + "**").hasRole("ADMIN")
req -> req.antMatchers(prefix + "/**").hasRole("ADMIN")
)
.sessionManagement(
req -> req.invalidSessionUrl(prefix + "signin")
req -> req.invalidSessionUrl(prefix + "/signin")
)
.formLogin(
req -> req.loginPage(prefix + "signin").permitAll()
req -> req.loginPage(prefix + "/signin").permitAll()
)
.logout(
req -> req.logoutUrl(prefix + "signout")
req -> req.logoutUrl(prefix + "/signout")
)
.build();
}

@Bean
@Order(1)
public SecurityFilterChain apiKeyFilterChain(HttpSecurity http) throws Exception {
return http.antMatcher(CONFIG.getApiMapping() + "/**")
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(new ApiKeyFilter())
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(new ApiKeyAuthenticationEntryPoint())
.and()
.build();
}

/**
* Enable Web APIs only if both properties for API key are set. This has two consequences:
* 1) Backwards compatibility: Existing installations with older properties file, that does not include these two
* new keys, will not expose the APIs. Every call will be blocked by default.
* 2) If you want to expose your APIs, you MUST set these properties. This action activates authentication (i.e.
* APIs without authentication are not possible, and this is a good thing).
*/
public static class ApiKeyFilter extends AbstractPreAuthenticatedProcessingFilter implements AuthenticationManager {

private final String headerKey;
private final String headerValue;
private final boolean isApiEnabled;

public ApiKeyFilter() {
setAuthenticationManager(this);

headerKey = CONFIG.getWebApi().getHeaderKey();
headerValue = CONFIG.getWebApi().getHeaderValue();
isApiEnabled = !Strings.isNullOrEmpty(headerKey) && !Strings.isNullOrEmpty(headerValue);

if (!isApiEnabled) {
log.warn("Web APIs will not be exposed. Reason: 'webapi.key' and 'webapi.value' are not set in config file");
}
}

@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
if (!isApiEnabled) {
throw new DisabledException("Web APIs are not exposed");
}
return request.getHeader(headerKey);
}

@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return null;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!isApiEnabled) {
throw new DisabledException("Web APIs are not exposed");
}

String principal = (String) authentication.getPrincipal();
authentication.setAuthenticated(headerValue.equals(principal));
return authentication;
}
}

public static class ApiKeyAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper mapper = new ObjectMapper();

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
HttpStatus status = HttpStatus.UNAUTHORIZED;

var apiResponse = ApiControllerAdvice.createResponse(
request.getRequestURL().toString(),
status,
"Full authentication is required to access this resource"
);

response.setStatus(status.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().print(mapper.writeValueAsString(apiResponse));
}
}
}
6 changes: 6 additions & 0 deletions src/main/resources/config/dev/main.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ db.password = changeme
auth.user = admin
auth.password = 1234

# The header key and value for Web API access using API key authorization.
# Both must be set for Web APIs to be enabled. Otherwise, we will block all calls.
#
webapi.key = STEVE-API-KEY
webapi.value =

# Jetty configuration
#
server.host = 127.0.0.1
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config/docker/main.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ db.password = changeme
auth.user = admin
auth.password = 1234

# The header key and value for Web API access using API key authorization.
# Both must be set for Web APIs to be enabled. Otherwise, we will block all calls.
#
webapi.key = STEVE-API-KEY
webapi.value =

# Jetty configuration
#
server.host = 0.0.0.0
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config/kubernetes/main.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ db.password=$DB_PASSWORD
auth.user=$ADMIN_USERNAME
auth.password=$ADMIN_PASSWORD

# The header key and value for Web API access using API key authorization.
# Both must be set for Web APIs to be enabled. Otherwise, we will block all calls.
#
webapi.key = STEVE-API-KEY
webapi.value =

# Jetty configuration
#
server.host = 0.0.0.0
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config/prod/main.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ db.password = changeme
auth.user = admin
auth.password = 1234

# The header key and value for Web API access using API key authorization.
# Both must be set for Web APIs to be enabled. Otherwise, we will block all calls.
#
webapi.key = STEVE-API-KEY
webapi.value =

# Jetty configuration
#
server.host = 127.0.0.1
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/config/test/main.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ db.password = changeme
auth.user = admin
auth.password = 1234

# The header key and value for Web API access using API key authorization.
# Both must be set for Web APIs to be enabled. Otherwise, we will block all calls.
#
webapi.key = STEVE-API-KEY
webapi.value =

# Jetty configuration
#
server.host = 127.0.0.1
Expand Down

0 comments on commit 637e7da

Please sign in to comment.