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 String resourceName;
|
||||||
protected boolean bearerOnly;
|
protected boolean bearerOnly;
|
||||||
|
protected boolean autodetectBearerOnly;
|
||||||
protected boolean enableBasicAuth;
|
protected boolean enableBasicAuth;
|
||||||
protected boolean publicClient;
|
protected boolean publicClient;
|
||||||
protected Map<String, Object> resourceCredentials = new HashMap<>();
|
protected Map<String, Object> resourceCredentials = new HashMap<>();
|
||||||
|
@ -201,6 +202,14 @@ public class KeycloakDeployment {
|
||||||
this.bearerOnly = bearerOnly;
|
this.bearerOnly = bearerOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAutodetectBearerOnly() {
|
||||||
|
return autodetectBearerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
|
||||||
|
this.autodetectBearerOnly = autodetectBearerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnableBasicAuth() {
|
public boolean isEnableBasicAuth() {
|
||||||
return enableBasicAuth;
|
return enableBasicAuth;
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,6 +99,7 @@ public class KeycloakDeploymentBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
deployment.setBearerOnly(adapterConfig.isBearerOnly());
|
deployment.setBearerOnly(adapterConfig.isBearerOnly());
|
||||||
|
deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly());
|
||||||
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
|
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
|
||||||
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
|
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
|
||||||
deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());
|
deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.adapters;
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.adapters.spi.AuthChallenge;
|
import org.keycloak.adapters.spi.AuthChallenge;
|
||||||
|
@ -116,6 +118,12 @@ public abstract class RequestAuthenticator {
|
||||||
return AuthOutcome.NOT_ATTEMPTED;
|
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()) {
|
if (log.isTraceEnabled()) {
|
||||||
log.trace("try oauth");
|
log.trace("try oauth");
|
||||||
}
|
}
|
||||||
|
@ -158,6 +166,36 @@ public abstract class RequestAuthenticator {
|
||||||
return false;
|
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 abstract OAuthRequestAuthenticator createOAuthAuthenticator();
|
||||||
|
|
||||||
protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
|
protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
|
||||||
|
|
|
@ -30,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||||
"resource", "public-client", "credentials",
|
"resource", "public-client", "credentials",
|
||||||
"use-resource-role-mappings",
|
"use-resource-role-mappings",
|
||||||
"enable-cors", "cors-max-age", "cors-allowed-methods",
|
"enable-cors", "cors-max-age", "cors-allowed-methods",
|
||||||
"expose-token", "bearer-only",
|
"expose-token", "bearer-only", "autodetect-bearer-only",
|
||||||
"connection-pool-size",
|
"connection-pool-size",
|
||||||
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
|
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
|
||||||
"client-keystore", "client-keystore-password", "client-key-password",
|
"client-keystore", "client-keystore-password", "client-key-password",
|
||||||
|
|
|
@ -33,7 +33,7 @@ import java.util.Map;
|
||||||
"resource", "public-client", "credentials",
|
"resource", "public-client", "credentials",
|
||||||
"use-resource-role-mappings",
|
"use-resource-role-mappings",
|
||||||
"enable-cors", "cors-max-age", "cors-allowed-methods",
|
"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 {
|
public class BaseAdapterConfig extends BaseRealmConfig {
|
||||||
@JsonProperty("resource")
|
@JsonProperty("resource")
|
||||||
protected String resource;
|
protected String resource;
|
||||||
|
@ -51,6 +51,8 @@ public class BaseAdapterConfig extends BaseRealmConfig {
|
||||||
protected boolean exposeToken;
|
protected boolean exposeToken;
|
||||||
@JsonProperty("bearer-only")
|
@JsonProperty("bearer-only")
|
||||||
protected boolean bearerOnly;
|
protected boolean bearerOnly;
|
||||||
|
@JsonProperty("autodetect-bearer-only")
|
||||||
|
protected boolean autodetectBearerOnly;
|
||||||
@JsonProperty("enable-basic-auth")
|
@JsonProperty("enable-basic-auth")
|
||||||
protected boolean enableBasicAuth;
|
protected boolean enableBasicAuth;
|
||||||
@JsonProperty("public-client")
|
@JsonProperty("public-client")
|
||||||
|
@ -123,6 +125,14 @@ public class BaseAdapterConfig extends BaseRealmConfig {
|
||||||
this.bearerOnly = bearerOnly;
|
this.bearerOnly = bearerOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAutodetectBearerOnly() {
|
||||||
|
return autodetectBearerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
|
||||||
|
this.autodetectBearerOnly = autodetectBearerOnly;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnableBasicAuth() {
|
public boolean isEnableBasicAuth() {
|
||||||
return enableBasicAuth;
|
return enableBasicAuth;
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,12 @@ public class AdapterTest {
|
||||||
.servletClass(ProductServlet.class).adapterConfigPath(url.getPath())
|
.servletClass(ProductServlet.class).adapterConfigPath(url.getPath())
|
||||||
.role("user").deployApplication();
|
.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
|
// Test that replacing system properties works for adapters
|
||||||
System.setProperty("app.server.base.url", "http://localhost:8081");
|
System.setProperty("app.server.base.url", "http://localhost:8081");
|
||||||
System.setProperty("my.host.name", "localhost");
|
System.setProperty("my.host.name", "localhost");
|
||||||
|
@ -149,6 +155,11 @@ public class AdapterTest {
|
||||||
testStrategy.testNullBearerTokenCustomErrorPage();
|
testStrategy.testNullBearerTokenCustomErrorPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAutodetectBearerOnly() throws Exception {
|
||||||
|
testStrategy.testAutodetectBearerOnly();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testBasicAuthErrorHandling() throws Exception {
|
public void testBasicAuthErrorHandling() throws Exception {
|
||||||
testStrategy.testBasicAuthErrorHandling();
|
testStrategy.testBasicAuthErrorHandling();
|
||||||
|
|
|
@ -400,6 +400,55 @@ public class AdapterTestStrategy extends ExternalResource {
|
||||||
Time.setOffset(0);
|
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
|
* KEYCLOAK-518
|
||||||
* @throws Exception
|
* @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