From b6d29ccd3043ebf4623ad22ac2bc838347caf23a Mon Sep 17 00:00:00 2001 From: Slawomir Dabek Date: Sun, 18 Dec 2016 15:25:50 +0100 Subject: [PATCH] KEYCLOAK-2962 Autodetect bearrer-only clients Suport more headers --- .../keycloak/adapters/KeycloakDeployment.java | 9 ++++ .../adapters/KeycloakDeploymentBuilder.java | 1 + .../adapters/RequestAuthenticator.java | 38 ++++++++++++++ .../adapters/config/AdapterConfig.java | 2 +- .../adapters/config/BaseAdapterConfig.java | 12 ++++- .../testsuite/adapter/AdapterTest.java | 11 +++++ .../adapter/AdapterTestStrategy.java | 49 +++++++++++++++++++ ...oduct-autodetect-bearer-only-keycloak.json | 11 +++++ 8 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 testsuite/integration/src/test/resources/adapter-test/product-autodetect-bearer-only-keycloak.json diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index b9ee4c6f05..8664800ec8 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -57,6 +57,7 @@ public class KeycloakDeployment { protected String resourceName; protected boolean bearerOnly; + protected boolean autodetectBearerOnly; protected boolean enableBasicAuth; protected boolean publicClient; protected Map resourceCredentials = new HashMap<>(); @@ -201,6 +202,14 @@ public class KeycloakDeployment { this.bearerOnly = bearerOnly; } + public boolean isAutodetectBearerOnly() { + return autodetectBearerOnly; + } + + public void setAutodetectBearerOnly(boolean autodetectBearerOnly) { + this.autodetectBearerOnly = autodetectBearerOnly; + } + public boolean isEnableBasicAuth() { return enableBasicAuth; } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index 85b19ca538..65e945601e 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -99,6 +99,7 @@ public class KeycloakDeploymentBuilder { } deployment.setBearerOnly(adapterConfig.isBearerOnly()); + deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly()); deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth()); deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken()); deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup()); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java index c04f21c050..0cbe687fc4 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java @@ -17,6 +17,8 @@ package org.keycloak.adapters; +import java.util.Collections; +import java.util.List; import org.jboss.logging.Logger; import org.keycloak.KeycloakPrincipal; import org.keycloak.adapters.spi.AuthChallenge; @@ -116,6 +118,12 @@ public abstract class RequestAuthenticator { return AuthOutcome.NOT_ATTEMPTED; } + if (isAutodetectedBearerOnly(facade.getRequest())) { + challenge = bearer.getChallenge(); + log.debug("NOT_ATTEMPTED: Treating as bearer only"); + return AuthOutcome.NOT_ATTEMPTED; + } + if (log.isTraceEnabled()) { log.trace("try oauth"); } @@ -158,6 +166,36 @@ public abstract class RequestAuthenticator { return false; } + protected boolean isAutodetectedBearerOnly(HttpFacade.Request request) { + if (!deployment.isAutodetectBearerOnly()) return false; + + String headerValue = facade.getRequest().getHeader("X-Requested-With"); + if (headerValue != null && headerValue.equalsIgnoreCase("XMLHttpRequest")) { + return true; + } + + headerValue = facade.getRequest().getHeader("Faces-Request"); + if (headerValue != null && headerValue.startsWith("partial/")) { + return true; + } + + headerValue = facade.getRequest().getHeader("SOAPAction"); + if (headerValue != null) { + return true; + } + + List accepts = facade.getRequest().getHeaders("Accept"); + if (accepts == null) accepts = Collections.emptyList(); + + for (String accept : accepts) { + if (accept.contains("text/html") || accept.contains("text/*") || accept.contains("*/*")) { + return false; + } + } + + return true; + } + protected abstract OAuthRequestAuthenticator createOAuthAuthenticator(); protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() { diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java index e4065bcd05..0a107bb4b4 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java @@ -30,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; "resource", "public-client", "credentials", "use-resource-role-mappings", "enable-cors", "cors-max-age", "cors-allowed-methods", - "expose-token", "bearer-only", + "expose-token", "bearer-only", "autodetect-bearer-only", "connection-pool-size", "allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password", "client-keystore", "client-keystore-password", "client-key-password", diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java index f9138a1ed6..3cef2a0eca 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java @@ -33,7 +33,7 @@ import java.util.Map; "resource", "public-client", "credentials", "use-resource-role-mappings", "enable-cors", "cors-max-age", "cors-allowed-methods", - "expose-token", "bearer-only", "enable-basic-auth"}) + "expose-token", "bearer-only", "autodetect-bearer-only", "enable-basic-auth"}) public class BaseAdapterConfig extends BaseRealmConfig { @JsonProperty("resource") protected String resource; @@ -51,6 +51,8 @@ public class BaseAdapterConfig extends BaseRealmConfig { protected boolean exposeToken; @JsonProperty("bearer-only") protected boolean bearerOnly; + @JsonProperty("autodetect-bearer-only") + protected boolean autodetectBearerOnly; @JsonProperty("enable-basic-auth") protected boolean enableBasicAuth; @JsonProperty("public-client") @@ -123,6 +125,14 @@ public class BaseAdapterConfig extends BaseRealmConfig { this.bearerOnly = bearerOnly; } + public boolean isAutodetectBearerOnly() { + return autodetectBearerOnly; + } + + public void setAutodetectBearerOnly(boolean autodetectBearerOnly) { + this.autodetectBearerOnly = autodetectBearerOnly; + } + public boolean isEnableBasicAuth() { return enableBasicAuth; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java index 404d24dd67..1f2f620f6a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java @@ -72,6 +72,12 @@ public class AdapterTest { .name("product-portal").contextPath("/product-portal") .servletClass(ProductServlet.class).adapterConfigPath(url.getPath()) .role("user").deployApplication(); + + url = getClass().getResource("/adapter-test/product-autodetect-bearer-only-keycloak.json"); + createApplicationDeployment() + .name("product-portal-autodetect-bearer-only").contextPath("/product-portal-autodetect-bearer-only") + .servletClass(ProductServlet.class).adapterConfigPath(url.getPath()) + .role("user").deployApplication(); // Test that replacing system properties works for adapters System.setProperty("app.server.base.url", "http://localhost:8081"); @@ -149,6 +155,11 @@ public class AdapterTest { testStrategy.testNullBearerTokenCustomErrorPage(); } + @Test + public void testAutodetectBearerOnly() throws Exception { + testStrategy.testAutodetectBearerOnly(); + } + @Test public void testBasicAuthErrorHandling() throws Exception { testStrategy.testBasicAuthErrorHandling(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java index 9341df2d2d..bd0a144607 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java @@ -400,6 +400,55 @@ public class AdapterTestStrategy extends ExternalResource { Time.setOffset(0); } + public void testAutodetectBearerOnly() throws Exception { + Client client = ClientBuilder.newClient(); + + // Do not redirect client to login page if it's an XHR + WebTarget target = client.target(APP_SERVER_BASE_URL + "/product-portal-autodetect-bearer-only"); + Response response = target.request().header("X-Requested-With", "XMLHttpRequest").get(); + Assert.assertEquals(401, response.getStatus()); + response.close(); + + // Do not redirect client to login page if it's a partial Faces request + response = target.request().header("Faces-Request", "partial/ajax").get(); + Assert.assertEquals(401, response.getStatus()); + response.close(); + + // Do not redirect client to login page if it's a SOAP request + response = target.request().header("SOAPAction", "").get(); + Assert.assertEquals(401, response.getStatus()); + response.close(); + + // Do not redirect client to login page if Accept header is missing + response = target.request().get(); + Assert.assertEquals(401, response.getStatus()); + response.close(); + + // Do not redirect client to login page if client does not understand HTML reponses + response = target.request().header(HttpHeaders.ACCEPT, "application/json,text/xml").get(); + Assert.assertEquals(401, response.getStatus()); + response.close(); + + // Redirect client to login page if it's not an XHR + response = target.request().header("X-Requested-With", "Dont-Know").header(HttpHeaders.ACCEPT, "*/*").get(); + Assert.assertEquals(302, response.getStatus()); + Assert.assertTrue(response.getHeaderString(HttpHeaders.LOCATION).contains("response_type=code")); + response.close(); + + // Redirect client to login page if client explicitely understands HTML responses + response = target.request().header(HttpHeaders.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9").get(); + Assert.assertEquals(302, response.getStatus()); + Assert.assertTrue(response.getHeaderString(HttpHeaders.LOCATION).contains("response_type=code")); + response.close(); + + // Redirect client to login page if client understands all response types + response = target.request().header(HttpHeaders.ACCEPT, "*/*").get(); + Assert.assertEquals(302, response.getStatus()); + Assert.assertTrue(response.getHeaderString(HttpHeaders.LOCATION).contains("response_type=code")); + response.close(); + client.close(); + } + /** * KEYCLOAK-518 * @throws Exception diff --git a/testsuite/integration/src/test/resources/adapter-test/product-autodetect-bearer-only-keycloak.json b/testsuite/integration/src/test/resources/adapter-test/product-autodetect-bearer-only-keycloak.json new file mode 100644 index 0000000000..b92abc6dc4 --- /dev/null +++ b/testsuite/integration/src/test/resources/adapter-test/product-autodetect-bearer-only-keycloak.json @@ -0,0 +1,11 @@ +{ + "realm" : "demo", + "resource" : "product-portal", + "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url" : "http://localhost:8081/auth", + "ssl-required" : "external", + "credentials" : { + "secret": "password" + }, + "autodetect-bearer-only" : true +}