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 e2bd3d74c0..0127decdb6 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 @@ -17,7 +17,9 @@ package org.keycloak.adapters; +import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; import org.jboss.logging.Logger; import org.keycloak.adapters.authentication.ClientCredentialsProvider; 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.constants.ServiceUrlConstants; import org.keycloak.enums.TokenStore; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.representations.adapters.config.AdapterConfig; +import org.keycloak.util.JsonSerialization; import java.net.URI; import java.util.HashMap; @@ -135,6 +139,17 @@ public class KeycloakDeployment { this.authServerBaseUrl = config.getAuthServerUrl(); 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); if (authServerUri.getHost() == null) { @@ -142,23 +157,49 @@ public class KeycloakDeployment { } else { // We have absolute URI in config 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) { if (log.isDebugEnabled()) { log.debug("resolveUrls"); } - authServerBaseUrl = authUrlBuilder.build().toString(); - String login = authUrlBuilder.clone().path(ServiceUrlConstants.AUTH_PATH).build(getRealm()).toString(); authUrl = KeycloakUriBuilder.fromUri(login); 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(); } + 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() { return relativeUrls; } public String getRealmInfoUrl() { + resolveUrls(); return realmInfoUrl; } public KeycloakUriBuilder getAuthUrl() { + resolveUrls(); return authUrl; } public String getTokenUrl() { + resolveUrls(); return tokenUrl; } public KeycloakUriBuilder getLogoutUrl() { + resolveUrls(); return logoutUrl; } public String getAccountUrl() { + resolveUrls(); return accountUrl; } public String getRegisterNodeUrl() { + resolveUrls(); return registerNodeUrl; } public String getUnregisterNodeUrl() { + resolveUrls(); return unregisterNodeUrl; } public String getJwksUrl() { + resolveUrls(); return jwksUrl; } 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 416af5dbb9..b3a632f53e 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 @@ -162,7 +162,6 @@ public class KeycloakDeploymentBuilder { }); } - log.debug("Use authServerUrl: " + deployment.getAuthServerBaseUrl() + ", tokenUrl: " + deployment.getTokenUrl() + ", relativeUrls: " + deployment.getRelativeUrls()); return deployment; } diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java index 770876b31a..fa465807b4 100644 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java +++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java @@ -28,6 +28,7 @@ import org.keycloak.common.enums.RelativeUrlsUsed; import org.keycloak.common.enums.SslRequired; import org.keycloak.common.util.PemUtils; import org.keycloak.enums.TokenStore; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -50,7 +51,7 @@ public class KeycloakDeploymentBuilderTest { assertEquals(PemUtils.decodePublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"), 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()); assertTrue(deployment.isUseResourceRoleMappings()); assertTrue(deployment.isCors()); @@ -66,7 +67,6 @@ public class KeycloakDeploymentBuilderTest { assertEquals("234234-234234-234234", deployment.getResourceCredentials().get("secret")); assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId()); 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()); assertTrue(deployment.isAlwaysRefreshToken()); assertTrue(deployment.isRegisterNodeAtStartup()); @@ -101,4 +101,6 @@ public class KeycloakDeploymentBuilderTest { KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-secret-jwt.json")); assertEquals(JWTClientSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId()); } + + } diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentTest.java index ec7f74f9af..2d2c32e46b 100644 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentTest.java +++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentTest.java @@ -17,6 +17,8 @@ package org.keycloak.adapters; 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 static org.junit.Assert.assertEquals; @@ -30,31 +32,32 @@ import static org.junit.Assert.assertTrue; public class KeycloakDeploymentTest { @Test public void shouldNotEnableOAuthQueryParamWhenIgnoreIsTrue() { - KeycloakDeployment keycloakDeployment = new KeycloakDeployment(); + KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock(); keycloakDeployment.setIgnoreOAuthQueryParameter(true); assertFalse(keycloakDeployment.isOAuthQueryParameterEnabled()); } @Test public void shouldEnableOAuthQueryParamWhenIgnoreIsFalse() { - KeycloakDeployment keycloakDeployment = new KeycloakDeployment(); + KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock(); keycloakDeployment.setIgnoreOAuthQueryParameter(false); assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled()); } @Test public void shouldEnableOAuthQueryParamWhenIgnoreNotSet() { - KeycloakDeployment keycloakDeployment = new KeycloakDeployment(); + KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock(); assertTrue(keycloakDeployment.isOAuthQueryParameterEnabled()); } @Test public void stripDefaultPorts() { - KeycloakDeployment keycloakDeployment = new KeycloakDeployment(); + KeycloakDeployment keycloakDeployment = new KeycloakDeploymentMock(); keycloakDeployment.setRealm("test"); AdapterConfig config = new AdapterConfig(); config.setAuthServerUrl("http://localhost:80/auth"); + keycloakDeployment.setAuthServerBaseUrl(config); assertEquals("http://localhost/auth", keycloakDeployment.getAuthServerBaseUrl()); @@ -65,4 +68,19 @@ public class KeycloakDeploymentTest { 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; + } + } } \ No newline at end of file diff --git a/adapters/oidc/osgi-adapter/pom.xml b/adapters/oidc/osgi-adapter/pom.xml index 4e35de6ba1..9b9fcbdff5 100755 --- a/adapters/oidc/osgi-adapter/pom.xml +++ b/adapters/oidc/osgi-adapter/pom.xml @@ -97,6 +97,11 @@ ${cxf.version} provided + + org.apache.httpcomponents + httpclient + provided + junit diff --git a/core/src/main/java/org/keycloak/constants/ServiceUrlConstants.java b/core/src/main/java/org/keycloak/constants/ServiceUrlConstants.java index 36b4f1d2d6..b03a2e2ff3 100755 --- a/core/src/main/java/org/keycloak/constants/ServiceUrlConstants.java +++ b/core/src/main/java/org/keycloak/constants/ServiceUrlConstants.java @@ -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_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 DISCOVERY_URL = "/realms/{realm-name}/.well-known/openid-configuration"; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java similarity index 100% rename from services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java rename to core/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java