From 164291af077781d30abea1f7b01c3121b9477945 Mon Sep 17 00:00:00 2001 From: Florian Hotze Date: Sun, 22 Dec 2024 14:59:14 +0100 Subject: [PATCH 1/2] [websocket] Support token authentication through Sec-WebSocket-Protocol header Signed-off-by: Florian Hotze --- .../openhab/core/io/rest/auth/AuthFilter.java | 7 +++ .../io/websocket/CommonWebSocketServlet.java | 50 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AuthFilter.java b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AuthFilter.java index 1d7230bc8c1..90c0042a239 100644 --- a/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AuthFilter.java +++ b/bundles/org.openhab.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/AuthFilter.java @@ -248,6 +248,13 @@ public void filter(@Nullable ContainerRequestContext requestContext) throws IOEx } } + public @Nullable SecurityContext getSecurityContext(@Nullable String bearerToken) throws AuthenticationException { + if (bearerToken == null) { + return null; + } + return authenticateBearerToken(bearerToken); + } + public @Nullable SecurityContext getSecurityContext(HttpServletRequest request, boolean allowQueryToken) throws AuthenticationException, IOException { String altTokenHeader = request.getHeader(ALT_AUTH_HEADER); diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java index 8109e5dd161..f591b93c395 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java @@ -13,8 +13,11 @@ package org.openhab.core.io.websocket; import java.io.IOException; +import java.util.Base64; import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.Servlet; import javax.servlet.ServletException; @@ -43,10 +46,19 @@ import org.slf4j.LoggerFactory; /** - * The {@link CommonWebSocketServlet} provides the servlet for WebSocket connections + * The {@link CommonWebSocketServlet} provides the servlet for WebSocket connections. + * + *

+ * Clients can authorize in two ways: + *

* * @author Jan N. Klug - Initial contribution * @author Miguel Álvarez Díez - Refactor into a common servlet + * @author Florian Hotze - Support passing access token through Sec-WebSocket-Protocol header */ @NonNullByDefault @HttpWhiteboardServletName(CommonWebSocketServlet.SERVLET_PATH) @@ -55,6 +67,11 @@ public class CommonWebSocketServlet extends WebSocketServlet { private static final long serialVersionUID = 1L; + public static final String SEC_WEBSOCKET_PROTOCOL_HEADER = "Sec-WebSocket-Protocol"; + public static final String WEBSOCKET_PROTOCOL_DEFAULT = "org.openhab.ws.protocol.default"; + private static final Pattern WEBSOCKET_ACCESS_TOKEN_PATTERN = Pattern + .compile("org.openhab.ws.accessToken.base64.(?[A-Za-z0-9+/]*)"); + public static final String SERVLET_PATH = "/ws"; public static final String DEFAULT_ADAPTER_ID = EventWebSocketAdapter.ADAPTER_ID; @@ -94,7 +111,25 @@ private class CommonWebSocketCreator implements WebSocketCreator { if (servletUpgradeRequest == null || servletUpgradeResponse == null) { return null; } - if (isAuthorizedRequest(servletUpgradeRequest)) { + + String accessToken = null; + String secWebSocketProtocolHeader = servletUpgradeRequest.getHeader(SEC_WEBSOCKET_PROTOCOL_HEADER); + if (secWebSocketProtocolHeader != null) { // if the client sends the Sec-WebSocket-Protocol header + // respond with the default protocol + servletUpgradeResponse.setHeader(SEC_WEBSOCKET_PROTOCOL_HEADER, WEBSOCKET_PROTOCOL_DEFAULT); + // extract the base64 encoded access token from the requested protocols + Matcher matcher = WEBSOCKET_ACCESS_TOKEN_PATTERN.matcher(secWebSocketProtocolHeader); + if (matcher.find() && matcher.group("base64") != null) { + String base64 = matcher.group("base64"); + accessToken = new String(Base64.getDecoder().decode(base64)); + } else { + logger.warn("Invalid use of Sec-WebSocket-Protocol header from {}.", + servletUpgradeRequest.getRemoteAddress()); + return null; + } + } + + if (accessToken != null ? isAuthorizedRequest(accessToken) : isAuthorizedRequest(servletUpgradeRequest)) { String requestPath = servletUpgradeRequest.getRequestURI().getPath(); String pathPrefix = SERVLET_PATH + "/"; boolean useDefaultAdapter = requestPath.equals(pathPrefix) || !requestPath.startsWith(pathPrefix); @@ -122,6 +157,17 @@ private class CommonWebSocketCreator implements WebSocketCreator { return null; } + private boolean isAuthorizedRequest(String bearerToken) { + try { + var securityContext = authFilter.getSecurityContext(bearerToken); + return securityContext != null + && (securityContext.isUserInRole(Role.USER) || securityContext.isUserInRole(Role.ADMIN)); + } catch (AuthenticationException e) { + logger.warn("Error handling WebSocket authorization", e); + return false; + } + } + private boolean isAuthorizedRequest(ServletUpgradeRequest servletUpgradeRequest) { try { var securityContext = authFilter.getSecurityContext(servletUpgradeRequest.getHttpServletRequest(), From f8eccec06e3865fe5eb3d183329509c1c6572f3d Mon Sep 17 00:00:00 2001 From: Florian Hotze Date: Tue, 31 Dec 2024 15:10:31 +0100 Subject: [PATCH 2/2] Catch IllegalArgumentException when decoding base64 Signed-off-by: Florian Hotze --- .../openhab/core/io/websocket/CommonWebSocketServlet.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java index f591b93c395..53aeba5f938 100644 --- a/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java +++ b/bundles/org.openhab.core.io.websocket/src/main/java/org/openhab/core/io/websocket/CommonWebSocketServlet.java @@ -121,7 +121,13 @@ private class CommonWebSocketCreator implements WebSocketCreator { Matcher matcher = WEBSOCKET_ACCESS_TOKEN_PATTERN.matcher(secWebSocketProtocolHeader); if (matcher.find() && matcher.group("base64") != null) { String base64 = matcher.group("base64"); - accessToken = new String(Base64.getDecoder().decode(base64)); + try { + accessToken = new String(Base64.getDecoder().decode(base64)); + } catch (IllegalArgumentException e) { + logger.warn("Invalid base64 encoded access token in Sec-WebSocket-Protocol header from {}.", + servletUpgradeRequest.getRemoteAddress()); + return null; + } } else { logger.warn("Invalid use of Sec-WebSocket-Protocol header from {}.", servletUpgradeRequest.getRemoteAddress());