diff --git a/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResource.java b/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResource.java index 6b6b39ab..86e5fd4e 100644 --- a/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResource.java +++ b/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResource.java @@ -21,18 +21,35 @@ import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.openapi.annotations.Operation; import com.redhat.ecosystemappeng.exploitiq.service.UserService; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + @Path("/user") public class TokenResource { @Inject UserService userService; + @ConfigProperty(name = "quarkus.oidc.auth-server-url") + Optional authServerUrl; + + @ConfigProperty(name = "quarkus.oidc.client-id") + Optional clientId; + + @ConfigProperty(name = "cognito.domain") + Optional cognitoDomain; + @GET @Produces("application/json") @Operation(hidden = true) @@ -41,17 +58,59 @@ public String getUserName() { } /** - * Performs a local logout using the standard 'Clear-Site-Data' header. - * This feature is available only in secure contexts (HTTPS) - * https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Clear-Site-Data + * Logout endpoint with AWS Cognito support. + * For Cognito (when cognito.domain is configured), redirects to Cognito's logout endpoint. + * For other OIDC providers, performs local logout with Clear-Site-Data header. + * + * NOTE: Cognito behind a reverse proxy is not currently supported. */ @POST @Path("/logout") @Produces(MediaType.TEXT_HTML) @Operation(hidden = true) @PermitAll - public Response logout() { - return Response.ok(LOGGED_OUT_HTML) + public Response logout(@Context UriInfo uriInfo) { + + if (cognitoDomain.isPresent()) { + String domain = cognitoDomain.get().trim(); + + if (domain.endsWith("/")) { + domain = domain.substring(0, domain.length() - 1); + } + + // Build logout redirect URI to the logged-out page + // uriInfo.getBaseUri() returns https://host/api/v1/, we need https://host/api/v1/user/logged-out + URI baseUri = uriInfo.getBaseUri(); + String logoutRedirectUri = baseUri.getScheme() + "://" + baseUri.getAuthority() + baseUri.getPath() + "user/logged-out"; + + // Build Cognito logout URL with required parameters + String cognitoLogoutUrl = String.format("%s/logout?client_id=%s&logout_uri=%s", + domain, + clientId.get(), + URLEncoder.encode(logoutRedirectUri, StandardCharsets.UTF_8)); + + return buildLogoutResponse(Response.seeOther(URI.create(cognitoLogoutUrl))); + } + + // For non-Cognito providers, perform local logout + return buildLogoutResponse(Response.ok(LOGGED_OUT_HTML)); + } + + /** + * Logged-out page endpoint. + * Displays the logout success message after Cognito completes its logout. + */ + @GET + @Path("/logged-out") + @Produces(MediaType.TEXT_HTML) + @Operation(hidden = true) + @PermitAll + public Response loggedOut() { + return buildLogoutResponse(Response.ok(LOGGED_OUT_HTML)); + } + + private Response buildLogoutResponse(Response.ResponseBuilder responseBuilder) { + return responseBuilder .header("Clear-Site-Data", "\"cookies\", \"storage\"") .build(); } diff --git a/src/main/resources/META-INF/resources/error/403.html b/src/main/resources/META-INF/resources/error/403.html index 400e92c6..41f0edea 100644 --- a/src/main/resources/META-INF/resources/error/403.html +++ b/src/main/resources/META-INF/resources/error/403.html @@ -21,10 +21,14 @@ diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a90ddb19..b3b2364b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -142,13 +142,16 @@ quarkus.oidc.authentication.java-script-auto-redirect=false # ============================================================================== # Allow logout for all authenticated users (even without roles) -quarkus.http.auth.permission.logout.paths=/api/v1/user/logout +exploitiq.security.public-logout-paths=/api/v1/user/logout,/api/v1/user/logged-out +quarkus.http.auth.permission.logout.paths=${exploitiq.security.public-logout-paths} quarkus.http.auth.permission.logout.policy=permit +quarkus.http.auth.permission.logout.order=1 # Allow health endpoints (for monitoring) quarkus.http.auth.permission.management.paths=/health,/health/* quarkus.http.auth.permission.management.policy=permit +quarkus.http.auth.permission.management.order=1 # Allow dev UI in development mode diff --git a/src/main/webui/package-lock.json b/src/main/webui/package-lock.json index 34df507a..a38f554a 100644 --- a/src/main/webui/package-lock.json +++ b/src/main/webui/package-lock.json @@ -89,7 +89,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -375,6 +374,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.0.0" }, @@ -403,6 +403,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", "integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" @@ -417,6 +418,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" @@ -431,6 +433,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.0.0" }, @@ -2055,7 +2058,6 @@ "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2071,7 +2073,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2150,7 +2151,6 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -2452,7 +2452,6 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2642,7 +2641,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", @@ -3280,7 +3278,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5718,7 +5715,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5737,7 +5733,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5935,7 +5930,8 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/resolve-from": { "version": "4.0.0", @@ -6574,7 +6570,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6833,7 +6828,6 @@ "resolved": "https://registry.npmjs.org/victory-area/-/victory-area-37.3.6.tgz", "integrity": "sha512-wVC8LKrZJLiSySNuJLRCB449qZTsPiRyzLlNoJwe21y+XA/a2HJbmJSeywmo8P153aX8viKe1H8ygDsTFXQhHw==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -6851,7 +6845,6 @@ "resolved": "https://registry.npmjs.org/victory-axis/-/victory-axis-37.3.6.tgz", "integrity": "sha512-Vi0dZvgmXmnCdoqc49WckeG5cMXnl7FTtqVhXu9JweA9cgCnkZabBd5mRvAjblb3Lo4j0HZCSPKHYWUPW70qZg==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -6868,7 +6861,6 @@ "resolved": "https://registry.npmjs.org/victory-bar/-/victory-bar-37.3.6.tgz", "integrity": "sha512-jdATFRWL1LUW/yEpKWx/aId2BiU2o1pPF9+Kh1TFISBduJoI4ZqvZD90H1QK4f/z50PikqiqiDECaKoKM1jfOQ==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -6886,7 +6878,6 @@ "resolved": "https://registry.npmjs.org/victory-box-plot/-/victory-box-plot-37.3.6.tgz", "integrity": "sha512-GOucnD63h14ScBuISC/nd1GBTEx6gIZfLE+0P0gyeH1poBKq0trTTvpQDvAMuGR8zICfEETG3ltmUMCwRrFyUg==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -6971,7 +6962,6 @@ "resolved": "https://registry.npmjs.org/victory-chart/-/victory-chart-37.3.6.tgz", "integrity": "sha512-IkPo/W4AJ7bPu902TGER09OseR9ODm+FQAKfOBw4JsdEhZZ7BiG9zgd/25+x0r5EsTLu81CYGQVkBa+ZazcOlA==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", @@ -6992,7 +6982,6 @@ "resolved": "https://registry.npmjs.org/victory-core/-/victory-core-37.3.6.tgz", "integrity": "sha512-aFgO6KokxPbUCPznZP5UPhOdI22pMuwDXKDt6eoQOnkVim66Ia+K95TQar2nwVKGYV5j26aKVf/n9blwphGJRw==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.21", "react-fast-compare": "^3.2.0", @@ -7010,7 +6999,6 @@ "resolved": "https://registry.npmjs.org/victory-create-container/-/victory-create-container-37.3.6.tgz", "integrity": "sha512-Uf5bFQvqUsXCjqpvBW4LhrdrHkM6dBqxYgub6FCsBb86f84xZQ3vY7jFkg/JfvF0oGKMoWXYYrYLC1sk+fcWVA==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-brush-container": "37.3.6", @@ -7032,7 +7020,6 @@ "resolved": "https://registry.npmjs.org/victory-cursor-container/-/victory-cursor-container-37.3.6.tgz", "integrity": "sha512-+Oiw57d5nE+iq8As8RvepknzmNtKq1Gsc50u1X3IRd4jXtX8zqZrgXGlVZ+BP/tkLsWnGYVjKulwKBf2oaEUuw==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -7065,7 +7052,6 @@ "resolved": "https://registry.npmjs.org/victory-group/-/victory-group-37.3.6.tgz", "integrity": "sha512-kgy/Azl5BxwlJAV0KDPGypv35TMrOD1J2ZxnJW2Wyyq+e8i0GGBIv5MoBzou64BRsDlS9V0CYRIjnkHgrBpB5w==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", @@ -7103,7 +7089,6 @@ "resolved": "https://registry.npmjs.org/victory-legend/-/victory-legend-37.3.6.tgz", "integrity": "sha512-vRRrhj3/ENqKVLdaBMzEmR83N6BOjox1bthYT1eJjN2H5SIK35bxn30IkiV/Pz3y627EqZe4TAWaxc0jiJlCiA==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -7120,7 +7105,6 @@ "resolved": "https://registry.npmjs.org/victory-line/-/victory-line-37.3.6.tgz", "integrity": "sha512-Ke817uf/qFbN9jU7Dba7CrcHXYO5wAZuKKnyeHJmLDeQeFST0773xejnIuC+dBgZipjFr4KIbSd+VcUafFNE1g==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -7138,7 +7122,6 @@ "resolved": "https://registry.npmjs.org/victory-pie/-/victory-pie-37.3.6.tgz", "integrity": "sha512-tvdgAZ/HQWlo3KDDe0XAVbizHuaNMbgkkiF7zfA7Ww+3bHSs+0P9dsDtK2xP365D8gBCOv8pWmuzvKRhzNbqeA==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -7172,7 +7155,6 @@ "resolved": "https://registry.npmjs.org/victory-scatter/-/victory-scatter-37.3.6.tgz", "integrity": "sha512-fp95zMTPXgW1cmTowzDXhn+KxePMVDrzU0lotsHQMdBV7eB+ioXdu9hORlx4VHmMYg2ihsGwRTF+VAZ7rGxphA==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -7223,7 +7205,6 @@ "resolved": "https://registry.npmjs.org/victory-stack/-/victory-stack-37.3.6.tgz", "integrity": "sha512-ldod04RdqGJGH5p5eWXCofdTkbhZqIp3iwW7NpxSbMDLs8zPQIVvDFVtuJgMwQiC5vnIpbhMmxVeFbr8m64ZKA==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", @@ -7242,7 +7223,6 @@ "resolved": "https://registry.npmjs.org/victory-tooltip/-/victory-tooltip-37.3.6.tgz", "integrity": "sha512-vqaJS9noauOqDDBBAV9Ln9duOY/i17h1DCfCPAqhwPFyvFbwKvAub9zPTeYWAm/14VvWX5O/0yekFCVbcC7hjg==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -7298,7 +7278,6 @@ "resolved": "https://registry.npmjs.org/victory-voronoi-container/-/victory-voronoi-container-37.3.6.tgz", "integrity": "sha512-qAAG0rMuK7A4EoJ4cyUk5wNdOW+HuCXNKPOko+hYK6wWOYXJvFhiglYyA85a695YyAXECc6JyJS/crm4IOEFag==", "license": "MIT", - "peer": true, "dependencies": { "delaunay-find": "0.0.6", "lodash": "^4.17.19", @@ -7318,7 +7297,6 @@ "resolved": "https://registry.npmjs.org/victory-zoom-container/-/victory-zoom-container-37.3.6.tgz", "integrity": "sha512-AGL+k20mI44OL5b0VgIxlmnNSefIoFmbbim5NraPmIxbtns9qQW/56ivIncJcYomBungIx99gUpsEpcQaMNHgQ==", "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -7336,7 +7314,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResourceLogoutCognitoTest.java b/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResourceLogoutCognitoTest.java new file mode 100644 index 00000000..081688ba --- /dev/null +++ b/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResourceLogoutCognitoTest.java @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, Red Hat Inc. & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.redhat.ecosystemappeng.exploitiq.rest; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.restassured.RestAssured; + +import java.util.Map; + +/** + * Tests for {@link TokenResource} logout endpoint with AWS Cognito configuration. + * Verifies that Cognito logout redirects to the correct URL with required parameters. + */ +@QuarkusTest +@TestProfile(TokenResourceLogoutCognitoTest.CognitoProfile.class) +class TokenResourceLogoutCognitoTest { + + @BeforeEach + void configureRestAssured() { + RestApiTestFixture.configureRestAssuredIfExternal(); + } + + @Test + void logoutWithCognitoRedirectsToCognitoLogoutEndpoint() { + RestAssured.given() + .redirects().follow(false) + .when() + .post("/api/v1/user/logout") + .then() + .statusCode(303) + .header("Location", containsString("https://test-domain.auth.us-east-1.amazoncognito.com/logout")) + .header("Location", containsString("client_id=test-client-id")) + .header("Location", containsString("logout_uri=")) + .header("Clear-Site-Data", equalTo("\"cookies\", \"storage\"")); + } + + @Test + void logoutWithCognitoEncodesLogoutUriParameter() { + String location = RestAssured.given() + .redirects().follow(false) + .when() + .post("/api/v1/user/logout") + .then() + .statusCode(303) + .extract() + .header("Location"); + + // Verify logout_uri is URL-encoded (contains %3A for : and %2F for /) + assert location.contains("logout_uri=http%3A%2F%2F"); + } + + @Test + void logoutWithCognitoRedirectsToLoggedOutPage() { + String location = RestAssured.given() + .redirects().follow(false) + .when() + .post("/api/v1/user/logout") + .then() + .statusCode(303) + .extract() + .header("Location"); + + // Verify the logout_uri parameter points to /api/v1/user/logged-out + assert location.contains("user%2Flogged-out") || location.contains("user/logged-out"); + } + + @Test + void logoutWithCognitoHandlesTrailingSlashInDomain() { + // This test uses a profile with trailing slash in cognito.domain + // The implementation should strip it + RestAssured.given() + .redirects().follow(false) + .when() + .post("/api/v1/user/logout") + .then() + .statusCode(303) + .header("Location", containsString("amazoncognito.com/logout")) + .header("Location", containsString("/logout?")); // Verify no double slash before query params + } + + @Test + void loggedOutPageReturnsSuccessHtml() { + RestAssured.given() + .when() + .get("/api/v1/user/logged-out") + .then() + .statusCode(200) + .contentType("text/html") + .body(containsString("Successfully Logged Out")) + .body(containsString("ExploitIQ")) + .body(containsString("Login Again")) + .header("Clear-Site-Data", equalTo("\"cookies\", \"storage\"")); + } + + public static class CognitoProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.oidc.enabled", "false", + "quarkus.oidc.auth-server-url", "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_TestPool", + "quarkus.oidc.client-id", "test-client-id", + "cognito.domain", "https://test-domain.auth.us-east-1.amazoncognito.com/" + ); + } + } +} diff --git a/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResourceLogoutMissingConfigTest.java b/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResourceLogoutMissingConfigTest.java new file mode 100644 index 00000000..86f23d0c --- /dev/null +++ b/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResourceLogoutMissingConfigTest.java @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, Red Hat Inc. & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.redhat.ecosystemappeng.exploitiq.rest; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.restassured.RestAssured; + +import java.util.Map; + +/** + * Tests for {@link TokenResource} logout endpoint with missing or minimal OIDC configuration. + * Verifies fallback behavior when configuration is incomplete. + */ +@QuarkusTest +@TestProfile(TokenResourceLogoutMissingConfigTest.MinimalConfigProfile.class) +class TokenResourceLogoutMissingConfigTest { + + @BeforeEach + void configureRestAssured() { + RestApiTestFixture.configureRestAssuredIfExternal(); + } + + @Test + void logoutWithoutAnyOidcConfigReturnsLocalLogout() { + RestAssured.given() + .when() + .post("/api/v1/user/logout") + .then() + .statusCode(200) + .contentType("text/html") + .body(containsString("Successfully Logged Out")) + .header("Clear-Site-Data", equalTo("\"cookies\", \"storage\"")); + } + + @Test + void loggedOutPageWorksWithoutOidcConfig() { + RestAssured.given() + .when() + .get("/api/v1/user/logged-out") + .then() + .statusCode(200) + .contentType("text/html") + .body(containsString("Successfully Logged Out")) + .header("Clear-Site-Data", equalTo("\"cookies\", \"storage\"")); + } + + public static class MinimalConfigProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.oidc.enabled", "false" + // No auth-server-url, client-id, or cognito.domain + ); + } + } +} diff --git a/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResourceLogoutNonCognitoTest.java b/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResourceLogoutNonCognitoTest.java new file mode 100644 index 00000000..a307f7f7 --- /dev/null +++ b/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/TokenResourceLogoutNonCognitoTest.java @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, Red Hat Inc. & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.redhat.ecosystemappeng.exploitiq.rest; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.restassured.RestAssured; + +import java.util.Map; + +/** + * Tests for {@link TokenResource} logout endpoint with non-Cognito OIDC provider. + * Verifies that non-Cognito logout performs local logout without redirect. + */ +@QuarkusTest +@TestProfile(TokenResourceLogoutNonCognitoTest.NonCognitoProfile.class) +class TokenResourceLogoutNonCognitoTest { + + @BeforeEach + void configureRestAssured() { + RestApiTestFixture.configureRestAssuredIfExternal(); + } + + @Test + void logoutWithoutCognitoReturnsLocalLogoutHtml() { + RestAssured.given() + .when() + .post("/api/v1/user/logout") + .then() + .statusCode(200) + .contentType("text/html") + .body(containsString("Successfully Logged Out")) + .body(containsString("ExploitIQ")) + .body(containsString("Login Again")) + .header("Clear-Site-Data", equalTo("\"cookies\", \"storage\"")); + } + + @Test + void logoutWithoutCognitoDoesNotRedirect() { + RestAssured.given() + .redirects().follow(false) + .when() + .post("/api/v1/user/logout") + .then() + .statusCode(200); + } + + @Test + void loggedOutPageReturnsSuccessHtml() { + RestAssured.given() + .when() + .get("/api/v1/user/logged-out") + .then() + .statusCode(200) + .contentType("text/html") + .body(containsString("Successfully Logged Out")) + .header("Clear-Site-Data", equalTo("\"cookies\", \"storage\"")); + } + + public static class NonCognitoProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.oidc.enabled", "false", + "quarkus.oidc.auth-server-url", "https://keycloak.example.com/realms/test", + "quarkus.oidc.client-id", "test-client-id" + // cognito.domain is NOT set + ); + } + } +}