[websocket] Support token authentication through header (#4515)
Signed-off-by: Florian Hotze <dev@florianhotze.com>pull/4532/head
parent
139a2e20a1
commit
25ca43d165
|
@ -248,6 +248,13 @@ public class AuthFilter implements ContainerRequestFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
public @Nullable SecurityContext getSecurityContext(HttpServletRequest request, boolean allowQueryToken)
|
||||||
throws AuthenticationException, IOException {
|
throws AuthenticationException, IOException {
|
||||||
String altTokenHeader = request.getHeader(ALT_AUTH_HEADER);
|
String altTokenHeader = request.getHeader(ALT_AUTH_HEADER);
|
||||||
|
|
|
@ -13,8 +13,11 @@
|
||||||
package org.openhab.core.io.websocket;
|
package org.openhab.core.io.websocket;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Base64;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.servlet.Servlet;
|
import javax.servlet.Servlet;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
|
@ -43,10 +46,19 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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 Jan N. Klug - Initial contribution
|
||||||
* @author Miguel Álvarez Díez - Refactor into a common servlet
|
* @author Miguel Álvarez Díez - Refactor into a common servlet
|
||||||
|
* @author Florian Hotze - Support passing access token through Sec-WebSocket-Protocol header
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@HttpWhiteboardServletName(CommonWebSocketServlet.SERVLET_PATH)
|
@HttpWhiteboardServletName(CommonWebSocketServlet.SERVLET_PATH)
|
||||||
|
@ -55,6 +67,11 @@ import org.slf4j.LoggerFactory;
|
||||||
public class CommonWebSocketServlet extends WebSocketServlet {
|
public class CommonWebSocketServlet extends WebSocketServlet {
|
||||||
private static final long serialVersionUID = 1L;
|
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 SERVLET_PATH = "/ws";
|
||||||
|
|
||||||
public static final String DEFAULT_ADAPTER_ID = EventWebSocketAdapter.ADAPTER_ID;
|
public static final String DEFAULT_ADAPTER_ID = EventWebSocketAdapter.ADAPTER_ID;
|
||||||
|
@ -94,7 +111,31 @@ public class CommonWebSocketServlet extends WebSocketServlet {
|
||||||
if (servletUpgradeRequest == null || servletUpgradeResponse == null) {
|
if (servletUpgradeRequest == null || servletUpgradeResponse == null) {
|
||||||
return 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");
|
||||||
|
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());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessToken != null ? isAuthorizedRequest(accessToken) : isAuthorizedRequest(servletUpgradeRequest)) {
|
||||||
String requestPath = servletUpgradeRequest.getRequestURI().getPath();
|
String requestPath = servletUpgradeRequest.getRequestURI().getPath();
|
||||||
String pathPrefix = SERVLET_PATH + "/";
|
String pathPrefix = SERVLET_PATH + "/";
|
||||||
boolean useDefaultAdapter = requestPath.equals(pathPrefix) || !requestPath.startsWith(pathPrefix);
|
boolean useDefaultAdapter = requestPath.equals(pathPrefix) || !requestPath.startsWith(pathPrefix);
|
||||||
|
@ -122,6 +163,17 @@ public class CommonWebSocketServlet extends WebSocketServlet {
|
||||||
return null;
|
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) {
|
private boolean isAuthorizedRequest(ServletUpgradeRequest servletUpgradeRequest) {
|
||||||
try {
|
try {
|
||||||
var securityContext = authFilter.getSecurityContext(servletUpgradeRequest.getHttpServletRequest(),
|
var securityContext = authFilter.getSecurityContext(servletUpgradeRequest.getHttpServletRequest(),
|
||||||
|
|
Loading…
Reference in New Issue