Browse Source

Remove WebSocket API key header authentication, keep only authCmd

pull/15170/head
Andrii Landiak 7 months ago
parent
commit
4b034d3067
  1. 80
      application/src/main/java/org/thingsboard/server/config/ApiKeyHandshakeInterceptor.java
  2. 2
      application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
  3. 16
      application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
  4. 7
      application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
  5. 51
      application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java
  6. 4
      application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

80
application/src/main/java/org/thingsboard/server/config/ApiKeyHandshakeInterceptor.java

@ -1,80 +0,0 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.pat.ApiKeyAuthenticationProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
@Slf4j
@Component
@TbCoreComponent
@RequiredArgsConstructor
public class ApiKeyHandshakeInterceptor implements HandshakeInterceptor {
public static final String API_KEY_SECURITY_CTX_ATTR = "apiKeySecurityCtx";
private final ApiKeyAuthenticationProvider apiKeyAuthenticationProvider;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
String apiKey = extractApiKey(request);
if (apiKey != null) {
if (apiKey.isEmpty()) {
log.debug("Empty API key provided during WS handshake");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}
try {
SecurityUser securityUser = apiKeyAuthenticationProvider.authenticate(apiKey);
attributes.put(API_KEY_SECURITY_CTX_ATTR, securityUser);
} catch (AuthenticationException e) {
log.warn("API key authentication failed during WS handshake: {}", e.getMessage());
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}
}
return true;
}
private String extractApiKey(ServerHttpRequest request) {
String header = request.getHeaders().getFirst(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER);
if (header == null) {
header = request.getHeaders().getFirst(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER_V2);
}
if (header != null && header.startsWith(ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX)) {
return header.substring(ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX.length());
}
return null;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
// no-op
}
}

2
application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java

@ -40,7 +40,6 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
private static final String WS_API_MAPPING = "/api/ws/**";
private final WebSocketHandler wsHandler;
private final ApiKeyHandshakeInterceptor apiKeyHandshakeInterceptor;
@Value("${server.ws.max_text_message_buffer_size:32768}")
private int maxTextMessageBufferSize;
@ -62,7 +61,6 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided");
}
registry.addHandler(wsHandler, WS_API_MAPPING)
.addInterceptors(apiKeyHandshakeInterceptor)
.setAllowedOriginPatterns("*");
}

16
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java

@ -48,7 +48,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.config.ApiKeyHandshakeInterceptor;
import org.thingsboard.server.config.WebSocketConfiguration;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -336,16 +335,11 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements WebSocke
}
SecurityUser securityCtx = null;
Object apiKeyCtx = session.getAttributes().get(ApiKeyHandshakeInterceptor.API_KEY_SECURITY_CTX_ATTR);
if (apiKeyCtx instanceof SecurityUser) {
securityCtx = (SecurityUser) apiKeyCtx;
} else {
String query = session.getUri().getQuery();
if (query != null) {
String token = extractQueryParam(query, "token");
if (StringUtils.isNotEmpty(token)) {
securityCtx = authenticationProvider.authenticate(token);
}
String query = session.getUri().getQuery();
if (query != null) {
String token = extractQueryParam(query, "token");
if (StringUtils.isNotEmpty(token)) {
securityCtx = authenticationProvider.authenticate(token);
}
}
return WebSocketSessionRef.builder()

7
application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java

@ -32,7 +32,6 @@ import org.springframework.web.socket.config.annotation.EnableWebSocket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@ -121,10 +120,4 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest {
return wsClient;
}
protected TbTestWebSocketClient buildAndConnectWebSocketClientWithApiKeyHeader(String apiKey) throws URISyntaxException, InterruptedException {
TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws"), Map.of("X-Authorization", "ApiKey " + apiKey));
assertThat(wsClient.connectBlocking(TIMEOUT, TimeUnit.SECONDS)).isTrue();
return wsClient;
}
}

51
application/src/test/java/org/thingsboard/server/controller/ApiKeyWebSocketApiTest.java

@ -17,22 +17,14 @@ package org.thingsboard.server.controller;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.pat.ApiKey;
import org.thingsboard.server.common.data.pat.ApiKeyInfo;
import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.query.EntityCountQuery;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@ -64,49 +56,6 @@ public class ApiKeyWebSocketApiTest extends WebSocketApiTest {
return buildAndConnectWebSocketClientWithApiKey(apiKey.getValue());
}
@Test
public void testApiKeyHeaderAuth() throws Exception {
TbTestWebSocketClient client = buildAndConnectWebSocketClientWithApiKeyHeader(apiKey.getValue());
try {
assertThat(client.isOpen()).isTrue();
DeviceTypeFilter dtf = new DeviceTypeFilter(List.of("default"), "Device");
EntityCountQuery ecq = new EntityCountQuery(dtf, Collections.emptyList());
EntityCountCmd cmd = new EntityCountCmd(1, ecq);
client.send(cmd);
EntityCountUpdate update = client.parseCountReply(client.waitForReply());
Assert.assertEquals(1, update.getCmdId());
Assert.assertTrue(update.getCount() >= 0);
} finally {
client.close();
}
}
@Test
public void testInvalidApiKeyHeader_connectionRejected() throws Exception {
TbTestWebSocketClient client = new TbTestWebSocketClient(
new URI(WS_URL + wsPort + "/api/ws"), Map.of("X-Authorization", "ApiKey invalid-key"));
try {
boolean connected = client.connectBlocking(TIMEOUT, TimeUnit.SECONDS);
assertThat(connected).isFalse();
} finally {
client.close();
}
}
@Test
public void testEmptyApiKeyHeader_connectionRejected() throws Exception {
TbTestWebSocketClient client = new TbTestWebSocketClient(
new URI(WS_URL + wsPort + "/api/ws"), Map.of("X-Authorization", "ApiKey "));
try {
boolean connected = client.connectBlocking(TIMEOUT, TimeUnit.SECONDS);
assertThat(connected).isFalse();
} finally {
client.close();
}
}
@Test
public void testInvalidApiKeyAuthCmd_connectionClosed() throws Exception {
TbTestWebSocketClient client = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws"));

4
application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java

@ -44,7 +44,6 @@ import java.net.URI;
import java.nio.channels.NotYetConnectedException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -64,9 +63,6 @@ public class TbTestWebSocketClient extends WebSocketClient {
super(serverUri);
}
public TbTestWebSocketClient(URI serverUri, Map<String, String> httpHeaders) {
super(serverUri, httpHeaders);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {

Loading…
Cancel
Save