From e96c6a4bcb9b9b9ec65bd908976a662ca9044d19 Mon Sep 17 00:00:00 2001 From: stianst Date: Wed, 20 Dec 2017 08:50:36 +0100 Subject: [PATCH] KEYCLOAK-6068 Fix preflight request on admin endpoints --- .../admin/AdminCorsPreflightService.java | 32 +++++++++++ .../services/resources/admin/AdminRoot.java | 25 +++------ .../testsuite/admin/AdminPreflightTest.java | 55 +++++++++++++++++++ 3 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 services/src/main/java/org/keycloak/services/resources/admin/AdminCorsPreflightService.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminPreflightTest.java diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminCorsPreflightService.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminCorsPreflightService.java new file mode 100644 index 0000000000..1388e3d859 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminCorsPreflightService.java @@ -0,0 +1,32 @@ +package org.keycloak.services.resources.admin; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.services.resources.Cors; + +import javax.ws.rs.OPTIONS; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +/** + * Created by st on 21/03/17. + */ +public class AdminCorsPreflightService { + + private HttpRequest request; + + public AdminCorsPreflightService(HttpRequest request) { + this.request = request; + } + + /** + * CORS preflight + * + * @return + */ + @Path("{any:.*}") + @OPTIONS + public Response preflight() { + return Cors.add(request, Response.ok()).preflight().allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index 650ac75d49..a550a71dcf 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -19,14 +19,12 @@ package org.keycloak.services.resources.admin; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; -import org.jboss.resteasy.spi.NoLogWebApplicationException; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.UnauthorizedException; import org.keycloak.common.ClientConnection; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; -import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -39,11 +37,11 @@ import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.admin.info.ServerInfoAdminResource; import org.keycloak.services.resources.admin.permissions.AdminPermissions; -import org.keycloak.services.resources.admin.permissions.RealmsPermissionEvaluator; import org.keycloak.theme.Theme; import org.keycloak.theme.ThemeProvider; import javax.ws.rs.GET; +import javax.ws.rs.HttpMethod; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.Context; @@ -197,7 +195,6 @@ public class AdminRoot { return adminBaseUrl(base).path(AdminRoot.class, "getRealmsAdmin"); } - /** * Base Path to realm admin REST interface * @@ -205,8 +202,10 @@ public class AdminRoot { * @return */ @Path("realms") - public RealmsAdminResource getRealmsAdmin(@Context final HttpHeaders headers) { - handlePreflightRequest(); + public Object getRealmsAdmin(@Context final HttpHeaders headers) { + if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) { + return new AdminCorsPreflightService(request); + } AdminAuth auth = authenticateRealmAdminRequest(headers); if (auth != null) { @@ -227,8 +226,10 @@ public class AdminRoot { * @return */ @Path("serverinfo") - public ServerInfoAdminResource getServerInfo(@Context final HttpHeaders headers) { - handlePreflightRequest(); + public Object getServerInfo(@Context final HttpHeaders headers) { + if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) { + return new AdminCorsPreflightService(request); + } AdminAuth auth = authenticateRealmAdminRequest(headers); if (!AdminPermissions.realms(session, auth).isAdmin()) { @@ -246,14 +247,6 @@ public class AdminRoot { return adminResource; } - protected void handlePreflightRequest() { - if (request.getHttpMethod().equalsIgnoreCase("OPTIONS")) { - logger.debug("Cors admin pre-flight"); - Response response = Cors.add(request, Response.ok()).preflight().allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(); - throw new NoLogWebApplicationException(response); - } - } - public static Theme getTheme(KeycloakSession session, RealmModel realm) throws IOException { ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); return themeProvider.getTheme(realm.getAdminTheme(), Theme.Type.ADMIN); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminPreflightTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminPreflightTest.java new file mode 100644 index 0000000000..ae73d221fa --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminPreflightTest.java @@ -0,0 +1,55 @@ +package org.keycloak.testsuite.admin; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.services.resources.Cors; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class AdminPreflightTest extends AbstractAdminTest { + + + private CloseableHttpClient client; + + @Before + public void before() { + client = HttpClientBuilder.create().build(); + } + + @After + public void after() { + try { + client.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testPreflight() throws IOException { + HttpOptions options = new HttpOptions(getAdminUrl("realms/master/users")); + options.setHeader("Origin", "http://test"); + + CloseableHttpResponse response = client.execute(options); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertEquals("true", response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_CREDENTIALS).getValue()); + assertEquals("DELETE, POST, GET, PUT", response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_METHODS).getValue()); + assertEquals("http://test", response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_ORIGIN).getValue()); + assertEquals("3600", response.getFirstHeader(Cors.ACCESS_CONTROL_MAX_AGE).getValue()); + assertTrue(response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_HEADERS).getValue().contains("Authorization")); + assertTrue(response.getFirstHeader(Cors.ACCESS_CONTROL_ALLOW_HEADERS).getValue().contains("Content-Type")); + } + + private String getAdminUrl(String resource) { + return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/admin/" + resource; + } + +}