KEYCLOAK-2962 Autodetect bearrer-only clients

Suport more headers
This commit is contained in:
Slawomir Dabek 2016-12-18 15:25:50 +01:00
parent a4cbf130b4
commit b6d29ccd30
8 changed files with 131 additions and 2 deletions

View file

@ -57,6 +57,7 @@ public class KeycloakDeployment {
protected String resourceName;
protected boolean bearerOnly;
protected boolean autodetectBearerOnly;
protected boolean enableBasicAuth;
protected boolean publicClient;
protected Map<String, Object> 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;
}

View file

@ -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());

View file

@ -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<String> 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() {

View file

@ -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",

View file

@ -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;
}

View file

@ -73,6 +73,12 @@ public class AdapterTest {
.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");
System.setProperty("my.host.name", "localhost");
@ -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();

View file

@ -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

View file

@ -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
}