diff --git a/kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/RestSessionHelper.java b/kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/RestSessionHelper.java index 33dab7de5ac..c891cd015a1 100644 --- a/kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/RestSessionHelper.java +++ b/kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/RestSessionHelper.java @@ -45,6 +45,7 @@ public HttpSession createNewAuthenticatedSession(final HttpServletRequest reques } final HttpSession newSession = request.getSession(true); + request.changeSessionId(); newSession.setAttribute(SessionAttributes.AUTORIZED_USER.getValue(), user); updateLastActivity(newSession); @@ -52,8 +53,7 @@ public HttpSession createNewAuthenticatedSession(final HttpServletRequest reques final Optional credentialsHash = userAdminHelper.getCredentialsHash(user); if (credentialsHash.isPresent()) { - newSession.setAttribute(SessionAttributes.CREDENTIALS_HASH.getValue(), - credentialsHash.get()); + newSession.setAttribute(SessionAttributes.CREDENTIALS_HASH.getValue(), credentialsHash.get()); } getOrCreateXsrfToken(newSession); @@ -95,8 +95,7 @@ public Optional getPrincipalFromSession(final HttpSession session) { } public boolean credentialsChanged(final HttpSession session, final String userName) { - return !Objects.equals( - session.getAttribute(SessionAttributes.CREDENTIALS_HASH.getValue()), + return !Objects.equals(session.getAttribute(SessionAttributes.CREDENTIALS_HASH.getValue()), userAdminHelper.getCredentialsHash(userName).orElse(null)); } @@ -123,8 +122,7 @@ public boolean isSessionExpired(final HttpSession session, final int maxInactive } public Optional getXsrfToken(final HttpSession httpSession) { - return Optional - .ofNullable(httpSession.getAttribute(SessionAttributes.XSRF_TOKEN.getValue())) + return Optional.ofNullable(httpSession.getAttribute(SessionAttributes.XSRF_TOKEN.getValue())) .flatMap(t -> t instanceof String ? Optional.of((String) t) : Optional.empty()); } diff --git a/kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/SessionRestService.java b/kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/SessionRestService.java index 4f977acf82e..3afd5d4a840 100644 --- a/kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/SessionRestService.java +++ b/kura/org.eclipse.kura.rest.provider/src/main/java/org/eclipse/kura/internal/rest/auth/SessionRestService.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Eurotech and/or its affiliates and others + * Copyright (c) 2023, 2024 Eurotech and/or its affiliates and others * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -84,8 +84,7 @@ public void setOptions(final RestServiceOptions options) { @Path(SessionRestServiceConstants.LOGIN_PASSWORD_PATH) @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public AuthenticationResponseDTO authenticateWithUsernameAndPassword( - final UsernamePasswordDTO usernamePassword, + public AuthenticationResponseDTO authenticateWithUsernameAndPassword(final UsernamePasswordDTO usernamePassword, @Context final HttpServletRequest request) { if (!options.isSessionManagementEnabled() || !options.isPasswordAuthEnabled()) { @@ -100,8 +99,7 @@ public AuthenticationResponseDTO authenticateWithUsernameAndPassword( try { - this.userAdminHelper.verifyUsernamePassword(usernamePassword.getUsername(), - usernamePassword.getPassword()); + this.userAdminHelper.verifyUsernamePassword(usernamePassword.getUsername(), usernamePassword.getPassword()); final HttpSession session = this.restSessionHelper.createNewAuthenticatedSession(request, usernamePassword.getUsername()); @@ -117,8 +115,12 @@ public AuthenticationResponseDTO authenticateWithUsernameAndPassword( return response; } catch (final AuthenticationException e) { + invalidateCurrentSession(request); handleAuthenticationException(e); throw new IllegalStateException("unreachable"); + } catch (final Exception e) { + invalidateCurrentSession(request); + throw e; } } @@ -131,21 +133,27 @@ public AuthenticationResponseDTO authenticateWithCertificate(@Context final Http throw new WebApplicationException(Status.NOT_FOUND); } - final CertificateAuthenticationProvider certificateAuthProvider = new CertificateAuthenticationProvider( - userAdminHelper); + try { - final Optional principal = certificateAuthProvider.authenticate(requestContext, - "Create session via certificate authentication"); + final CertificateAuthenticationProvider certificateAuthProvider = new CertificateAuthenticationProvider( + userAdminHelper); - if (principal.isPresent()) { - this.restSessionHelper.createNewAuthenticatedSession(request, - principal.get().getName()); - } else { - throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, - "Certificate authentication failed"); - } + final Optional principal = certificateAuthProvider.authenticate(requestContext, + "Create session via certificate authentication"); + + if (principal.isPresent()) { + this.restSessionHelper.createNewAuthenticatedSession(request, principal.get().getName()); + } else { - return buildAuthenticationResponse(principal.get().getName()); + throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, + "Certificate authentication failed"); + } + + return buildAuthenticationResponse(principal.get().getName()); + } catch (final Exception e) { + invalidateCurrentSession(request); + throw e; + } } @GET @@ -159,13 +167,11 @@ public XsrfTokenDTO getXSRFToken(@Context final HttpServletRequest request, final Optional session = this.restSessionHelper.getExistingSession(request); if (!session.isPresent()) { - throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, - INVALID_SESSION_MESSAGE); + throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, INVALID_SESSION_MESSAGE); } if (!this.restSessionHelper.getCurrentPrincipal(requestContext).isPresent()) { - throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, - INVALID_SESSION_MESSAGE); + throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, INVALID_SESSION_MESSAGE); } return new XsrfTokenDTO(this.restSessionHelper.getOrCreateXsrfToken(session.get())); @@ -174,8 +180,7 @@ public XsrfTokenDTO getXSRFToken(@Context final HttpServletRequest request, @POST @Path(SessionRestServiceConstants.CHANGE_PASSWORD_PATH) public void updateUserPassword(@Context final ContainerRequestContext requestContext, - @Context final HttpServletRequest request, - final UpdatePasswordDTO passwordUpdate) { + @Context final HttpServletRequest request, final UpdatePasswordDTO passwordUpdate) { passwordUpdate.validate(); @@ -196,13 +201,15 @@ public void updateUserPassword(@Context final ContainerRequestContext requestCon this.userAdminHelper.changeUserPassword(username.get(), passwordUpdate.getNewPassword()); - final Optional session = this.restSessionHelper.getExistingSession(request); + final HttpSession session = this.restSessionHelper.createNewAuthenticatedSession(request, newPassword); + this.restSessionHelper.unlockSession(session); - if (session.isPresent()) { - this.restSessionHelper.unlockSession(session.get()); - } } catch (final AuthenticationException e) { + invalidateCurrentSession(request); handleAuthenticationException(e); + } catch (final Exception e) { + invalidateCurrentSession(request); + throw e; } } @@ -215,14 +222,12 @@ public void logout(@Context final HttpServletRequest request, @Context final Htt } if (!this.restSessionHelper.getCurrentPrincipal(requestContext).isPresent()) { - throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, - INVALID_SESSION_MESSAGE); + throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, INVALID_SESSION_MESSAGE); } this.restSessionHelper.logout(request, response); - auditLogger.info("{} Rest - Success - Logout succeeded", - AuditContext.currentOrInternal()); + auditLogger.info("{} Rest - Success - Logout succeeded", AuditContext.currentOrInternal()); } @GET @@ -237,14 +242,12 @@ public IdentityInfoDTO getCurrentIdentityInfo(@Context final ContainerRequestCon final Optional currentPrincipal = this.restSessionHelper.getCurrentPrincipal(requestContext); if (!currentPrincipal.isPresent()) { - throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, - INVALID_SESSION_MESSAGE); + throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, INVALID_SESSION_MESSAGE); } final String identityName = currentPrincipal.get().getName(); final Set permissions = this.userAdminHelper.getIdentityPermissions(identityName); - final boolean needsPasswordChange = this.userAdminHelper - .isPasswordChangeRequired(identityName); + final boolean needsPasswordChange = this.userAdminHelper.isPasswordChangeRequired(identityName); return new IdentityInfoDTO(identityName, needsPasswordChange, permissions); } @@ -269,8 +272,7 @@ public AuthenticationInfoDTO getAuthenticationMethodInfo() { final Map httpServiceConfig = ConfigurationAdminHelper .loadHttpServiceConfigurationProperties(configAdmin); - final Set httpsClientAuthPorts = ConfigurationAdminHelper - .getHttpsMutualAuthPorts(httpServiceConfig); + final Set httpsClientAuthPorts = ConfigurationAdminHelper.getHttpsMutualAuthPorts(httpServiceConfig); if (!httpsClientAuthPorts.isEmpty()) { return new AuthenticationInfoDTO(isPasswordAuthEnabled, true, httpsClientAuthPorts, message); @@ -299,8 +301,7 @@ private void validatePasswordStrength(final String newPassword) { } private AuthenticationResponseDTO buildAuthenticationResponse(final String username) { - final boolean needsPasswordChange = this.userAdminHelper - .isPasswordChangeRequired(username); + final boolean needsPasswordChange = this.userAdminHelper.isPasswordChangeRequired(username); return new AuthenticationResponseDTO(needsPasswordChange); } @@ -309,23 +310,29 @@ private void handleAuthenticationException(final AuthenticationException e) { final AuditContext auditContext = AuditContext.currentOrInternal(); switch (e.getReason()) { - case INCORRECT_PASSWORD: - case USER_NOT_FOUND: - auditLogger.warn(AUDIT_FORMAT_STRING, auditContext, BAD_USERNAME_OR_PASSWORD_MESSAGE); - throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, - BAD_USERNAME_OR_PASSWORD_MESSAGE); - case PASSWORD_CHANGE_WITH_SAME_PASSWORD: - auditLogger.warn(AUDIT_FORMAT_STRING, auditContext, PASSWORD_CHANGE_SAME_PASSWORD_MESSAGE); - throw DefaultExceptionHandler.buildWebApplicationException(Status.BAD_REQUEST, - PASSWORD_CHANGE_SAME_PASSWORD_MESSAGE); - case USER_NOT_IN_ROLE: - auditLogger.warn(AUDIT_FORMAT_STRING, auditContext, IDENTITY_NOT_IN_ROLE_MESSAGE); - throw DefaultExceptionHandler.buildWebApplicationException(Status.FORBIDDEN, - IDENTITY_NOT_IN_ROLE_MESSAGE); - default: - throw DefaultExceptionHandler.buildWebApplicationException(Status.INTERNAL_SERVER_ERROR, - "An internal error occurred"); + case INCORRECT_PASSWORD: + case USER_NOT_FOUND: + auditLogger.warn(AUDIT_FORMAT_STRING, auditContext, BAD_USERNAME_OR_PASSWORD_MESSAGE); + throw DefaultExceptionHandler.buildWebApplicationException(Status.UNAUTHORIZED, + BAD_USERNAME_OR_PASSWORD_MESSAGE); + case PASSWORD_CHANGE_WITH_SAME_PASSWORD: + auditLogger.warn(AUDIT_FORMAT_STRING, auditContext, PASSWORD_CHANGE_SAME_PASSWORD_MESSAGE); + throw DefaultExceptionHandler.buildWebApplicationException(Status.BAD_REQUEST, + PASSWORD_CHANGE_SAME_PASSWORD_MESSAGE); + case USER_NOT_IN_ROLE: + auditLogger.warn(AUDIT_FORMAT_STRING, auditContext, IDENTITY_NOT_IN_ROLE_MESSAGE); + throw DefaultExceptionHandler.buildWebApplicationException(Status.FORBIDDEN, IDENTITY_NOT_IN_ROLE_MESSAGE); + default: + throw DefaultExceptionHandler.buildWebApplicationException(Status.INTERNAL_SERVER_ERROR, + "An internal error occurred"); } } + private void invalidateCurrentSession(final HttpServletRequest request) { + final HttpSession currentSession = request.getSession(false); + + if (currentSession != null) { + currentSession.invalidate(); + } + } } diff --git a/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/Console.java b/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/Console.java index 3515441cbec..bfa587b280a 100644 --- a/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/Console.java +++ b/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/Console.java @@ -377,7 +377,10 @@ public HttpSession createNewSession(final HttpServletRequest request) { existingSession.invalidate(); } - return createSession(request); + final HttpSession newSession = createSession(request); + request.changeSessionId(); + + return newSession; } public HttpSession createSession(final HttpServletRequest request) { diff --git a/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/server/GwtSessionServiceImpl.java b/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/server/GwtSessionServiceImpl.java index 7bac33ad06c..4460cca47c7 100644 --- a/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/server/GwtSessionServiceImpl.java +++ b/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/server/GwtSessionServiceImpl.java @@ -151,7 +151,8 @@ public void updatePassword(GwtXSRFToken xsrfToken, String oldPassword, String ne throw new GwtKuraException(GwtKuraErrorCode.INTERNAL_ERROR); } - setAuthenticated(session, username); + final HttpSession newSession = Console.instance().createNewSession(request); + setAuthenticated(newSession, username); } private String getSessionUsername(HttpSession session) throws GwtKuraException { diff --git a/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/server/servlet/SslAuthenticationServlet.java b/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/server/servlet/SslAuthenticationServlet.java index 863c33bcb39..71947d52a4a 100644 --- a/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/server/servlet/SslAuthenticationServlet.java +++ b/kura/org.eclipse.kura.web2/src/main/java/org/eclipse/kura/web/server/servlet/SslAuthenticationServlet.java @@ -57,12 +57,6 @@ public SslAuthenticationServlet(final String redirectPath, final UserManager use protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { final Console console = Console.instance(); - final HttpSession session = console.createSession(req); - - if (console.getUsername(session).isPresent()) { - sendRedirect(resp, redirectPath); - return; - } final AuditContext auditContext = Console.instance().initAuditContext(req); @@ -95,6 +89,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se auditContext.getProperties().put(AuditConstants.KEY_IDENTITY.getValue(), commonName); + final HttpSession session = console.createNewSession(req); + console.setAuthenticated(session, commonName, auditContext); auditContext.getProperties().put("session.id", GwtServerUtil.getSessionIdHash(session)); @@ -104,6 +100,12 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se sendRedirect(resp, redirectPath); } catch (final Exception e) { + final HttpSession existingSession = req.getSession(false); + + if (existingSession != null) { + existingSession.invalidate(); + } + auditLogger.info("{} UI Login - Failure - Certificate login", auditContext); logger.warn("certificate authentication failed", e); sendUnauthorized(resp); diff --git a/kura/test/org.eclipse.kura.core.testutil/src/main/java/org/eclipse/kura/core/testutil/requesthandler/AbstractRequestHandlerTest.java b/kura/test/org.eclipse.kura.core.testutil/src/main/java/org/eclipse/kura/core/testutil/requesthandler/AbstractRequestHandlerTest.java index 9b793dedd1a..39517c59287 100644 --- a/kura/test/org.eclipse.kura.core.testutil/src/main/java/org/eclipse/kura/core/testutil/requesthandler/AbstractRequestHandlerTest.java +++ b/kura/test/org.eclipse.kura.core.testutil/src/main/java/org/eclipse/kura/core/testutil/requesthandler/AbstractRequestHandlerTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022, 2023 Eurotech and/or its affiliates and others + * Copyright (c) 2022, 2024 Eurotech and/or its affiliates and others * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -13,11 +13,20 @@ package org.eclipse.kura.core.testutil.requesthandler; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.net.CookieManager; +import java.net.CookieStore; +import java.net.HttpCookie; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import org.eclipse.kura.core.testutil.requesthandler.Transport.MethodSpec; import org.eclipse.kura.core.testutil.requesthandler.Transport.Response; @@ -30,6 +39,7 @@ public abstract class AbstractRequestHandlerTest { protected final Transport transport; protected Optional response = Optional.empty(); protected final TransportType transportType; + protected Map cookieSnapshot = new HashMap<>(); protected AbstractRequestHandlerTest(final Transport transport) { this.transport = transport; @@ -44,6 +54,17 @@ protected AbstractRequestHandlerTest(final Transport transport) { this.transport.init(); } + protected void givenSnapshotOfCurrentCookies() { + final CookieManager cookieManager = expectCookieManager(); + + this.cookieSnapshot = cookieManager.getCookieStore().getCookies().stream() + .collect(Collectors.toMap(c -> c.getName(), c -> (HttpCookie) c.clone())); + } + + protected void givenCookieInSnapshot(final String name) { + assertNotNull("cookie " + name + " is not in captured snapshot", this.cookieSnapshot.get(name)); + } + protected void whenRequestIsPerformed(final MethodSpec method, final String resource) { this.response = Optional.of(this.transport.runRequest(resource, method)); } @@ -52,6 +73,35 @@ protected void whenRequestIsPerformed(final MethodSpec method, final String reso this.response = Optional.of(this.transport.runRequest(resource, method, body)); } + protected void whenCookieIsRestoredFromSnapshot(final String name) { + final CookieManager cookieManager = expectCookieManager(); + final CookieStore cookieStore = cookieManager.getCookieStore(); + + final HttpCookie cookie = Optional.ofNullable(this.cookieSnapshot.get(name)) + .orElseThrow(() -> new IllegalStateException("cookie " + name + " is not in the captured snapshot")); + + Optional targetURI = Optional.empty(); + + for (final URI uri : cookieStore.getURIs()) { + for (final HttpCookie storedCookie : cookieStore.get(uri)) { + if (Objects.equals(storedCookie.getName(), name)) { + targetURI = Optional.of(uri); + break; + } + } + } + + cookieStore.add(targetURI.orElseThrow(() -> new IllegalStateException("Cannot determine cookie URI")), cookie); + } + + protected void thenCurrentCookieDiffersFromPreviousSnapshot(final String name) { + final Optional cookieInSnapshot = Optional.ofNullable(this.cookieSnapshot.get(name)); + final Optional currentCookie = expectCookieManager().getCookieStore().getCookies().stream() + .filter(c -> name.equals(c.getName())).findAny(); + + assertNotEquals(cookieInSnapshot.map(HttpCookie::getValue), currentCookie.map(HttpCookie::getValue)); + } + protected void thenRequestSucceeds() { final Response currentResponse = expectResponse(); @@ -116,4 +166,5 @@ protected JsonObject expectJsonResponse() { public TransportType getTransportType() { return this.transportType; } + } diff --git a/kura/test/org.eclipse.kura.core.testutil/src/main/java/org/eclipse/kura/core/testutil/requesthandler/RestTransport.java b/kura/test/org.eclipse.kura.core.testutil/src/main/java/org/eclipse/kura/core/testutil/requesthandler/RestTransport.java index 18331c8b0df..d2e11c95f27 100644 --- a/kura/test/org.eclipse.kura.core.testutil/src/main/java/org/eclipse/kura/core/testutil/requesthandler/RestTransport.java +++ b/kura/test/org.eclipse.kura.core.testutil/src/main/java/org/eclipse/kura/core/testutil/requesthandler/RestTransport.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021, 2023 Eurotech and/or its affiliates and others + * Copyright (c) 2021, 2024 Eurotech and/or its affiliates and others * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -191,8 +191,8 @@ private void storeCookies(final String urlPrefix, final String relativeUri, fina private void setCookies(final String urlPrefix, final String relativeUri, final HttpURLConnection connection) throws IOException, URISyntaxException { - for (final Entry> e : cookieManager.get(new URI(urlPrefix + relativeUri), - connection.getRequestProperties()).entrySet()) { + for (final Entry> e : cookieManager + .get(new URI(urlPrefix + relativeUri), connection.getRequestProperties()).entrySet()) { if (!e.getValue().isEmpty()) { connection.setRequestProperty(e.getKey(), e.getValue().stream().collect(Collectors.joining(","))); } diff --git a/kura/test/org.eclipse.kura.rest.provider.test/src/main/java/org/eclipse/kura/rest/provider/test/RestServiceTest.java b/kura/test/org.eclipse.kura.rest.provider.test/src/main/java/org/eclipse/kura/rest/provider/test/RestServiceTest.java index 82e63d2a830..2cedb2eb76a 100644 --- a/kura/test/org.eclipse.kura.rest.provider.test/src/main/java/org/eclipse/kura/rest/provider/test/RestServiceTest.java +++ b/kura/test/org.eclipse.kura.rest.provider.test/src/main/java/org/eclipse/kura/rest/provider/test/RestServiceTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022, 2023 Eurotech and/or its affiliates and others + * Copyright (c) 2022, 2024 Eurotech and/or its affiliates and others * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -354,8 +354,7 @@ public void shouldReturnXSRFTokenIfSessionIsOpen() { givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", "{\"username\":\"foo\",\"password\":\"bar\"}"); - whenRequestIsPerformed("http", 8080, new MethodSpec("GET"), "/session/v1/xsrfToken", - null); + whenRequestIsPerformed("http", 8080, new MethodSpec("GET"), "/session/v1/xsrfToken", null); thenResponseCodeIs(200); } @@ -365,8 +364,7 @@ public void shouldNotReturnXSRFTokenIfSessionIsNotOpen() { givenIdentity("foo", Optional.of("bar"), Collections.emptyList()); givenNoBasicCredentials(); - whenRequestIsPerformed("http", 8080, new MethodSpec("GET"), "/session/v1/xsrfToken", - null); + whenRequestIsPerformed("http", 8080, new MethodSpec("GET"), "/session/v1/xsrfToken", null); thenResponseCodeIs(401); } @@ -407,8 +405,7 @@ public void shouldSupportLogout() { "{\"username\":\"foo\",\"password\":\"bar\"}"); givenXsrfToken(); givenSuccessfulRequest(new MethodSpec("GET"), "/requireAssets"); - givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/logout", - null); + givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/logout", null); whenRequestIsPerformed(new MethodSpec("GET"), "/requireAssets"); @@ -422,8 +419,7 @@ public void shouldRequireXsrfTokenForLogout() { givenIdentity("foo", Optional.of("bar"), Arrays.asList("rest.assets")); givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", "{\"username\":\"foo\",\"password\":\"bar\"}"); - whenRequestIsPerformed("http", 8080, new MethodSpec("POST"), "/session/v1/logout", - null); + whenRequestIsPerformed("http", 8080, new MethodSpec("POST"), "/session/v1/logout", null); thenResponseCodeIs(401); } @@ -796,8 +792,7 @@ public void shouldReturnCurrentIdentityInfoWithNoPermissions() { whenRequestIsPerformed("http", 8080, new MethodSpec("GET"), "/session/v1/currentIdentity", null); thenRequestSucceeds(); - thenResponseBodyEqualsJson( - "{\"name\":\"nopermissions\",\"passwordChangeNeeded\":false,\"permissions\":[]}"); + thenResponseBodyEqualsJson("{\"name\":\"nopermissions\",\"passwordChangeNeeded\":false,\"permissions\":[]}"); } @Test @@ -830,6 +825,269 @@ public void shouldReturnMessageIfLoginBannnerIsEnabled() { "{\"passwordAuthenticationEnabled\":true,\"certificateAuthenticationEnabled\":false,\"message\":\"foo\"}"); } + @Test + public void shouldChangeSessionCookieRepeatingPasswordAuthentication() { + givenConfiguration("org.eclipse.kura.web.Console", // + "access.banner.enabled", false); + givenService(new RequiresAssetsRole()); + givenIdentity("foo", Optional.of("bar"), Collections.emptyList()); + givenNoBasicCredentials(); + + givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + whenRequestIsPerformed("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + + thenResponseCodeIs(200); + thenResponseBodyEqualsJson("{\"passwordChangeNeeded\":false}"); + thenCurrentCookieDiffersFromPreviousSnapshot("JSESSIONID"); + } + + @Test + public void shouldChangeSessionCookieAfterPasswordChange() { + givenConfiguration("org.eclipse.kura.web.Console", // + "new.password.min.length", 1, // + "new.password.require.digits", false, // + "new.password.require.special.characters", false, // + "new.password.require.both.cases", false, // + "access.banner.enabled", false); + givenService(new RequiresAssetsRole()); + givenIdentity("foo", Optional.of("bar"), Collections.emptyList(), true); + givenNoBasicCredentials(); + + givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + givenXsrfToken("http", 8080); + + whenRequestIsPerformed("http", 8080, new MethodSpec("POST"), "/session/v1/changePassword", + "{\"currentPassword\":\"bar\",\"newPassword\":\"baz\"}"); + + thenRequestSucceeds(); + thenCurrentCookieDiffersFromPreviousSnapshot("JSESSIONID"); + } + + @Test + public void shouldChangeSessionCookieRepeatingCertificateAuthentication() { + givenRestServiceConfiguration(Collections.singletonMap("allowed.ports", new Integer[] { 8080, 9999 })); + givenNoBasicCredentials(); + givenIdentity("foo", Optional.empty(), Arrays.asList("rest.assets")); + givenService(new RequiresAssetsRole()); + givenCA("clientCA"); + givenCA("serverCA"); + givenKeystoreService("clientKeystore"); + givenCACertificateInKeystore("clientKeystore", "serverCA"); + givenKeystoreService("serverKeystore"); + givenCACertificateInKeystore("serverKeystore", "clientCA"); + givenKeyPairInKeystore("clientKeystore", "clientCA", "foo"); + givenKeyPairInKeystore("serverKeystore", "serverCA", "serverCert"); + givenHttpServiceClientCertAuthEnabled("serverKeystore", 9999); + givenClientKeystore("clientKeystore"); + + givenSuccessfulRequest("https", 9999, new MethodSpec("POST"), "/session/v1/login/certificate", null); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + whenRequestIsPerformed("https", 9999, new MethodSpec("POST"), "/session/v1/login/certificate", null); + + thenRequestSucceeds(); + thenCurrentCookieDiffersFromPreviousSnapshot("JSESSIONID"); + } + + @Test + public void shouldChangeSessionCookieAuthenticatingWithPasswordAndThenWithCertificateOnDifferentPort() { + givenRestServiceConfiguration(Collections.singletonMap("allowed.ports", new Integer[] { 8080, 9999 })); + givenNoBasicCredentials(); + givenIdentity("foo", Optional.of("bar"), Arrays.asList("rest.assets")); + givenService(new RequiresAssetsRole()); + givenCA("clientCA"); + givenCA("serverCA"); + givenKeystoreService("clientKeystore"); + givenCACertificateInKeystore("clientKeystore", "serverCA"); + givenKeystoreService("serverKeystore"); + givenCACertificateInKeystore("serverKeystore", "clientCA"); + givenKeyPairInKeystore("clientKeystore", "clientCA", "foo"); + givenKeyPairInKeystore("serverKeystore", "serverCA", "serverCert"); + givenHttpServiceClientCertAuthEnabled("serverKeystore", 9999); + givenClientKeystore("clientKeystore"); + + givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + whenRequestIsPerformed("https", 9999, new MethodSpec("POST"), "/session/v1/login/certificate", null); + + thenRequestSucceeds(); + thenCurrentCookieDiffersFromPreviousSnapshot("JSESSIONID"); + } + + @Test + public void shouldChangeSessionCookieAuthenticatingWithCertificateAndThenWithPasswordOnDifferentPort() { + givenRestServiceConfiguration(Collections.singletonMap("allowed.ports", new Integer[] { 8080, 9999 })); + givenNoBasicCredentials(); + givenIdentity("foo", Optional.of("bar"), Arrays.asList("rest.assets")); + givenService(new RequiresAssetsRole()); + givenCA("clientCA"); + givenCA("serverCA"); + givenKeystoreService("clientKeystore"); + givenCACertificateInKeystore("clientKeystore", "serverCA"); + givenKeystoreService("serverKeystore"); + givenCACertificateInKeystore("serverKeystore", "clientCA"); + givenKeyPairInKeystore("clientKeystore", "clientCA", "foo"); + givenKeyPairInKeystore("serverKeystore", "serverCA", "serverCert"); + givenHttpServiceClientCertAuthEnabled("serverKeystore", 9999); + givenClientKeystore("clientKeystore"); + + givenSuccessfulRequest("https", 9999, new MethodSpec("POST"), "/session/v1/login/certificate", null); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + whenRequestIsPerformed("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + thenRequestSucceeds(); + thenCurrentCookieDiffersFromPreviousSnapshot("JSESSIONID"); + } + + @Test + public void shouldChangeSessionCookieAuthenticatingWithPasswordAndThenWithCertificateOnSamePort() { + givenRestServiceConfiguration(Collections.singletonMap("allowed.ports", new Integer[] { 8080, 9999 })); + givenNoBasicCredentials(); + givenIdentity("foo", Optional.of("bar"), Arrays.asList("rest.assets")); + givenService(new RequiresAssetsRole()); + givenCA("clientCA"); + givenCA("serverCA"); + givenKeystoreService("clientKeystore"); + givenCACertificateInKeystore("clientKeystore", "serverCA"); + givenKeystoreService("serverKeystore"); + givenCACertificateInKeystore("serverKeystore", "clientCA"); + givenKeyPairInKeystore("clientKeystore", "clientCA", "foo"); + givenKeyPairInKeystore("serverKeystore", "serverCA", "serverCert"); + givenHttpServiceClientCertAuthEnabled("serverKeystore", 9999); + givenClientKeystore("clientKeystore"); + + givenSuccessfulRequest("https", 9999, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + whenRequestIsPerformed("https", 9999, new MethodSpec("POST"), "/session/v1/login/certificate", null); + + thenRequestSucceeds(); + thenCurrentCookieDiffersFromPreviousSnapshot("JSESSIONID"); + } + + @Test + public void shouldChangeSessionCookieAuthenticatingWithCertificateAndThenWithPasswordOnSamePort() { + givenRestServiceConfiguration(Collections.singletonMap("allowed.ports", new Integer[] { 8080, 9999 })); + givenNoBasicCredentials(); + givenIdentity("foo", Optional.of("bar"), Arrays.asList("rest.assets")); + givenService(new RequiresAssetsRole()); + givenCA("clientCA"); + givenCA("serverCA"); + givenKeystoreService("clientKeystore"); + givenCACertificateInKeystore("clientKeystore", "serverCA"); + givenKeystoreService("serverKeystore"); + givenCACertificateInKeystore("serverKeystore", "clientCA"); + givenKeyPairInKeystore("clientKeystore", "clientCA", "foo"); + givenKeyPairInKeystore("serverKeystore", "serverCA", "serverCert"); + givenHttpServiceClientCertAuthEnabled("serverKeystore", 9999); + givenClientKeystore("clientKeystore"); + + givenSuccessfulRequest("https", 9999, new MethodSpec("POST"), "/session/v1/login/certificate", null); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + whenRequestIsPerformed("https", 9999, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + thenRequestSucceeds(); + thenCurrentCookieDiffersFromPreviousSnapshot("JSESSIONID"); + } + + @Test + public void shouldNotLoginWithOldCookieAfterNewLogin() { + givenConfiguration("org.eclipse.kura.web.Console", // + "access.banner.enabled", false); + givenService(new RequiresAssetsRole()); + givenIdentity("foo", Optional.of("bar"), Collections.emptyList()); + givenNoBasicCredentials(); + + givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + + whenCookieIsRestoredFromSnapshot("JSESSIONID"); + whenRequestIsPerformed("http", 8080, new MethodSpec("GET"), "/session/v1/xsrfToken", null); + + thenResponseCodeIs(401); + + } + + @Test + public void shouldInvalidateSessionAfterFailedPasswordLogin() { + givenConfiguration("org.eclipse.kura.web.Console", // + "access.banner.enabled", false); + givenService(new RequiresAssetsRole()); + givenIdentity("foo", Optional.of("bar"), Collections.emptyList()); + givenNoBasicCredentials(); + + givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + givenXsrfToken(); + + whenRequestIsPerformed("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"baz\"}"); + + whenCookieIsRestoredFromSnapshot("JSESSIONID"); + whenRequestIsPerformed("http", 8080, new MethodSpec("GET"), "/session/v1/xsrfToken", null); + + thenResponseCodeIs(401); + } + + @Test + public void shouldInvalidateSessionAfterFailedCertificateLogin() { + givenRestServiceConfiguration(Collections.singletonMap("allowed.ports", new Integer[] { 8080, 9999 })); + givenNoBasicCredentials(); + givenIdentity("foo", Optional.of("bar"), Arrays.asList("rest.assets")); + givenService(new RequiresAssetsRole()); + givenCA("clientCA"); + givenCA("serverCA"); + givenKeystoreService("clientKeystore"); + givenCACertificateInKeystore("clientKeystore", "serverCA"); + givenKeystoreService("serverKeystore"); + givenCACertificateInKeystore("serverKeystore", "clientCA"); + givenKeyPairInKeystore("clientKeystore", "clientCA", "bar"); + givenKeyPairInKeystore("serverKeystore", "serverCA", "serverCert"); + givenHttpServiceClientCertAuthEnabled("serverKeystore", 9999); + givenClientKeystore("clientKeystore"); + + givenSuccessfulRequest("http", 8080, new MethodSpec("POST"), "/session/v1/login/password", + "{\"username\":\"foo\",\"password\":\"bar\"}"); + givenSnapshotOfCurrentCookies(); + givenCookieInSnapshot("JSESSIONID"); + + givenXsrfToken(); + + whenRequestIsPerformed("https", 9999, new MethodSpec("POST"), "/session/v1/login/certificate", null); + + whenCookieIsRestoredFromSnapshot("JSESSIONID"); + whenRequestIsPerformed("http", 8080, new MethodSpec("GET"), "/session/v1/xsrfToken", null); + + thenResponseCodeIs(401); + } + private List> registeredServices = new ArrayList<>(); private CompletableFuture providerEnabled = new CompletableFuture<>(); private CompletableFuture providerDisabled = new CompletableFuture<>(); @@ -876,8 +1134,8 @@ private void givenKeystoreService(final String pid) { new File(directoryPath.toFile(), System.nanoTime() + ".ks").getAbsolutePath()); final KeystoreService keystoreService = ServiceUtil - .createFactoryConfiguration(configurationService, KeystoreService.class, - pid, "org.eclipse.kura.core.keystore.FilesystemKeystoreServiceImpl", properties) + .createFactoryConfiguration(configurationService, KeystoreService.class, pid, + "org.eclipse.kura.core.keystore.FilesystemKeystoreServiceImpl", properties) .get(30, TimeUnit.SECONDS); this.keystoreServices.put(pid, keystoreService); @@ -915,9 +1173,8 @@ private void givenKeyPairInKeystore(final String keystorePid, final String caCN, final KeystoreService keystoreService = this.keystoreServices.get(keystorePid); final KeyPair keyPair = TestCA.generateKeyPair(); - final X509Certificate clientCertificate = testCA.createAndSignCertificate(CertificateCreationOptions - .builder(new X500Name("cn=" + certCN + ", dc=bar.com")) - .build(), keyPair); + final X509Certificate clientCertificate = testCA.createAndSignCertificate( + CertificateCreationOptions.builder(new X500Name("cn=" + certCN + ", dc=bar.com")).build(), keyPair); keystoreService.setEntry(certCN, new PrivateKeyEntry(keyPair.getPrivate(), new Certificate[] { clientCertificate, testCA.getCertificate() })); @@ -932,8 +1189,7 @@ private void givenHttpServiceClientCertAuthEnabled(final String keystorePid, fin .trackService(ConfigurationService.class, Optional.empty()).get(30, TimeUnit.SECONDS); final Map properties = new HashMap<>(); - properties.put("KeystoreService.target", - "(kura.service.pid=" + keystorePid + ")"); + properties.put("KeystoreService.target", "(kura.service.pid=" + keystorePid + ")"); properties.put("https.client.auth.ports", new Integer[] { port }); configurationService.updateConfiguration("org.eclipse.kura.http.server.manager.HttpService", properties); @@ -964,14 +1220,13 @@ private void givenHttpServiceClientCertAuthDisabled() { private void givenConfiguration(final String pid, final Object... properties) { try { - final ConfigurationAdmin configAdmin = ServiceUtil - .trackService(ConfigurationAdmin.class, Optional.empty()).get(30, TimeUnit.SECONDS); + final ConfigurationAdmin configAdmin = ServiceUtil.trackService(ConfigurationAdmin.class, Optional.empty()) + .get(30, TimeUnit.SECONDS); final Configuration configuration = configAdmin.getConfiguration(pid, "?"); final Dictionary configurationProperties = Optional - .ofNullable(configuration.getProperties()) - .orElseGet(Hashtable::new); + .ofNullable(configuration.getProperties()).orElseGet(Hashtable::new); final Iterator iter = Arrays.asList(properties).iterator(); @@ -1088,14 +1343,12 @@ private void givenAuthenticationProvider(final AuthenticationProvider provider) } private void givenSuccessfulRequest(final String proto, final int port, final MethodSpec method, - final String resource, - final String body) { + final String resource, final String body) { whenRequestIsPerformed(proto, port, method, resource, body); thenRequestSucceeds(); } - private void givenSuccessfulRequest(final MethodSpec method, - final String resource) { + private void givenSuccessfulRequest(final MethodSpec method, final String resource) { whenRequestIsPerformed(method, resource); thenRequestSucceeds(); } @@ -1113,8 +1366,7 @@ private void givenDelay(final long amount, final TimeUnit timeUnit) { } private void whenRequestIsPerformed(final String proto, final int port, final MethodSpec method, - final String resource, - final String body) { + final String resource, final String body) { final RestTransport restTransport = (RestTransport) this.transport; this.response = Optional @@ -1129,8 +1381,7 @@ private void whenXsrfTokenIsObtained(final String protocol, final int port) { final RestTransport restTransport = (RestTransport) this.transport; final Response response = restTransport.runRequest(protocol + "://localhost:" + port + "/services", - "/session/v1/xsrfToken", - new MethodSpec("GET"), null); + "/session/v1/xsrfToken", new MethodSpec("GET"), null); final JsonObject object = Json .parse(response.getBody().orElseThrow(() -> new IllegalStateException("no response body"))).asObject(); diff --git a/kura/test/pom.xml b/kura/test/pom.xml index 4fc4728554b..93ded67171c 100644 --- a/kura/test/pom.xml +++ b/kura/test/pom.xml @@ -141,6 +141,7 @@ ${tycho.testArgLine} ${tycho.surefire.testenv.args} + -Dlog4j.configurationFile=file:${kura.basedir}/emulator/org.eclipse.kura.emulator/src/main/resources/log4j.xml @@ -272,6 +273,14 @@ p2-installable-unit org.eclipse.kura.emulator.position + + p2-installable-unit + org.apache.logging.log4j.slf4j.impl + + + p2-installable-unit + org.apache.logging.log4j.core + @@ -381,17 +390,17 @@ org.apache.logging.log4j.api - 4 + 1 true org.apache.logging.log4j.core - 4 + 1 true org.apache.logging.log4j.slf4j.impl - 4 + 1 true