diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java index 99e0e971db1..71c62206004 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/BaseWebSession.java @@ -123,6 +123,16 @@ public void removeEventHandler(@NotNull CBWebSessionEventHandler handler) { } } + public void migrateEventHandlersTo(@NotNull BaseWebSession target) { + synchronized (sessionEventHandlers) { + for (CBWebSessionEventHandler handler : sessionEventHandlers) { + handler.migrateToSession(target); + target.addEventHandler(handler); + } + sessionEventHandlers.clear(); + } + } + public boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException { return userContext.refresh(smAuthInfo); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java index c5a8f95796d..9aa6983c510 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/websocket/CBWebSessionEventHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ */ package io.cloudbeaver.websocket; +import io.cloudbeaver.model.session.BaseWebSession; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.websocket.event.WSEvent; @@ -24,4 +26,6 @@ public interface CBWebSessionEventHandler { void close(); + void migrateToSession(@NotNull BaseWebSession newSession); + } diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java index bcad1a00f11..f310ac94668 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java @@ -202,6 +202,42 @@ protected String getSessionId(@NotNull HttpServletRequest request) { return httpSession.getId(); } + /** + * Invalidates the current HTTP session, creates a new one, and binds a new {@link WebSession} to it. + */ + @NotNull + public WebSession rotateSession( + @NotNull HttpServletRequest request, + @NotNull WebSession oldWebSession + ) throws DBWebException { + HttpSession oldHttpSession = request.getSession(false); + if (oldHttpSession != null) { + oldHttpSession.invalidate(); + } + String newSessionId = request.getSession(true).getId(); + + String locale = oldWebSession.getLocale(); + String remoteAddr = oldWebSession.getLastRemoteAddr(); + String remoteUserAgent = oldWebSession.getLastRemoteUserAgent(); + var requestInfo = new WebHttpRequestInfo(newSessionId, locale, remoteAddr, remoteUserAgent); + WebSession newWebSession; + try { + newWebSession = createWebSessionImpl(requestInfo); + } catch (DBException e) { + throw new DBWebException(e); + } + oldWebSession.migrateEventHandlersTo(newWebSession); + String oldSessionId = oldWebSession.getSessionId(); + synchronized (sessionMap) { + sessionMap.remove(oldSessionId); + sessionMap.put(newSessionId, newWebSession); + } + oldWebSession.close(false, false); + + log.debug("Session rotated '" + oldSessionId + "' -> '" + newSessionId + "'"); + return newWebSession; + } + /** * Returns not expired session from cache, or restore it. * @@ -288,13 +324,12 @@ public BaseWebSession getSession(@NotNull String sessionId) { @Nullable public WebSession findWebSession(@NotNull HttpServletRequest request) { String sessionId = getSessionId(request); + WebSession webSession; synchronized (sessionMap) { var session = sessionMap.get(sessionId); - if (session instanceof WebSession) { - return (WebSession) session; - } - return null; + webSession = (session instanceof WebSession) ? (WebSession) session : null; } + return webSession; } @Nullable diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBClientEventProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBClientEventProcessor.java index 0bb70b84a5a..cde2a2d4e99 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBClientEventProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBClientEventProcessor.java @@ -33,12 +33,16 @@ public class CBClientEventProcessor { private static final Log log = Log.getLog(CBClientEventProcessor.class); - final BaseWebSession webSession; + volatile BaseWebSession webSession; public CBClientEventProcessor(@NotNull BaseWebSession webSession) { this.webSession = webSession; } + void setWebSession(@NotNull BaseWebSession webSession) { + this.webSession = webSession; + } + public void process(@Nullable String message) { if (CommonUtils.isEmpty(message)) { return; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsLongPolling.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsLongPolling.java index a6ff8f82f2b..9f147524a7b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsLongPolling.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsLongPolling.java @@ -40,7 +40,7 @@ public class CBEventsLongPolling implements CBWebSessionEventHandler { private static final int QUEUE_CAPACITY = 1000; - private final BaseWebSession webSession; + private volatile BaseWebSession webSession; private final BlockingQueue queue = new LinkedBlockingQueue<>(QUEUE_CAPACITY); private final CBClientEventProcessor processor; private volatile long lastPoll; @@ -88,6 +88,12 @@ public List pollEvents(long timeoutSec) throws InterruptedException { return result; } + @Override + public void migrateToSession(@NotNull BaseWebSession newSession) { + this.webSession = newSession; + this.processor.setWebSession(newSession); + } + @Override public void handleWebSessionEvent(@NotNull WSEvent event) { if (!queue.offer(event)) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java index bcf71109d90..131ac000439 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBEventsWebSocket.java @@ -32,7 +32,11 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi private static final Log log = Log.getLog(CBEventsWebSocket.class); @Nullable - private BaseWebSession webSession; + private volatile BaseWebSession webSession; + @Nullable + private FromUserEventHandler eventProcessor; + @Nullable + private WebSocketPingPongCallback pingPongCallback; @Override public void onOpen(Session session, EndpointConfig config) { @@ -48,13 +52,31 @@ public void onOpen(Session session, EndpointConfig config) { log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session"); session.setMaxIdleTimeout(Duration.ofMinutes(5).toMillis()); - session.addMessageHandler(String.class, new FromUserEventHandler(webSession)); - session.addMessageHandler(PongMessage.class, new WebSocketPingPongCallback(webSession)); + this.eventProcessor = new FromUserEventHandler(webSession); + session.addMessageHandler(String.class, eventProcessor); + this.pingPongCallback = new WebSocketPingPongCallback(webSession); + session.addMessageHandler(PongMessage.class, pingPongCallback); CBJettyWebSocketManager.registerWebSocket(webSession.getSessionId(), this); } } + @Override + public void migrateToSession(@NotNull BaseWebSession newSession) { + BaseWebSession oldSession = this.webSession; + this.webSession = newSession; + if (eventProcessor != null) { + eventProcessor.setWebSession(newSession); + } + if (pingPongCallback != null) { + pingPongCallback.setWebSession(newSession); + } + if (oldSession != null) { + CBJettyWebSocketManager.migrateWebSocket(oldSession.getSessionId(), newSession.getSessionId(), this); + } + log.debug("EventWebSocket migrated to the " + newSession.getSessionId() + " session"); + } + private static class FromUserEventHandler extends CBClientEventProcessor implements MessageHandler.Whole { private FromUserEventHandler(@NotNull BaseWebSession webSession) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java index bfe6712f867..411d7cda1b0 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/CBJettyWebSocketManager.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,25 @@ public static void registerWebSocket(@NotNull String webSessionId, @NotNull CBEv socketBySessionId.computeIfAbsent(webSessionId, key -> new CopyOnWriteArrayList<>()).add(webSocket); } + /** + * Re-keys a live web socket from the old session id to the new one after a session rotation, + * so that keep-alive pings keep being delivered to it. + */ + public static void migrateWebSocket( + @NotNull String oldSessionId, + @NotNull String newSessionId, + @NotNull CBEventsWebSocket webSocket + ) { + List oldSockets = socketBySessionId.get(oldSessionId); + if (oldSockets != null) { + oldSockets.remove(webSocket); + if (oldSockets.isEmpty()) { + socketBySessionId.remove(oldSessionId); + } + } + registerWebSocket(newSessionId, webSocket); + } + public static void sendPing() { //remove expired sessions WebAppSessionManager webSessionManager = WebAppUtils.getWebApplication().getSessionManager(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java index 79e875f238a..dcb5de3456a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/websockets/WebSocketPingPongCallback.java @@ -24,12 +24,16 @@ public class WebSocketPingPongCallback implements MessageHandler.Whole { @NotNull - private final BaseWebSession webSession; + private volatile BaseWebSession webSession; public WebSocketPingPongCallback(@NotNull BaseWebSession webSession) { this.webSession = webSession; } + void setWebSession(@NotNull BaseWebSession webSession) { + this.webSession = webSession; + } + @Override public void onMessage(PongMessage message) { if (webSession instanceof WebHeadlessSession) { diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java index a5c5371af20..67a9df0d602 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ public interface DBWServiceAuth extends DBWService { @WebAction(authRequired = false) WebAuthStatus authLogin( + @NotNull HttpServletRequest httpRequest, @NotNull WebSession webSession, @NotNull String providerId, @Nullable String providerConfigurationId, diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthJob.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthJob.java index 00105a7daa3..665ba78c6cc 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthJob.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebAsyncAuthJob.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java index e788df0c5fc..c0b3ebc5539 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ public WebServiceBindingAuth() { public void bindWiring(DBWBindingContext model) { model.getQueryType() .dataFetcher("authLogin", env -> getService(env).authLogin( + GraphQLEndpoint.getServletRequestOrThrow(env), getWebSession(env, false), getArgumentVal(env, "provider"), getArgument(env, "configuration"), diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/handler/WSAuthSessionEventHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/handler/WSAuthSessionEventHandler.java index 753140bf380..f7de998fef2 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/handler/WSAuthSessionEventHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/handler/WSAuthSessionEventHandler.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index 7df05d3c467..af8854421ca 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,8 @@ public class WebServiceAuthImpl implements DBWServiceAuth { @Override public WebAuthStatus authLogin( - @NotNull WebSession webSession, + @NotNull HttpServletRequest httpRequest, + @NotNull WebSession inputWebSession, @NotNull String providerId, @Nullable String providerConfigurationId, @Nullable Map authParameters, @@ -75,6 +76,13 @@ public WebAuthStatus authLogin( boolean forceSessionsLogout ) throws DBWebException { try { + WebSession webSession = inputWebSession; + if (inputWebSession.getUser() == null) { + // Rotate anonymous web sessions during login attempts to prevent session fixation attacks. + webSession = CBApplication.getInstance().getSessionManager() + .rotateSession(httpRequest, inputWebSession); + } + var smAuthInfo = initiateAuthentication(webSession, providerId, providerConfigurationId, authParameters, forceSessionsLogout); //TODO deprecated, use asyncAuthLogin for federated auth, exits for backward compatibility linkWithActiveUser = linkWithActiveUser && CBApplication.getInstance().getAppConfiguration() @@ -85,7 +93,8 @@ public WebAuthStatus authLogin( } else { //run it sync var authProcessor = new WebSessionAuthProcessor(webSession, smAuthInfo, linkWithActiveUser); - return new WebAuthStatus(smAuthInfo.getAuthStatus(), authProcessor.authenticateSession()); + List authInfos = authProcessor.authenticateSession(); + return new WebAuthStatus(smAuthInfo.getAuthStatus(), authInfos); } } catch (SMTooManySessionsException e) { throw new DBWebException("User authentication failed", e.getErrorType(), e); @@ -95,14 +104,22 @@ public WebAuthStatus authLogin( } @Override + @NotNull public WebAsyncAuthStatus federatedLogin( @NotNull HttpServletRequest httpRequest, - @NotNull WebSession webSession, + @NotNull WebSession inputWebSession, @NotNull String providerId, @Nullable String providerConfigurationId, boolean linkWithActiveUser, boolean forceSessionsLogout ) throws DBWebException { + WebSession webSession = inputWebSession; + if (inputWebSession.getUser() == null) { + // Rotate anonymous web sessions during login attempts to prevent session fixation attacks. + webSession = CBApplication.getInstance().getSessionManager() + .rotateSession(httpRequest, inputWebSession); + } + WebAuthProviderDescriptor providerDescriptor = WebAuthProviderRegistry.getInstance().getAuthProvider(providerId); if (providerDescriptor == null) { throw new DBWebException("Provider '" + providerId + "' not found");