KEYCLOAK-6073 Make adapters use discovery endpoint for URLs instead of hardcoding (#6412)
This commit is contained in:
parent
041229f9ca
commit
f14f92ab0b
7 changed files with 100 additions and 14 deletions
|
@ -17,7 +17,9 @@
|
||||||
|
|
||||||
package org.keycloak.adapters;
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
|
import org.apache.http.HttpResponse;
|
||||||
import org.apache.http.client.HttpClient;
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.adapters.authentication.ClientCredentialsProvider;
|
import org.keycloak.adapters.authentication.ClientCredentialsProvider;
|
||||||
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
import org.keycloak.adapters.authorization.PolicyEnforcer;
|
||||||
|
@ -27,7 +29,9 @@ import org.keycloak.common.enums.SslRequired;
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
import org.keycloak.constants.ServiceUrlConstants;
|
import org.keycloak.constants.ServiceUrlConstants;
|
||||||
import org.keycloak.enums.TokenStore;
|
import org.keycloak.enums.TokenStore;
|
||||||
|
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -135,6 +139,17 @@ public class KeycloakDeployment {
|
||||||
this.authServerBaseUrl = config.getAuthServerUrl();
|
this.authServerBaseUrl = config.getAuthServerUrl();
|
||||||
if (authServerBaseUrl == null) return;
|
if (authServerBaseUrl == null) return;
|
||||||
|
|
||||||
|
authServerBaseUrl = KeycloakUriBuilder.fromUri(authServerBaseUrl).build().toString();
|
||||||
|
|
||||||
|
authUrl = null;
|
||||||
|
realmInfoUrl = null;
|
||||||
|
tokenUrl = null;
|
||||||
|
logoutUrl = null;
|
||||||
|
accountUrl = null;
|
||||||
|
registerNodeUrl = null;
|
||||||
|
unregisterNodeUrl = null;
|
||||||
|
jwksUrl = null;
|
||||||
|
|
||||||
URI authServerUri = URI.create(authServerBaseUrl);
|
URI authServerUri = URI.create(authServerBaseUrl);
|
||||||
|
|
||||||
if (authServerUri.getHost() == null) {
|
if (authServerUri.getHost() == null) {
|
||||||
|
@ -142,23 +157,49 @@ public class KeycloakDeployment {
|
||||||
} else {
|
} else {
|
||||||
// We have absolute URI in config
|
// We have absolute URI in config
|
||||||
relativeUrls = RelativeUrlsUsed.NEVER;
|
relativeUrls = RelativeUrlsUsed.NEVER;
|
||||||
KeycloakUriBuilder serverBuilder = KeycloakUriBuilder.fromUri(authServerBaseUrl);
|
|
||||||
resolveUrls(serverBuilder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param authUrlBuilder absolute URI
|
* URLs are loaded lazily when used. This allows adapter to be deployed prior to Keycloak server starting, and will
|
||||||
|
* also allow the adapter to retry loading config for each request until the Keycloak server is ready.
|
||||||
|
*
|
||||||
|
* In the future we may want to support reloading config at a configurable interval.
|
||||||
*/
|
*/
|
||||||
|
protected void resolveUrls() {
|
||||||
|
if (realmInfoUrl == null) {
|
||||||
|
synchronized (this) {
|
||||||
|
KeycloakUriBuilder authUrlBuilder = KeycloakUriBuilder.fromUri(authServerBaseUrl);
|
||||||
|
|
||||||
|
String discoveryUrl = authUrlBuilder.clone().path(ServiceUrlConstants.DISCOVERY_URL).build(getRealm()).toString();
|
||||||
|
try {
|
||||||
|
log.debugv("Resolving URLs from {0}", discoveryUrl);
|
||||||
|
|
||||||
|
OIDCConfigurationRepresentation config = getOidcConfiguration(discoveryUrl);
|
||||||
|
|
||||||
|
authUrl = KeycloakUriBuilder.fromUri(config.getAuthorizationEndpoint());
|
||||||
|
realmInfoUrl = config.getIssuer();
|
||||||
|
|
||||||
|
tokenUrl = config.getTokenEndpoint();
|
||||||
|
logoutUrl = KeycloakUriBuilder.fromUri(config.getLogoutEndpoint());
|
||||||
|
accountUrl = KeycloakUriBuilder.fromUri(config.getIssuer()).path("/account").build().toString();
|
||||||
|
registerNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_REGISTER_NODE_PATH).build(getRealm()).toString();
|
||||||
|
unregisterNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH).build(getRealm()).toString();
|
||||||
|
jwksUrl = config.getJwksUri();
|
||||||
|
|
||||||
|
log.infov("Loaded URLs from {0}", discoveryUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warnv(e, "Failed to load URLs from {0}", discoveryUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void resolveUrls(KeycloakUriBuilder authUrlBuilder) {
|
protected void resolveUrls(KeycloakUriBuilder authUrlBuilder) {
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("resolveUrls");
|
log.debug("resolveUrls");
|
||||||
}
|
}
|
||||||
|
|
||||||
authServerBaseUrl = authUrlBuilder.build().toString();
|
|
||||||
|
|
||||||
String login = authUrlBuilder.clone().path(ServiceUrlConstants.AUTH_PATH).build(getRealm()).toString();
|
String login = authUrlBuilder.clone().path(ServiceUrlConstants.AUTH_PATH).build(getRealm()).toString();
|
||||||
authUrl = KeycloakUriBuilder.fromUri(login);
|
authUrl = KeycloakUriBuilder.fromUri(login);
|
||||||
realmInfoUrl = authUrlBuilder.clone().path(ServiceUrlConstants.REALM_INFO_PATH).build(getRealm()).toString();
|
realmInfoUrl = authUrlBuilder.clone().path(ServiceUrlConstants.REALM_INFO_PATH).build(getRealm()).toString();
|
||||||
|
@ -171,39 +212,59 @@ public class KeycloakDeployment {
|
||||||
jwksUrl = authUrlBuilder.clone().path(ServiceUrlConstants.JWKS_URL).build(getRealm()).toString();
|
jwksUrl = authUrlBuilder.clone().path(ServiceUrlConstants.JWKS_URL).build(getRealm()).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected OIDCConfigurationRepresentation getOidcConfiguration(String discoveryUrl) throws Exception {
|
||||||
|
HttpGet request = new HttpGet(discoveryUrl);
|
||||||
|
request.addHeader("accept", "application/json");
|
||||||
|
|
||||||
|
HttpResponse response = getClient().execute(request);
|
||||||
|
if (response.getStatusLine().getStatusCode() != 200) {
|
||||||
|
throw new Exception(response.getStatusLine().getReasonPhrase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonSerialization.readValue(response.getEntity().getContent(), OIDCConfigurationRepresentation.class);
|
||||||
|
}
|
||||||
|
|
||||||
public RelativeUrlsUsed getRelativeUrls() {
|
public RelativeUrlsUsed getRelativeUrls() {
|
||||||
return relativeUrls;
|
return relativeUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRealmInfoUrl() {
|
public String getRealmInfoUrl() {
|
||||||
|
resolveUrls();
|
||||||
return realmInfoUrl;
|
return realmInfoUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeycloakUriBuilder getAuthUrl() {
|
public KeycloakUriBuilder getAuthUrl() {
|
||||||
|
resolveUrls();
|
||||||
return authUrl;
|
return authUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTokenUrl() {
|
public String getTokenUrl() {
|
||||||
|
resolveUrls();
|
||||||
return tokenUrl;
|
return tokenUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeycloakUriBuilder getLogoutUrl() {
|
public KeycloakUriBuilder getLogoutUrl() {
|
||||||
|
resolveUrls();
|
||||||
return logoutUrl;
|
return logoutUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAccountUrl() {
|
public String getAccountUrl() {
|
||||||
|
resolveUrls();
|
||||||
return accountUrl;
|
return accountUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRegisterNodeUrl() {
|
public String getRegisterNodeUrl() {
|
||||||
|
resolveUrls();
|
||||||
return registerNodeUrl;
|
return registerNodeUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUnregisterNodeUrl() {
|
public String getUnregisterNodeUrl() {
|
||||||
|
resolveUrls();
|
||||||
return unregisterNodeUrl;
|
return unregisterNodeUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getJwksUrl() {
|
public String getJwksUrl() {
|
||||||
|
resolveUrls();
|
||||||
return jwksUrl;
|
return jwksUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,6 @@ public class KeycloakDeploymentBuilder {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Use authServerUrl: " + deployment.getAuthServerBaseUrl() + ", tokenUrl: " + deployment.getTokenUrl() + ", relativeUrls: " + deployment.getRelativeUrls());
|
|
||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import org.keycloak.common.enums.RelativeUrlsUsed;
|
||||||
import org.keycloak.common.enums.SslRequired;
|
import org.keycloak.common.enums.SslRequired;
|
||||||
import org.keycloak.common.util.PemUtils;
|
import org.keycloak.common.util.PemUtils;
|
||||||
import org.keycloak.enums.TokenStore;
|
import org.keycloak.enums.TokenStore;
|
||||||
|
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
@ -50,7 +51,7 @@ public class KeycloakDeploymentBuilderTest {
|
||||||
assertEquals(PemUtils.decodePublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"),
|
assertEquals(PemUtils.decodePublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"),
|
||||||
deployment.getPublicKeyLocator().getPublicKey(null, deployment));
|
deployment.getPublicKeyLocator().getPublicKey(null, deployment));
|
||||||
|
|
||||||
assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/auth", deployment.getAuthUrl().build().toString());
|
assertEquals("https://localhost:8443/auth", deployment.getAuthServerBaseUrl());
|
||||||
assertEquals(SslRequired.EXTERNAL, deployment.getSslRequired());
|
assertEquals(SslRequired.EXTERNAL, deployment.getSslRequired());
|
||||||
assertTrue(deployment.isUseResourceRoleMappings());
|
assertTrue(deployment.isUseResourceRoleMappings());
|
||||||
assertTrue(deployment.isCors());
|
assertTrue(deployment.isCors());
|
||||||
|
@ -66,7 +67,6 @@ public class KeycloakDeploymentBuilderTest {
|
||||||
assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret"));
|
assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret"));
|
||||||
assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
||||||
assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal());
|
assertEquals(20, ((ThreadSafeClientConnManager) deployment.getClient().getConnectionManager()).getMaxTotal());
|
||||||
assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/token", deployment.getTokenUrl());
|
|
||||||
assertEquals(RelativeUrlsUsed.NEVER, deployment.getRelativeUrls());
|
assertEquals(RelativeUrlsUsed.NEVER, deployment.getRelativeUrls());
|
||||||
assertTrue(deployment.isAlwaysRefreshToken());
|
assertTrue(deployment.isAlwaysRefreshToken());
|
||||||
assertTrue(deployment.isRegisterNodeAtStartup());
|
assertTrue(deployment.isRegisterNodeAtStartup());
|
||||||
|
@ -101,4 +101,6 @@ public class KeycloakDeploymentBuilderTest {
|
||||||
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-secret-jwt.json"));
|
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-secret-jwt.json"));
|
||||||
assertEquals(JWTClientSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
assertEquals(JWTClientSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
package org.keycloak.adapters;
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
@ -30,31 +32,32 @@ import static org.junit.Assert.assertTrue;
|
||||||
public class KeycloakDeploymentTest {
|
public class KeycloakDeploymentTest {
|
||||||
@Test
|
@Test
|
||||||
public void shouldNotEnableOAuthQueryParamWhenIgnoreIsTrue() {
|
public void shouldNotEnableOAuthQueryParamWhenIgnoreIsTrue() {
|
||||||
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
|
KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock();
|
||||||
keycloakDeployment.setIgnoreOAuthQueryParameter(true);
|
keycloakDeployment.setIgnoreOAuthQueryParameter(true);
|
||||||
assertFalse(keycloakDeployment.isOAuthQueryParameterEnabled());
|
assertFalse(keycloakDeployment.isOAuthQueryParameterEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldEnableOAuthQueryParamWhenIgnoreIsFalse() {
|
public void shouldEnableOAuthQueryParamWhenIgnoreIsFalse() {
|
||||||
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
|
KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock();
|
||||||
keycloakDeployment.setIgnoreOAuthQueryParameter(false);
|
keycloakDeployment.setIgnoreOAuthQueryParameter(false);
|
||||||
assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled());
|
assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldEnableOAuthQueryParamWhenIgnoreNotSet() {
|
public void shouldEnableOAuthQueryParamWhenIgnoreNotSet() {
|
||||||
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
|
KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock();
|
||||||
|
|
||||||
assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled());
|
assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void stripDefaultPorts() {
|
public void stripDefaultPorts() {
|
||||||
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
|
KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock();
|
||||||
keycloakDeployment.setRealm("test");
|
keycloakDeployment.setRealm("test");
|
||||||
AdapterConfig config = new AdapterConfig();
|
AdapterConfig config = new AdapterConfig();
|
||||||
config.setAuthServerUrl("http://localhost:80/auth");
|
config.setAuthServerUrl("http://localhost:80/auth");
|
||||||
|
|
||||||
keycloakDeployment.setAuthServerBaseUrl(config);
|
keycloakDeployment.setAuthServerBaseUrl(config);
|
||||||
|
|
||||||
assertEquals("http://localhost/auth", keycloakDeployment.getAuthServerBaseUrl());
|
assertEquals("http://localhost/auth", keycloakDeployment.getAuthServerBaseUrl());
|
||||||
|
@ -65,4 +68,19 @@ public class KeycloakDeploymentTest {
|
||||||
assertEquals("https://localhost/auth", keycloakDeployment.getAuthServerBaseUrl());
|
assertEquals("https://localhost/auth", keycloakDeployment.getAuthServerBaseUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class KeycloakDeploymentMock extends KeycloakDeployment {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected OIDCConfigurationRepresentation getOidcConfiguration(String discoveryUrl) throws Exception {
|
||||||
|
String base = KeycloakUriBuilder.fromUri(discoveryUrl).replacePath("/auth").build().toString();
|
||||||
|
|
||||||
|
OIDCConfigurationRepresentation rep = new OIDCConfigurationRepresentation();
|
||||||
|
rep.setAuthorizationEndpoint(base + "/realms/test/authz");
|
||||||
|
rep.setTokenEndpoint(base + "/realms/test/tokens");
|
||||||
|
rep.setIssuer(base + "/realms/test");
|
||||||
|
rep.setJwksUri(base + "/realms/test/jwks");
|
||||||
|
rep.setLogoutEndpoint(base + "/realms/test/logout");
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -97,6 +97,11 @@
|
||||||
<version>${cxf.version}</version>
|
<version>${cxf.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
|
|
|
@ -31,5 +31,6 @@ public interface ServiceUrlConstants {
|
||||||
public static final String CLIENTS_MANAGEMENT_REGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/register-node";
|
public static final String CLIENTS_MANAGEMENT_REGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/register-node";
|
||||||
public static final String CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/unregister-node";
|
public static final String CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/unregister-node";
|
||||||
public static final String JWKS_URL = "/realms/{realm-name}/protocol/openid-connect/certs";
|
public static final String JWKS_URL = "/realms/{realm-name}/protocol/openid-connect/certs";
|
||||||
|
public static final String DISCOVERY_URL = "/realms/{realm-name}/.well-known/openid-configuration";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue