KEYCLOAK-2962 Autodetect bearrer-only clients
Suport more headers
This commit is contained in:
parent
a4cbf130b4
commit
b6d29ccd30
8 changed files with 131 additions and 2 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue