Skip to content

Commit

Permalink
[websocket] Support token authentication through Sec-WebSocket-Protoc…
Browse files Browse the repository at this point in the history
…ol header

Signed-off-by: Florian Hotze <dev@florianhotze.com>
  • Loading branch information
florian-h05 committed Dec 22, 2024
1 parent 12f2314 commit 164291a
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
* <p>
* Clients can authorize in two ways:
* <ul>
* <li>By setting <code>org.openhab.ws.accessToken.base64.</code> + base64-encoded access token and the
* {@link CommonWebSocketServlet#WEBSOCKET_PROTOCOL_DEFAULT} in the <code>Sec-WebSocket-Protocol</code> header.</li>
* <li>By providing the access token as query parameter <code>accessToken</code>.</li>
* </ul>
*
* @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)
Expand All @@ -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.(?<base64>[A-Za-z0-9+/]*)");

public static final String SERVLET_PATH = "/ws";

public static final String DEFAULT_ADAPTER_ID = EventWebSocketAdapter.ADAPTER_ID;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
Expand Down

0 comments on commit 164291a

Please sign in to comment.