Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[websocket] Support token authentication through header #4515

Merged
merged 2 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
florian-h05 marked this conversation as resolved.
Show resolved Hide resolved
} 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