Cleanup and improve the controller authentication (#2287)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-02-18 15:10:16 +02:00
committed by GitHub
parent cace8bd20e
commit 76ce1cf052
51 changed files with 942 additions and 1517 deletions

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.security.controller;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.eclipse.hawkbit.security.DdiSecurityProperties;
import org.eclipse.hawkbit.util.UrlUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* An abstraction for all controller based security to parse the e.g. the tenant name from the URL and the controller ID from the URL to do
* security checks based on this information.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class AuthenticationFilters {
public static class GatewayTokenAuthenticationFilter extends AbstractAuthenticationFilter {
public GatewayTokenAuthenticationFilter(final GatewayTokenAuthenticator authenticator, final DdiSecurityProperties ddiSecurityProperties) {
super(authenticator, ddiSecurityProperties);
}
}
public static class SecurityHeaderAuthenticationFilter extends AbstractAuthenticationFilter {
public SecurityHeaderAuthenticationFilter(final SecurityHeaderAuthenticator authenticator, final DdiSecurityProperties ddiSecurityProperties) {
super(authenticator, ddiSecurityProperties);
}
}
public static class SecurityTokenAuthenticationFilter extends AbstractAuthenticationFilter {
public SecurityTokenAuthenticationFilter(final SecurityTokenAuthenticator authenticator, final DdiSecurityProperties ddiSecurityProperties) {
super(authenticator, ddiSecurityProperties);
}
}
/**
* An abstraction for all controller based security to parse the e.g. the tenant name from the URL and the controller ID from the URL to do
* security checks based on this information.
*/
public static abstract class AbstractAuthenticationFilter extends OncePerRequestFilter {
private static final String TENANT_PLACE_HOLDER = "tenant";
private static final String CONTROLLER_ID_PLACE_HOLDER = "controllerId";
/**
* requestURIPathPattern the request URI path pattern in ANT style containing the placeholder key for retrieving the principal from the URI
* request. e.g."/{tenant}/controller/v1/{controllerId}
*/
private static final String CONTROLLER_REQUEST_ANT_PATTERN =
"/{" + TENANT_PLACE_HOLDER + "}/controller/v1/{" + CONTROLLER_ID_PLACE_HOLDER + "}/**";
private static final String CONTROLLER_DL_REQUEST_ANT_PATTERN =
"/{" + TENANT_PLACE_HOLDER + "}/controller/artifacts/v1/**";
private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
private final AntPathMatcher pathExtractor = new AntPathMatcher();
private final Authenticator authenticator;
private final List<String> authorizedSourceIps;
protected AbstractAuthenticationFilter(final Authenticator authenticator, final DdiSecurityProperties ddiSecurityProperties) {
this.authenticator = authenticator;
authorizedSourceIps = ddiSecurityProperties.getRp().getTrustedIPs();
}
@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain)
throws IOException, ServletException {
if (acceptIPAddress(request)) {
final Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication();
if (currentAuthentication == null || !currentAuthentication.isAuthenticated()) {
final ControllerSecurityToken securityToken = createTenantSecurityTokenVariables(request);
if (securityToken != null) {
final Authentication authentication = authenticator.authenticate(securityToken);
if (authentication != null) {
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication);
this.securityContextHolderStrategy.setContext(context);
}
}
} else {
authenticator.log().trace("Request is already authenticated. Skip filter");
}
}
chain.doFilter(request, response);
}
/**
* Extracts tenant and controllerId from the request URI as path variables.
*
* @param request the Http request to extract the path variables.
* @return the extracted {@link ControllerSecurityToken} or {@code null} if the request does not match the pattern and no variables could be
* extracted
*/
private ControllerSecurityToken createTenantSecurityTokenVariables(final HttpServletRequest request) {
final String requestURI = request.getRequestURI();
if (pathExtractor.match(request.getContextPath() + CONTROLLER_REQUEST_ANT_PATTERN, requestURI)) {
authenticator.log().debug("retrieving principal from URI request {}", requestURI);
final Map<String, String> extractUriTemplateVariables = pathExtractor
.extractUriTemplateVariables(request.getContextPath() + CONTROLLER_REQUEST_ANT_PATTERN, requestURI);
final String controllerId = UrlUtils.decodeUriValue(extractUriTemplateVariables.get(CONTROLLER_ID_PLACE_HOLDER));
final String tenant = UrlUtils.decodeUriValue(extractUriTemplateVariables.get(TENANT_PLACE_HOLDER));
authenticator.log().trace("Parsed tenant {} and controllerId {} from path request {}", tenant, controllerId, requestURI);
return createTenantSecurityTokenVariables(request, tenant, controllerId);
} else if (pathExtractor.match(request.getContextPath() + CONTROLLER_DL_REQUEST_ANT_PATTERN, requestURI)) {
authenticator.log().debug("retrieving path variables from URI request {}", requestURI);
final Map<String, String> extractUriTemplateVariables = pathExtractor.extractUriTemplateVariables(
request.getContextPath() + CONTROLLER_DL_REQUEST_ANT_PATTERN, requestURI);
final String tenant = UrlUtils.decodeUriValue(extractUriTemplateVariables.get(TENANT_PLACE_HOLDER));
authenticator.log().trace("Parsed tenant {} from path request {}", tenant, requestURI);
return createTenantSecurityTokenVariables(request, tenant, "anonymous");
} else {
authenticator.log().trace("request {} does not match the path pattern {}, request gets ignored", requestURI, CONTROLLER_REQUEST_ANT_PATTERN);
return null;
}
}
private ControllerSecurityToken createTenantSecurityTokenVariables(
final HttpServletRequest request, final String tenant, final String controllerId) {
final ControllerSecurityToken securityToken = new ControllerSecurityToken(tenant, null, controllerId, null);
Collections.list(request.getHeaderNames()).forEach(header -> securityToken.putHeader(header, request.getHeader(header)));
return securityToken;
}
private boolean acceptIPAddress(final HttpServletRequest request) {
if (authorizedSourceIps == null) {
// no trusted IP check, because no authorizedSourceIPs configuration
return true;
}
final String remoteAddress = request.getRemoteAddr();
if (authorizedSourceIps.contains(remoteAddress)) {
// source ip matches the given pattern -> authenticated
return true;
} else {
authenticator.log().debug(
"The remote source IP address {} is not in the list of trusted IP addresses {}", remoteAddress, authorizedSourceIps);
return false;
}
}
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.security.controller;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import lombok.EqualsAndHashCode;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
import org.slf4j.Logger;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
/**
* Interface for Authentication mechanism.
*/
public interface Authenticator {
/**
* If the authentication mechanism is not enabled for the tenant - it just returns null.
* If the authentication mechanism is supported, the filter extracts from the security token the related credentials,
* validate them (do authenticate the caller).
* If validation / authentication is successful returns an authenticated authentication object. Otherwise,
* throws BadCredentialsException.
*
* @param controllerSecurityToken the securityToken
* @return the extracted tenant and controller id
*/
Authentication authenticate(ControllerSecurityToken controllerSecurityToken);
Logger log();
abstract class AbstractAuthenticator implements Authenticator {
protected final TenantConfigurationManagement tenantConfigurationManagement;
protected final TenantAware tenantAware;
protected final SystemSecurityContext systemSecurityContext;
private final TenantAware.TenantRunner<Boolean> isEnabledTenantRunner;
protected AbstractAuthenticator(
final TenantConfigurationManagement tenantConfigurationManagement,
final TenantAware tenantAware, final SystemSecurityContext systemSecurityContext) {
this.tenantConfigurationManagement = tenantConfigurationManagement;
this.tenantAware = tenantAware;
this.systemSecurityContext = systemSecurityContext;
isEnabledTenantRunner = () -> systemSecurityContext.runAsSystem(
() -> tenantConfigurationManagement.getConfigurationValue(getTenantConfigurationKey(), Boolean.class).getValue());
}
protected boolean isEnabled(final ControllerSecurityToken securityToken) {
return tenantAware.runAsTenant(securityToken.getTenant(), isEnabledTenantRunner);
}
protected abstract String getTenantConfigurationKey();
protected Authentication authenticatedController(final String tenant, final String controllerId) {
Objects.requireNonNull(tenant, "tenant must not be null");
Objects.requireNonNull(controllerId, "controllerId must not be null");
return new AuthenticatedController(tenant, controllerId);
}
@EqualsAndHashCode(callSuper = true)
private static class AuthenticatedController extends AbstractAuthenticationToken {
private static final Collection<GrantedAuthority> CONTROLLER_AUTHORITY =
List.of(new SimpleGrantedAuthority(SpPermission.SpringEvalExpressions.CONTROLLER_ROLE));
private final String controllerId;
AuthenticatedController(final String tenant, final String controllerId) {
super(CONTROLLER_AUTHORITY);
super.setDetails(new TenantAwareAuthenticationDetails(tenant, true));
this.controllerId = controllerId;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return controllerId;
}
}
}
}

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.security.controller;
import java.util.Map;
import java.util.TreeMap;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* JSON representation to authenticate a tenant.
*/
@Data
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class ControllerSecurityToken {
public static final String AUTHORIZATION_HEADER = "Authorization";
@JsonProperty
private final Long tenantId;
@JsonProperty
private final Long targetId;
@JsonProperty
private final String controllerId;
@JsonProperty
private String tenant;
@JsonProperty
private Map<String, String> headers;
/**
* Constructor.
*
* @param tenant the tenant for the security token
* @param tenantId alternative tenant identification by technical ID
* @param controllerId the ID of the controller for the security token
* @param targetId alternative target identification by technical ID
*/
@JsonCreator
public ControllerSecurityToken(
@JsonProperty("tenant") final String tenant,
@JsonProperty("tenantId") final Long tenantId, @JsonProperty("controllerId") final String controllerId,
@JsonProperty("targetId") final Long targetId) {
this.tenant = tenant;
this.tenantId = tenantId;
this.controllerId = controllerId;
this.targetId = targetId;
}
/**
* Constructor.
*
* @param tenant the tenant for the security token
* @param controllerId the ID of the controller for the security token
*/
public ControllerSecurityToken(final String tenant, final String controllerId) {
this(tenant, null, controllerId, null);
}
/**
* Gets a header value.
*
* @param name of header
* @return the value
*/
public String getHeader(final String name) {
if (headers == null) {
return null;
}
return headers.get(name);
}
/**
* Associates the specified header value with the specified name.
*
* @param name of the header
* @param value of the header
*/
public void putHeader(final String name, final String value) {
if (headers == null) {
headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
}
headers.put(name, value);
}
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.security.controller;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.slf4j.Logger;
import org.springframework.security.core.Authentication;
/**
* An authenticator which extracts (if enabled through configuration) the possibility to authenticate a target based through
* a gateway security token. This is commonly used for targets connected indirectly via a gateway. This gateway controls multiple targets
* under the gateway security token which can be set via the {@code Authorization} header.
* <p>
* {@code Example Header: Authorization: GatewayToken 5d8fSD54fdsFG98DDsa.}
*/
@Slf4j
public class GatewayTokenAuthenticator extends Authenticator.AbstractAuthenticator {
public static final String GATEWAY_SECURITY_TOKEN_AUTH_SCHEME = "GatewayToken ";
private static final int OFFSET_GATEWAY_TOKEN = GATEWAY_SECURITY_TOKEN_AUTH_SCHEME.length();
private final TenantAware.TenantRunner<String> gatewaySecurityTokenKeyConfigRunner;
public GatewayTokenAuthenticator(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
gatewaySecurityTokenKeyConfigRunner = () -> {
log.trace("retrieving configuration value for configuration key {}",
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY);
return systemSecurityContext
.runAsSystem(() -> tenantConfigurationManagement
.getConfigurationValue(TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class)
.getValue());
};
}
@Override
public Authentication authenticate(final ControllerSecurityToken controllerSecurityToken) {
final String authHeader = controllerSecurityToken.getHeader(ControllerSecurityToken.AUTHORIZATION_HEADER);
if (authHeader == null) {
log.debug("The request doesn't contain the 'authorization' header");
return null;
} else if (!authHeader.startsWith(GATEWAY_SECURITY_TOKEN_AUTH_SCHEME)) {
log.debug("The request contains the 'authorization' header but it doesn't start with '{}'", GATEWAY_SECURITY_TOKEN_AUTH_SCHEME);
return null;
}
if (!isEnabled(controllerSecurityToken)) {
log.debug("The gateway token authentication is disabled");
return null;
}
log.debug("Found 'authorization' header starting with '{}'", GATEWAY_SECURITY_TOKEN_AUTH_SCHEME);
final String presentedToken = authHeader.substring(OFFSET_GATEWAY_TOKEN);
// validate if the presented token is the same as the gateway token
return presentedToken.equals(tenantAware.runAsTenant(controllerSecurityToken.getTenant(), gatewaySecurityTokenKeyConfigRunner))
? authenticatedController(controllerSecurityToken.getTenant(), controllerSecurityToken.getControllerId()) : null;
}
@Override
public Logger log() {
return log;
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED;
}
}

View File

@@ -0,0 +1,130 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.security.controller;
import java.util.Arrays;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
/**
* An authenticator which extracts the principal from a request URI and the credential from a request header in a the
* {@link ControllerSecurityToken}.
*/
@Slf4j
public class SecurityHeaderAuthenticator extends Authenticator.AbstractAuthenticator {
private static final Logger LOG_SECURITY_AUTH = LoggerFactory.getLogger("server-security.authentication");
// Example Headers with Cert Information
// Clientip: 217.24.201.180
// X-Forwarded-Proto: https
// X-Ssl-Client-Cn: my.name
// X-Ssl-Client-Dn: CN=my.name,CN=O,CN=R,CN=DE,CN=BOSCH,CN=pki,DC=bosch,DC=com
// X-Ssl-Client-Hash: 7f:87:cb:b5:9c:e0:c5:0a:1a:a6:57:69:0f:ca:0a:95
// X-Ssl-Client-Notafter: Dec 18 08:02:45 2017 GMT
// X-Ssl-Client-Notbefore: Dec 18 07:32:45 2014 GMT
// X-Ssl-Client-Verify: ok
// X-Ssl-Issuer: CN=Bosch-CA1-DE,CN=PKI,DC=Bosch,DC=com
// X-Ssl-Issuer-Dn-1: CN=Bosch-CA-DE,CN=PKI,DC=Bosch,DC=com
// X-Ssl-Issuer-Hash-1: ae:11:f5:6a:0a:e8:74:50:81:0e:0c:37:ec:c5:22:fc
private final String caCommonNameHeader;
// the X-Ssl-Issuer-Hash basic header: Contains the x509 fingerprint hash, this
// header exists multiple times in the request for all trusted chains.
private final String sslIssuerHashBasicHeader;
private final TenantAware.TenantRunner<String> sslIssuerNameConfigTenantRunner;
public SecurityHeaderAuthenticator(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext,
final String caCommonNameHeader, final String caAuthorityNameHeader) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
this.caCommonNameHeader = caCommonNameHeader;
this.sslIssuerHashBasicHeader = caAuthorityNameHeader;
sslIssuerNameConfigTenantRunner = () -> systemSecurityContext.runAsSystem(
() -> tenantConfigurationManagement.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class).getValue());
}
@Override
public Authentication authenticate(final ControllerSecurityToken controllerSecurityToken) {
// retrieve the common name header and the authority name header from the http request and combine them together
final String commonNameValue = controllerSecurityToken.getHeader(caCommonNameHeader);
if (commonNameValue == null) {
log.debug("The request doesn't contain the 'common name' header");
return null;
}
if (!commonNameValue.equals(controllerSecurityToken.getControllerId())) {
log.debug("The request contains the 'common name' header but it doesn't match the controller id");
return null;
}
if (!isEnabled(controllerSecurityToken)) {
log.debug("The gateway header authentication is disabled");
return null;
}
final String sslIssuerHashValue = getIssuerHashHeader(
controllerSecurityToken,
tenantAware.runAsTenant(controllerSecurityToken.getTenant(), sslIssuerNameConfigTenantRunner));
if (sslIssuerHashValue == null) {
log.debug("The request contains the 'common name' header but trusted hash is not found");
return null;
}
if (log.isTraceEnabled()) {
log.debug("Found sslIssuerHash ****, using as credentials for tenant {}", controllerSecurityToken.getTenant());
}
return authenticatedController(controllerSecurityToken.getTenant(), commonNameValue);
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED;
}
@Override
public Logger log() {
return log;
}
/**
* Iterates over the {@link #sslIssuerHashBasicHeader} basic header {@code X-Ssl-Issuer-Hash-%d} and try to find the same hash as known.
* It's ok if we find the hash in any the trusted CA chain to accept this request for this tenant.
*/
@SuppressWarnings("java:S2629") // check if debug is enabled is maybe heavier then evaluation
private String getIssuerHashHeader(final ControllerSecurityToken controllerSecurityToken, final String knownIssuerHashes) {
// there may be several knownIssuerHashes configured for the tenant
final List<String> knownHashes = Arrays.stream(knownIssuerHashes.split("[;,]")).map(String::toLowerCase).toList();
// iterate over the headers until we get a null header.
String foundHash;
for (int iHeader = 1; (foundHash = controllerSecurityToken.getHeader(String.format(sslIssuerHashBasicHeader, iHeader))) != null; iHeader++) {
if (knownHashes.contains(foundHash.toLowerCase())) {
if (log.isTraceEnabled()) {
log.trace("Found matching ssl issuer hash at position {}", iHeader);
}
return foundHash.toLowerCase();
}
}
LOG_SECURITY_AUTH.debug(
"Certificate request but no matching hash found in headers {} for common name {} in request",
sslIssuerHashBasicHeader, controllerSecurityToken.getHeader(caCommonNameHeader));
return null;
}
}

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.security.controller;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.TenantAware;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.slf4j.Logger;
import org.springframework.security.core.Authentication;
/**
* An authenticator which extracts (if enabled through configuration) the possibility to authenticate a target based on
* its target security-token with the {@code Authorization} HTTP header.
* <p>
* {@code Example Header: Authorization: TargetToken 5d8fSD54fdsFG98DDsa.}
*/
@Slf4j
public class SecurityTokenAuthenticator extends Authenticator.AbstractAuthenticator {
public static final String TARGET_SECURITY_TOKEN_AUTH_SCHEME = "TargetToken ";
private static final int OFFSET_TARGET_TOKEN = TARGET_SECURITY_TOKEN_AUTH_SCHEME.length();
private final ControllerManagement controllerManagement;
public SecurityTokenAuthenticator(
final TenantConfigurationManagement tenantConfigurationManagement, final TenantAware tenantAware,
final SystemSecurityContext systemSecurityContext,
final ControllerManagement controllerManagement) {
super(tenantConfigurationManagement, tenantAware, systemSecurityContext);
this.controllerManagement = controllerManagement;
}
@Override
public Authentication authenticate(final ControllerSecurityToken controllerSecurityToken) {
final String authHeader = controllerSecurityToken.getHeader(ControllerSecurityToken.AUTHORIZATION_HEADER);
if (authHeader == null) {
log.debug("The request doesn't contain the 'authorization' header");
return null;
} else if (!authHeader.startsWith(TARGET_SECURITY_TOKEN_AUTH_SCHEME)) {
log.debug("The request contains the 'authorization' header but it doesn't start with '{}'", TARGET_SECURITY_TOKEN_AUTH_SCHEME);
return null;
}
if (!isEnabled(controllerSecurityToken)) {
log.debug("The target security token authentication is disabled");
return null;
}
log.debug("Found 'authorization' header starting with '{}'", TARGET_SECURITY_TOKEN_AUTH_SCHEME);
final String presentedToken = authHeader.substring(OFFSET_TARGET_TOKEN);
return systemSecurityContext.runAsSystemAsTenant(() -> controllerSecurityToken.getTargetId() != null
? controllerManagement.get(controllerSecurityToken.getTargetId())
: controllerManagement.getByControllerId(controllerSecurityToken.getControllerId()),
controllerSecurityToken.getTenant())
// validate if the presented token is the same as the one set for the target
.filter(target -> presentedToken.equals(target.getSecurityToken()))
.map(target -> authenticatedController(controllerSecurityToken.getTenant(), target.getControllerId()))
.orElse(null);
}
@Override
public Logger log() {
return log;
}
@Override
protected String getTenantConfigurationKey() {
return TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED;
}
}

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.security.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
import org.eclipse.hawkbit.security.SecurityContextSerializer;
import org.eclipse.hawkbit.security.SecurityContextTenantAware;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@Feature("Unit Tests - Security")
@Story("Gateway token authentication")
@ExtendWith(MockitoExtension.class)
class GatewayTokenAuthenticatorTest {
private static final String CONTROLLER_ID = "controllerId_gwtoken";
private static final String GATEWAY_TOKEN = "test-gw-token";
private static final String UNKNOWN_TOKEN = "unknown";
private static final TenantConfigurationValue<String> CONFIG_VALUE_GW_TOKEN = TenantConfigurationValue
.<String> builder().value(GATEWAY_TOKEN).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_ENABLED = TenantConfigurationValue
.<Boolean> builder().value(true).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_DISABLED = TenantConfigurationValue
.<Boolean> builder().value(false).build();
private Authenticator authenticator;
@Mock
private TenantConfigurationManagement tenantConfigurationManagementMock;
@Mock
private UserAuthoritiesResolver authoritiesResolver;
@Mock
private SecurityContextSerializer securityContextSerializer;
@BeforeEach
void before() {
final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(authoritiesResolver, securityContextSerializer);
authenticator = new GatewayTokenAuthenticator(
tenantConfigurationManagementMock, tenantAware,
new SystemSecurityContext(tenantAware));
}
@Test
@Description("Tests successful authentication with gateway token")
void testWithGwToken() {
final ControllerSecurityToken securityToken = prepareSecurityToken(GATEWAY_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class))
.thenReturn(CONFIG_VALUE_GW_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CONTROLLER_ID);
}
@Test
@Description("Tests that if gateway token doesn't match, the authentication fails")
void testWithBadGwToken() {
final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY, String.class))
.thenReturn(CONFIG_VALUE_GW_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
@Test
@Description("Tests that if gateway token miss, the authentication fails")
void testWithoutGwToken() {
assertThat(authenticator.authenticate(new ControllerSecurityToken("DEFAULT", CONTROLLER_ID))).isNull();
}
@Test
@Description("Tests that if disabled, the authentication fails")
void testWithGwTokenButDisabled() {
final ControllerSecurityToken securityToken = prepareSecurityToken(GATEWAY_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_DISABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
private static ControllerSecurityToken prepareSecurityToken(final String gwToken) {
final ControllerSecurityToken securityToken = new ControllerSecurityToken("DEFAULT", CONTROLLER_ID);
securityToken.putHeader(ControllerSecurityToken.AUTHORIZATION_HEADER, GatewayTokenAuthenticator.GATEWAY_SECURITY_TOKEN_AUTH_SCHEME + gwToken);
return securityToken;
}
}

View File

@@ -0,0 +1,159 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.security.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
import org.eclipse.hawkbit.security.SecurityContextSerializer;
import org.eclipse.hawkbit.security.SecurityContextTenantAware;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@Feature("Unit Tests - Security")
@Story("Security header authenticator")
@ExtendWith(MockitoExtension.class)
class SecurityHeaderAuthenticatorTest {
private static final String CA_COMMON_NAME = "ca-cn";
private static final String CA_COMMON_NAME_VALUE = "box1";
private static final String X_SSL_ISSUER_HASH_1 = "X-Ssl-Issuer-Hash-1";
private static final String SINGLE_HASH = "hash1";
private static final String SECOND_HASH = "hash2";
private static final String THIRD_HASH = "hash3";
private static final String UNKNOWN_HASH = "unknown";
private static final String MULTI_HASH = "HASH1;hash2,HASH3,HASH1";
private static final TenantConfigurationValue<String> CONFIG_VALUE_SINGLE_HASH = TenantConfigurationValue
.<String> builder().value(SINGLE_HASH).build();
private static final TenantConfigurationValue<String> CONFIG_VALUE_MULTI_HASH = TenantConfigurationValue
.<String> builder().value(MULTI_HASH).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_ENABLED = TenantConfigurationValue
.<Boolean> builder().value(true).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_DISABLED = TenantConfigurationValue
.<Boolean> builder().value(false).build();
private Authenticator authenticator;
@Mock
private TenantConfigurationManagement tenantConfigurationManagementMock;
@Mock
private UserAuthoritiesResolver authoritiesResolver;
@Mock
private SecurityContextSerializer securityContextSerializer;
@BeforeEach
void before() {
final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(authoritiesResolver, securityContextSerializer);
authenticator = new SecurityHeaderAuthenticator(
tenantConfigurationManagementMock, tenantAware,
new SystemSecurityContext(tenantAware), CA_COMMON_NAME, "X-Ssl-Issuer-Hash-%d"
);
}
@Test
@Description("Tests successful authentication with multiple a single hashes")
void testWithSingleKnownHash() {
final ControllerSecurityToken securityToken = prepareSecurityToken(SINGLE_HASH);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
.thenReturn(CONFIG_VALUE_SINGLE_HASH);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CA_COMMON_NAME_VALUE);
}
@Test
@Description("Tests successful authentication with multiple hashes")
void testWithMultipleKnownHashes() {
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
.thenReturn(CONFIG_VALUE_MULTI_HASH);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(prepareSecurityToken(SINGLE_HASH)))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CA_COMMON_NAME_VALUE);
assertThat(authenticator.authenticate(prepareSecurityToken(SECOND_HASH)))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CA_COMMON_NAME_VALUE);
assertThat(authenticator.authenticate(prepareSecurityToken(THIRD_HASH)))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CA_COMMON_NAME_VALUE);
}
@Test
@Description("Tests that if the hash is unknown, the authentication fails")
void testWithUnknownHash() {
final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_HASH);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME, String.class))
.thenReturn(CONFIG_VALUE_MULTI_HASH);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
@Test
@Description("Tests that if CN doesn't match the CN in the security token, the authentication fails")
void testWithNonMatchingCN() {
final ControllerSecurityToken securityToken = new ControllerSecurityToken("DEFAULT", "otherControllerID");
securityToken.putHeader(CA_COMMON_NAME, CA_COMMON_NAME_VALUE);
securityToken.putHeader(X_SSL_ISSUER_HASH_1, SINGLE_HASH);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
@Test
@Description("Tests that if the hash miss, the authentication fails")
void testWithoutHash() {
assertThat(authenticator.authenticate(new ControllerSecurityToken("DEFAULT", CA_COMMON_NAME_VALUE))).isNull();
}
@Test
@Description("Tests that if disabled, the authentication fails")
void testWithSingleKnownHashButDisabled() {
final ControllerSecurityToken securityToken = prepareSecurityToken(SINGLE_HASH);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_HEADER_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_DISABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
private static ControllerSecurityToken prepareSecurityToken(final String issuerHashHeaderValue) {
final ControllerSecurityToken securityToken = new ControllerSecurityToken("DEFAULT", CA_COMMON_NAME_VALUE);
securityToken.putHeader(CA_COMMON_NAME, CA_COMMON_NAME_VALUE);
securityToken.putHeader(X_SSL_ISSUER_HASH_1, issuerHashHeaderValue);
return securityToken;
}
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.security.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import java.util.Optional;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.repository.ControllerManagement;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
import org.eclipse.hawkbit.security.SecurityContextSerializer;
import org.eclipse.hawkbit.security.SecurityContextTenantAware;
import org.eclipse.hawkbit.security.SystemSecurityContext;
import org.eclipse.hawkbit.tenancy.UserAuthoritiesResolver;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@Feature("Unit Tests - Security")
@Story("Gateway token authentication")
@ExtendWith(MockitoExtension.class)
class SecurityTokenAuthenticatorTest {
private static final String CONTROLLER_ID = "controllerId_gwtoken";
private static final String SECURITY_TOKEN = "test-sec-token";
private static final String UNKNOWN_TOKEN = "unknown";
private static final TenantConfigurationValue<String> CONFIG_VALUE_GW_TOKEN = TenantConfigurationValue
.<String> builder().value(SECURITY_TOKEN).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_ENABLED = TenantConfigurationValue
.<Boolean> builder().value(true).build();
private static final TenantConfigurationValue<Boolean> CONFIG_VALUE_DISABLED = TenantConfigurationValue
.<Boolean> builder().value(false).build();
private Authenticator authenticator;
@Mock
private TenantConfigurationManagement tenantConfigurationManagementMock;
@Mock
private ControllerManagement controllerManagementMock;
@Mock
private UserAuthoritiesResolver authoritiesResolver;
@Mock
private SecurityContextSerializer securityContextSerializer;
@BeforeEach
void before() {
final SecurityContextTenantAware tenantAware = new SecurityContextTenantAware(authoritiesResolver, securityContextSerializer);
authenticator = new SecurityTokenAuthenticator(
tenantConfigurationManagementMock, tenantAware,
new SystemSecurityContext(tenantAware), controllerManagementMock);
}
@Test
@Description("Tests successful authentication with gateway token")
void testWithSecToken() {
final ControllerSecurityToken securityToken = prepareSecurityToken(SECURITY_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
final Target target = Mockito.mock(Target.class);
when(target.getControllerId()).thenReturn(CONTROLLER_ID);
when(target.getSecurityToken()).thenReturn(SECURITY_TOKEN);
when(controllerManagementMock.getByControllerId(CONTROLLER_ID)).thenReturn(Optional.of(target));
assertThat(authenticator.authenticate(securityToken))
.isNotNull()
.hasFieldOrPropertyWithValue("principal", CONTROLLER_ID);
}
@Test
@Description("Tests that if gateway token doesn't match, the authentication fails")
void testWithBadSecToken() {
final ControllerSecurityToken securityToken = prepareSecurityToken(UNKNOWN_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_ENABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
@Test
@Description("Tests that if gateway token miss, the authentication fails")
void testWithoutSecToken() {
assertThat(authenticator.authenticate(new ControllerSecurityToken("DEFAULT", CONTROLLER_ID))).isNull();
}
@Test
@Description("Tests that if disabled, the authentication fails")
void testWithSecTokenButDisabled() {
final ControllerSecurityToken securityToken = prepareSecurityToken(SECURITY_TOKEN);
when(tenantConfigurationManagementMock.getConfigurationValue(
TenantConfigurationKey.AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED, Boolean.class))
.thenReturn(CONFIG_VALUE_DISABLED);
assertThat(authenticator.authenticate(securityToken)).isNull();
}
private static ControllerSecurityToken prepareSecurityToken(final String secToken) {
final ControllerSecurityToken securityToken = new ControllerSecurityToken("DEFAULT", CONTROLLER_ID);
securityToken.putHeader(ControllerSecurityToken.AUTHORIZATION_HEADER, SecurityTokenAuthenticator.TARGET_SECURITY_TOKEN_AUTH_SCHEME + secToken);
return securityToken;
}
}