From b8881b8ea062afa2f1b5a613dd856900b73f2121 Mon Sep 17 00:00:00 2001 From: stianst Date: Wed, 16 Oct 2019 10:33:55 +0200 Subject: [PATCH] KEYCLOAK-11728 New default hostname provider Co-authored-by: Hynek Mlnarik --- .../content/bin/migrate-domain-clustered.cli | 19 ++ .../content/bin/migrate-domain-standalone.cli | 19 ++ .../content/bin/migrate-standalone-ha.cli | 18 ++ .../content/bin/migrate-standalone.cli | 19 ++ .../migration/MigrationModelManager.java | 4 +- .../migration/migrators/MigrateTo8_0_0.java | 62 +++++ .../models/BrowserSecurityHeaders.java | 73 ++++-- .../java/org/keycloak/models/Constants.java | 3 + .../org/keycloak/models/KeycloakContext.java | 16 ++ .../org/keycloak/models/KeycloakUriInfo.java | 27 +- .../org/keycloak/urls/HostnameProvider.java | 93 ++++++- .../main/java/org/keycloak/urls/UrlType.java | 7 + .../ExecuteActionsActionTokenHandler.java | 7 +- .../AuthorizationTokenService.java | 2 +- .../exportimport/util/ImportUtils.java | 1 - .../freemarker/model/ApplicationsBean.java | 47 +--- .../FreeMarkerLoginFormsProvider.java | 2 +- .../login/freemarker/model/ClientBean.java | 10 +- .../keys/loader/ClientPublicKeyLoader.java | 2 +- ...DockerComposeYamlInstallationProvider.java | 2 +- .../protocol/oidc/OIDCLoginProtocol.java | 2 +- .../protocol/oidc/OIDCWellKnownProvider.java | 26 +- .../oidc/endpoints/AuthorizationEndpoint.java | 2 +- .../endpoints/LoginStatusIframeEndpoint.java | 2 +- .../oidc/endpoints/LogoutEndpoint.java | 4 +- .../oidc/endpoints/TokenEndpoint.java | 4 +- .../AllowedWebOriginsProtocolMapper.java | 3 +- .../protocol/oidc/utils/RedirectUtils.java | 47 ++-- .../protocol/oidc/utils/WebOriginsUtils.java | 5 +- .../keycloak/protocol/saml/SamlProtocol.java | 8 +- .../keycloak/protocol/saml/SamlService.java | 8 +- .../services/DefaultKeycloakContext.java | 33 ++- ...nstallationClientRegistrationProvider.java | 2 +- .../services/managers/ApplianceBootstrap.java | 3 +- .../services/managers/RealmManager.java | 29 +-- .../managers/ResourceAdminManager.java | 56 ++-- .../org/keycloak/services/resources/Cors.java | 5 +- .../resources/IdentityBrokerService.java | 4 +- .../resources/KeycloakApplication.java | 19 +- .../resources/LoginActionsServiceChecks.java | 2 +- .../services/resources/RealmsResource.java | 2 +- .../services/resources/WelcomeResource.java | 8 +- .../resources/account/AccountConsole.java | 13 +- .../resources/account/AccountFormService.java | 6 +- .../resources/admin/AdminConsole.java | 51 +++- .../services/resources/admin/AdminRoot.java | 3 +- .../resources/admin/ClientResource.java | 6 +- .../resources/admin/RealmAdminResource.java | 4 +- .../resources/admin/RealmsAdminResource.java | 1 - .../resources/admin/UserResource.java | 2 +- .../services/util/ResolveRelative.java | 39 ++- .../theme/BrowserSecurityHeaderSetup.java | 48 +++- .../keycloak/url/DefaultHostnameProvider.java | 112 ++++++++ .../url/DefaultHostnameProviderFactory.java | 56 ++++ .../keycloak/url/FixedHostnameProvider.java | 1 + .../url/FixedHostnameProviderFactory.java | 15 +- .../keycloak/url/RequestHostnameProvider.java | 1 + .../url/RequestHostnameProviderFactory.java | 11 + .../org.keycloak.urls.HostnameProviderFactory | 1 + .../integration-arquillian/HOW-TO-RUN.md | 1 + .../keycloak/testsuite/updaters/Creator.java | 13 + .../account/AccountFormServiceTest.java | 2 +- .../admin/AdminConsoleLandingPageTest.java | 4 +- .../testsuite/admin/realm/RealmTest.java | 10 +- .../broker/AbstractAdvancedBrokerTest.java | 2 +- .../client/AdapterInstallationConfigTest.java | 2 +- .../testsuite/client/ClientRedirectTest.java | 2 +- .../ldap/LDAPMultipleAttributesTest.java | 2 +- .../migration/AbstractMigrationTest.java | 36 +++ .../JsonFileImport198MigrationTest.java | 1 + .../JsonFileImport255MigrationTest.java | 1 + .../JsonFileImport343MigrationTest.java | 1 + .../JsonFileImport483MigrationTest.java | 1 + .../testsuite/migration/MigrationTest.java | 4 + .../testsuite/url/AbstractHostnameTest.java | 116 +++++++++ .../testsuite/url/DefaultHostnameTest.java | 239 ++++++++++++++++++ .../testsuite/url/FixedHostnameTest.java | 190 +++++++++----- .../keycloak/testsuite/util/RealmBuilder.java | 8 + .../resources/META-INF/keycloak-server.json | 8 +- .../keycloak/testsuite/KeycloakServer.java | 1 - .../resources/META-INF/keycloak-server.json | 10 +- .../main/resources/theme/base/admin/index.ftl | 1 + .../messages/admin-messages_en.properties | 2 + .../theme/base/admin/resources/js/app.js | 10 + .../admin/resources/partials/client-list.html | 2 +- .../resources/partials/realm-detail.html | 8 + .../theme/keycloak/welcome/index.ftl | 2 +- .../default-server-subsys-config.properties | 9 +- .../cli/default-keycloak-subsys-config.cli | 4 +- 89 files changed, 1396 insertions(+), 373 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo8_0_0.java create mode 100644 server-spi/src/main/java/org/keycloak/urls/UrlType.java create mode 100644 services/src/main/java/org/keycloak/url/DefaultHostnameProvider.java create mode 100644 services/src/main/java/org/keycloak/url/DefaultHostnameProviderFactory.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/AbstractHostnameTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/DefaultHostnameTest.java diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli index 91cc16af91..b0596dfa8b 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli @@ -676,4 +676,23 @@ if ((result.time == 100L) && (result.unit == MILLISECONDS)) of /profile=$cluster echo end-if +if (outcome == failed) of /profile=$clusteredProfile/subsystem=keycloak-server/spi=hostname/provider=default/:read-resource + echo Adding default hostname provider + /profile=$clusteredProfile/subsystem=keycloak-server/spi=hostname/provider=default/:add(properties={frontendUrl => "${keycloak.frontendUrl:}",forceBackendUrlToFrontendUrl => "false"},enabled=true) +end-if + +if (result == request) of /profile=$clusteredProfile/subsystem=keycloak-server/spi=hostname/:read-attribute(name=default-provider) + echo Switching from request to default hostname provider + + /profile=$clusteredProfile/subsystem=keycloak-server/spi=hostname/:write-attribute(name=default-provider,value=default) +end-if + +if (result != fixed) of /profile=$clusteredProfile/subsystem=keycloak-server/spi=hostname/:read-attribute(name=default-provider) + try + /profile=$clusteredProfile/subsystem=keycloak-server/spi=hostname/provider=fixed:remove + echo Removed config for unused fixed hostname provider + catch + end-try +end-if + echo *** End Migration of /profile=$clusteredProfile *** diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli index 2c6850b5a1..adc2f8e0f4 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli @@ -577,4 +577,23 @@ if ((result.time == 100L) && (result.unit == MILLISECONDS)) of /profile=$standal echo end-if +if (outcome == failed) of /profile=$standaloneProfile/subsystem=keycloak-server/spi=hostname/provider=default/:read-resource + echo Adding default hostname provider + /profile=$standaloneProfile/subsystem=keycloak-server/spi=hostname/provider=default/:add(properties={frontendUrl => "${keycloak.frontendUrl:}",forceBackendUrlToFrontendUrl => "false"},enabled=true) +end-if + +if (result == request) of /profile=$standaloneProfile/subsystem=keycloak-server/spi=hostname/:read-attribute(name=default-provider) + echo Switching from request to default hostname provider + + /profile=$standaloneProfile/subsystem=keycloak-server/spi=hostname/:write-attribute(name=default-provider,value=default) +end-if + +if (result != fixed) of /profile=$standaloneProfile/subsystem=keycloak-server/spi=hostname/:read-attribute(name=default-provider) + try + /profile=$standaloneProfile/subsystem=keycloak-server/spi=hostname/provider=fixed:remove + echo Removed config for unused fixed hostname provider + catch + end-try +end-if + echo *** End Migration of /profile=$standaloneProfile *** diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli index 07e71f2056..8f7952fb6a 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli @@ -748,5 +748,23 @@ if (result == UP) of /subsystem=microprofile-health-smallrye:read-attribute(name echo end-if +if (outcome == failed) of /subsystem=keycloak-server/spi=hostname/provider=default/:read-resource + echo Adding default hostname provider + /subsystem=keycloak-server/spi=hostname/provider=default/:add(properties={frontendUrl => "${keycloak.frontendUrl:}",forceBackendUrlToFrontendUrl => "false"},enabled=true) +end-if + +if (result == request) of /subsystem=keycloak-server/spi=hostname/:read-attribute(name=default-provider) + echo Switching from request to default hostname provider + + /subsystem=keycloak-server/spi=hostname/:write-attribute(name=default-provider,value=default) +end-if + +if (result != fixed) of /subsystem=keycloak-server/spi=hostname/:read-attribute(name=default-provider) + try + /subsystem=keycloak-server/spi=hostname/provider=fixed:remove + echo Removed config for unused fixed hostname provider + catch + end-try +end-if echo *** End Migration *** diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli index 4c251da180..672f3d54b9 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli @@ -613,4 +613,23 @@ if (result == UP) of /subsystem=microprofile-health-smallrye:read-attribute(name echo end-if +if (outcome == failed) of /subsystem=keycloak-server/spi=hostname/provider=default/:read-resource + echo Adding default hostname provider + /subsystem=keycloak-server/spi=hostname/provider=default/:add(properties={frontendUrl => "${keycloak.frontendUrl:}",forceBackendUrlToFrontendUrl => "false"},enabled=true) +end-if + +if (result == request) of /subsystem=keycloak-server/spi=hostname/:read-attribute(name=default-provider) + echo Switching from request to default hostname provider + + /subsystem=keycloak-server/spi=hostname/:write-attribute(name=default-provider,value=default) +end-if + +if (result != fixed) of /subsystem=keycloak-server/spi=hostname/:read-attribute(name=default-provider) + try + /subsystem=keycloak-server/spi=hostname/provider=fixed:remove + echo Removed config for unused fixed hostname provider + catch + end-try +end-if + echo *** End Migration *** diff --git a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java index d3cffc24e5..b0b6ea477d 100755 --- a/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java +++ b/server-spi-private/src/main/java/org/keycloak/migration/MigrationModelManager.java @@ -45,6 +45,7 @@ import org.keycloak.migration.migrators.MigrateTo4_0_0; import org.keycloak.migration.migrators.MigrateTo4_2_0; import org.keycloak.migration.migrators.MigrateTo4_6_0; import org.keycloak.migration.migrators.MigrateTo6_0_0; +import org.keycloak.migration.migrators.MigrateTo8_0_0; import org.keycloak.migration.migrators.Migration; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -81,7 +82,8 @@ public class MigrationModelManager { new MigrateTo4_0_0(), new MigrateTo4_2_0(), new MigrateTo4_6_0(), - new MigrateTo6_0_0() + new MigrateTo6_0_0(), + new MigrateTo8_0_0() }; public static void migrate(KeycloakSession session) { diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo8_0_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo8_0_0.java new file mode 100644 index 0000000000..9061b7e031 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo8_0_0.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.migration.migrators; + +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.RealmRepresentation; + +import java.util.Collections; + +public class MigrateTo8_0_0 implements Migration { + + public static final ModelVersion VERSION = new ModelVersion("8.0.0"); + + @Override + public ModelVersion getVersion() { + return VERSION; + } + + @Override + public void migrate(KeycloakSession session) { + session.realms().getRealms().stream().forEach(realm -> migrateRealm(realm)); + } + + @Override + public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) { + migrateRealm(realm); + } + + protected void migrateRealm(RealmModel realm) { + ClientModel adminConsoleClient = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID); + adminConsoleClient.setRootUrl(Constants.AUTH_ADMIN_URL_PROP); + String adminConsoleBaseUrl = "/admin/" + realm.getName() + "/console/"; + adminConsoleClient.setBaseUrl(adminConsoleBaseUrl); + adminConsoleClient.setRedirectUris(Collections.singleton(adminConsoleBaseUrl + "*")); + adminConsoleClient.setWebOrigins(Collections.singleton("+")); + + ClientModel accountClient = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); + accountClient.setRootUrl(Constants.AUTH_BASE_URL_PROP); + String accountClientBaseUrl = "/realms/" + realm.getName() + "/account/"; + accountClient.setBaseUrl(accountClientBaseUrl); + accountClient.setRedirectUris(Collections.singleton(accountClientBaseUrl + "*")); + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java b/server-spi-private/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java index 7bddd17d3b..cfc3a46a58 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java +++ b/server-spi-private/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java @@ -26,29 +26,72 @@ import java.util.Map; * @version $Revision: 1 $ */ public class BrowserSecurityHeaders { + + public static final String X_FRAME_OPTIONS = "X-Frame-Options"; + + public static final String X_FRAME_OPTIONS_DEFAULT = "SAMEORIGIN"; + + public static final String X_FRAME_OPTIONS_KEY = "xFrameOptions"; + + public static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy"; + + public static final String CONTENT_SECURITY_POLICY_DEFAULT = "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"; + + public static final String CONTENT_SECURITY_POLICY_KEY = "contentSecurityPolicy"; + + public static final String CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"; + + public static final String CONTENT_SECURITY_POLICY_REPORT_ONLY_DEFAULT = ""; + + public static final String CONTENT_SECURITY_POLICY_REPORT_ONLY_KEY = "contentSecurityPolicyReportOnly"; + + public static final String X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options"; + + public static final String X_CONTENT_TYPE_OPTIONS_DEFAULT = "nosniff"; + + public static final String X_CONTENT_TYPE_OPTIONS_KEY = "xContentTypeOptions"; + + public static final String X_ROBOTS_TAG = "X-Robots-Tag"; + + public static final String X_ROBOTS_TAG_KEY = "xRobotsTag"; + + public static final String X_ROBOTS_TAG_DEFAULT = "none"; + + public static final String X_XSS_PROTECTION = "X-XSS-Protection"; + + public static final String X_XSS_PROTECTION_DEFAULT = "1; mode=block"; + + public static final String X_XSS_PROTECTION_KEY = "xXSSProtection"; + + public static final String STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security"; + + public static final String STRICT_TRANSPORT_SECURITY_DEFAULT = "max-age=31536000; includeSubDomains"; + + public static final String STRICT_TRANSPORT_SECURITY_KEY = "strictTransportSecurity"; + public static final Map headerAttributeMap; public static final Map defaultHeaders; static { Map headerMap = new HashMap<>(); - headerMap.put("xFrameOptions", "X-Frame-Options"); - headerMap.put("contentSecurityPolicy", "Content-Security-Policy"); - headerMap.put("contentSecurityPolicyReportOnly", "Content-Security-Policy-Report-Only"); - headerMap.put("xContentTypeOptions", "X-Content-Type-Options"); - headerMap.put("xRobotsTag", "X-Robots-Tag"); - headerMap.put("xXSSProtection", "X-XSS-Protection"); - headerMap.put("strictTransportSecurity", "Strict-Transport-Security"); + headerMap.put(X_FRAME_OPTIONS_KEY, X_FRAME_OPTIONS); + headerMap.put(CONTENT_SECURITY_POLICY_KEY, CONTENT_SECURITY_POLICY); + headerMap.put(CONTENT_SECURITY_POLICY_REPORT_ONLY_KEY, CONTENT_SECURITY_POLICY_REPORT_ONLY); + headerMap.put(X_CONTENT_TYPE_OPTIONS_KEY, X_CONTENT_TYPE_OPTIONS); + headerMap.put(X_ROBOTS_TAG_KEY, X_ROBOTS_TAG); + headerMap.put(X_XSS_PROTECTION_KEY, X_XSS_PROTECTION); + headerMap.put(STRICT_TRANSPORT_SECURITY_KEY, STRICT_TRANSPORT_SECURITY); Map dh = new HashMap<>(); - dh.put("xFrameOptions", "SAMEORIGIN"); - dh.put("contentSecurityPolicy", "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"); - dh.put("contentSecurityPolicyReportOnly", ""); - dh.put("xContentTypeOptions", "nosniff"); - dh.put("xRobotsTag", "none"); - dh.put("xXSSProtection", "1; mode=block"); - dh.put("strictTransportSecurity", "max-age=31536000; includeSubDomains"); + dh.put(X_FRAME_OPTIONS_KEY, X_FRAME_OPTIONS_DEFAULT); + dh.put(CONTENT_SECURITY_POLICY_KEY, CONTENT_SECURITY_POLICY_DEFAULT); + dh.put(CONTENT_SECURITY_POLICY_REPORT_ONLY_KEY, CONTENT_SECURITY_POLICY_REPORT_ONLY_DEFAULT); + dh.put(X_CONTENT_TYPE_OPTIONS_KEY, X_CONTENT_TYPE_OPTIONS_DEFAULT); + dh.put(X_ROBOTS_TAG_KEY, X_ROBOTS_TAG_DEFAULT); + dh.put(X_XSS_PROTECTION_KEY, X_XSS_PROTECTION_DEFAULT); + dh.put(STRICT_TRANSPORT_SECURITY_KEY, STRICT_TRANSPORT_SECURITY_DEFAULT); defaultHeaders = Collections.unmodifiableMap(dh); headerAttributeMap = Collections.unmodifiableMap(headerMap); } -} \ No newline at end of file +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index c8fdf81502..01b8dd85c4 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -36,6 +36,9 @@ public final class Constants { public static final String BROKER_SERVICE_CLIENT_ID = "broker"; public static final String REALM_MANAGEMENT_CLIENT_ID = "realm-management"; + public static final String AUTH_BASE_URL_PROP = "${authBaseUrl}"; + public static final String AUTH_ADMIN_URL_PROP = "${authAdminUrl}"; + public static final Collection defaultClients = Arrays.asList(ACCOUNT_MANAGEMENT_CLIENT_ID, ADMIN_CLI_CLIENT_ID, BROKER_SERVICE_CLIENT_ID, REALM_MANAGEMENT_CLIENT_ID, ADMIN_CONSOLE_CLIENT_ID); public static final String INSTALLED_APP_URN = "urn:ietf:wg:oauth:2.0:oob"; diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakContext.java b/server-spi/src/main/java/org/keycloak/models/KeycloakContext.java index 86f6a4f68a..725c3d7c6b 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakContext.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakContext.java @@ -19,6 +19,7 @@ package org.keycloak.models; import org.keycloak.common.ClientConnection; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.urls.UrlType; import javax.ws.rs.core.HttpHeaders; import java.net.URI; @@ -33,8 +34,23 @@ public interface KeycloakContext { String getContextPath(); + /** + * Returns the URI assuming it is a frontend request. To resolve URI for a backend request use {@link #getUri(UrlType)} + * @return + */ KeycloakUriInfo getUri(); + /** + * Returns the URI. If a frontend request (from user-agent) @frontendRequest should be set to true. If a backend + * request (request from a client) should be set to false. Depending on the configure hostname provider it may + * return a hard-coded base URL for frontend request (for example https://auth.mycompany.com) and use the + * request URL for backend requests. Frontend URI should also be used for realm issuer fields in tokens. + * + * @param type the type of the request + * @return + */ + KeycloakUriInfo getUri(UrlType type); + HttpHeaders getRequestHeaders(); T getContextObject(Class clazz); diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java b/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java index c1d9156d1e..56e4993060 100644 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java @@ -17,8 +17,8 @@ package org.keycloak.models; import org.jboss.resteasy.specimpl.ResteasyUriBuilder; -import org.keycloak.models.KeycloakSession; import org.keycloak.urls.HostnameProvider; +import org.keycloak.urls.UrlType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.PathSegment; @@ -33,29 +33,38 @@ public class KeycloakUriInfo implements UriInfo { private final String hostname; private final String scheme; private final int port; + private final String contextPath; private URI absolutePath; private URI requestURI; private URI baseURI; - public KeycloakUriInfo(KeycloakSession session, UriInfo delegate) { + public KeycloakUriInfo(KeycloakSession session, UrlType type, UriInfo delegate) { this.delegate = delegate; HostnameProvider hostnameProvider = session.getProvider(HostnameProvider.class); - this.scheme = hostnameProvider.getScheme(delegate); - this.hostname = hostnameProvider.getHostname(delegate); - this.port = hostnameProvider.getPort(delegate); - + this.scheme = hostnameProvider.getScheme(delegate, type); + this.hostname = hostnameProvider.getHostname(delegate, type); + this.port = hostnameProvider.getPort(delegate, type); + this.contextPath = hostnameProvider.getContextPath(delegate, type); } public UriInfo getDelegate() { return delegate; } + private UriBuilder initUriBuilder(UriBuilder b) { + b.scheme(scheme); + b.host(hostname); + b.port(port); + b.replacePath(contextPath); + return b; + } + @Override public URI getRequestUri() { if (requestURI == null) { - requestURI = delegate.getRequestUriBuilder().scheme(scheme).host(hostname).port(port).build(); + requestURI = delegate.getRequestUri(); } return requestURI; } @@ -68,7 +77,7 @@ public class KeycloakUriInfo implements UriInfo { @Override public URI getAbsolutePath() { if (absolutePath == null) { - absolutePath = delegate.getAbsolutePathBuilder().scheme(scheme).host(hostname).port(port).build(); + absolutePath = delegate.getAbsolutePath(); } return absolutePath; } @@ -81,7 +90,7 @@ public class KeycloakUriInfo implements UriInfo { @Override public URI getBaseUri() { if (baseURI == null) { - baseURI = delegate.getBaseUriBuilder().scheme(scheme).host(hostname).port(port).build(); + baseURI = initUriBuilder(delegate.getBaseUriBuilder()).build(); } return baseURI; } diff --git a/server-spi/src/main/java/org/keycloak/urls/HostnameProvider.java b/server-spi/src/main/java/org/keycloak/urls/HostnameProvider.java index 35366c5304..1ed6f71e07 100644 --- a/server-spi/src/main/java/org/keycloak/urls/HostnameProvider.java +++ b/server-spi/src/main/java/org/keycloak/urls/HostnameProvider.java @@ -21,20 +21,99 @@ import org.keycloak.provider.Provider; import javax.ws.rs.core.UriInfo; +/** + * The Hostname provider is used by Keycloak to decide URLs for frontend and backend requests. A provider can either + * base the URL on the request (Host header for example) or based on hard-coded URLs. Further, it is possible to have + * different URLs on frontend requests and backend requests. + * + * Note: Do NOT use {@link KeycloakContext#getUri()} within a Hostname provider. It will result in an infinite loop. + */ public interface HostnameProvider extends Provider { - String getScheme(UriInfo originalUriInfo); + /** + * Returns the URL scheme. If not implemented will delegate to {@link #getScheme(UriInfo)}. + * + * @param originalUriInfo the original URI + * @param uype type of the request + * @return the schema + */ + default String getScheme(UriInfo originalUriInfo, UrlType type) { + return getScheme(originalUriInfo); + } /** - * Return the hostname. Http headers, realm details, etc. can be retrieved from the KeycloakSession. Do NOT use - * {@link KeycloakContext#getUri()} as it will in turn call the HostnameProvider resulting in an infinite loop! + * Returns the URL scheme. If not implemented will get the scheme from the request. * - * @param originalUriInfo the original UriInfo before hostname is replaced by the HostnameProvider - * @return the hostname + * @param originalUriInfo the original URI + * @return the schema */ - String getHostname(UriInfo originalUriInfo); + default String getScheme(UriInfo originalUriInfo) { + return originalUriInfo.getBaseUri().getScheme(); + } - int getPort(UriInfo originalUriInfo); + /** + * Returns the host. If not implemented will delegate to {@link #getHostname(UriInfo)}. + * + * @param originalUriInfo the original URI + * @param type type of the request + * @return the host + */ + default String getHostname(UriInfo originalUriInfo, UrlType type) { + return getHostname(originalUriInfo); + } + + /** + * Returns the host. If not implemented will get the host from the request. + * @param originalUriInfo + * @return the host + */ + default String getHostname(UriInfo originalUriInfo) { + return originalUriInfo.getBaseUri().getHost(); + } + + /** + * Returns the port (or -1 for default port). If not implemented will delegate to {@link #getPort(UriInfo)} + * + * @param originalUriInfo the original URI + * @param type type of the request + * @return the port + */ + default int getPort(UriInfo originalUriInfo, UrlType type) { + return getPort(originalUriInfo); + } + + /** + * Returns the port (or -1 for default port). If not implemented will get the port from the request. + * + * @param originalUriInfo the original URI + * @return the port + */ + default int getPort(UriInfo originalUriInfo) { + return originalUriInfo.getBaseUri().getPort(); + } + + /** + * Returns the context-path for Keycloak. This is useful when Keycloak is exposed on a different context-path on + * a reverse proxy. If not implemented will delegate to {@link #getContextPath(UriInfo)} + * + * @param originalUriInfo the original URI + * @param type type of the request + * @return the context-path + */ + default String getContextPath(UriInfo originalUriInfo, UrlType type) { + return getContextPath(originalUriInfo); + } + + /** + * Returns the context-path for Keycloak This is useful when Keycloak is exposed on a different context-path on + * a reverse proxy. If not implemented will use the context-path from the request, which by default is /auth + * + * @param originalUriInfo the original URI + * @return the context-path + */ + default String getContextPath(UriInfo originalUriInfo) { + return originalUriInfo.getBaseUri().getPath(); + } @Override default void close() { diff --git a/server-spi/src/main/java/org/keycloak/urls/UrlType.java b/server-spi/src/main/java/org/keycloak/urls/UrlType.java new file mode 100644 index 0000000000..fdb58cde62 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/urls/UrlType.java @@ -0,0 +1,7 @@ +package org.keycloak.urls; + +public enum UrlType { + + FRONTEND, BACKEND, ADMIN + +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java index 8fad471814..b5c9164e07 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java @@ -58,8 +58,8 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander< TokenUtils.checkThat( // either redirect URI is not specified or must be valid for the client t -> t.getRedirectUri() == null - || RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), t.getRedirectUri(), - tokenContext.getRealm(), tokenContext.getAuthenticationSession().getClient()) != null, + || RedirectUtils.verifyRedirectUri(tokenContext.getSession(), t.getRedirectUri(), + tokenContext.getAuthenticationSession().getClient()) != null, Errors.INVALID_REDIRECT_URI, Messages.INVALID_REDIRECT_URI ) @@ -88,8 +88,7 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander< .createInfoPage(); } - String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(), - tokenContext.getRealm(), authSession.getClient()); + String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getSession(), token.getRedirectUri(), authSession.getClient()); if (redirectUri != null) { authSession.setAuthNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true"); diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java index 93abd38111..b9ce827145 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -236,7 +236,7 @@ public class AuthorizationTokenService { private Response createSuccessfulResponse(Object response, KeycloakAuthorizationRequest request) { return Cors.add(request.getHttpRequest(), Response.status(Status.OK).type(MediaType.APPLICATION_JSON_TYPE).entity(response)) - .allowedOrigins(request.getKeycloakSession().getContext().getUri(), request.getKeycloakSession().getContext().getClient()) + .allowedOrigins(request.getKeycloakSession(), request.getKeycloakSession().getContext().getClient()) .allowedMethods(HttpMethod.POST) .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); } diff --git a/services/src/main/java/org/keycloak/exportimport/util/ImportUtils.java b/services/src/main/java/org/keycloak/exportimport/util/ImportUtils.java index 7eb44cf3b9..1f435719be 100755 --- a/services/src/main/java/org/keycloak/exportimport/util/ImportUtils.java +++ b/services/src/main/java/org/keycloak/exportimport/util/ImportUtils.java @@ -110,7 +110,6 @@ public class ImportUtils { } RealmManager realmManager = new RealmManager(session); - realmManager.setContextPath(session.getContext().getContextPath()); realmManager.importRealm(rep, skipUserDependent); if (System.getProperty(ExportImportConfig.ACTION) != null) { diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java index baaa5257cd..a215ef8be8 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/ApplicationsBean.java @@ -30,6 +30,7 @@ import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.resources.admin.permissions.AdminPermissions; +import org.keycloak.services.util.ResolveRelative; import org.keycloak.storage.StorageId; import java.util.ArrayList; @@ -47,7 +48,6 @@ public class ApplicationsBean { private List applications = new LinkedList<>(); public ApplicationsBean(KeycloakSession session, RealmModel realm, UserModel user) { - Set offlineClients = new UserSessionManager(session).findClientsWithOfflineToken(realm, user); for (ClientModel client : getApplications(session, realm, user)) { @@ -90,7 +90,7 @@ public class ApplicationsBean { additionalGrants.add("${offlineToken}"); } - applications.add(new ApplicationEntry(realmRolesAvailable, resourceRolesAvailable, client, clientScopesGranted, additionalGrants)); + applications.add(new ApplicationEntry(session, realmRolesAvailable, resourceRolesAvailable, client, clientScopesGranted, additionalGrants)); } } @@ -142,14 +142,16 @@ public class ApplicationsBean { public static class ApplicationEntry { + private KeycloakSession session; private final List realmRolesAvailable; private final MultivaluedHashMap resourceRolesAvailable; private final ClientModel client; private final List clientScopesGranted; private final List additionalGrants; - public ApplicationEntry(List realmRolesAvailable, MultivaluedHashMap resourceRolesAvailable, + public ApplicationEntry(KeycloakSession session, List realmRolesAvailable, MultivaluedHashMap resourceRolesAvailable, ClientModel client, List clientScopesGranted, List additionalGrants) { + this.session = session; this.realmRolesAvailable = realmRolesAvailable; this.resourceRolesAvailable = resourceRolesAvailable; this.client = client; @@ -170,44 +172,7 @@ public class ApplicationsBean { } public String getEffectiveUrl() { - String rootUrl = getClient().getRootUrl(); - String baseUrl = getClient().getBaseUrl(); - - if (rootUrl == null) rootUrl = ""; - if (baseUrl == null) baseUrl = ""; - - if (rootUrl.equals("") && baseUrl.equals("")) { - return ""; - } - - if (rootUrl.equals("") && !baseUrl.equals("")) { - return baseUrl; - } - - if (!rootUrl.equals("") && baseUrl.equals("")) { - return rootUrl; - } - - if (isBaseUrlRelative() && !rootUrl.equals("")) { - return concatUrls(rootUrl, baseUrl); - } - - return baseUrl; - } - - private String concatUrls(String u1, String u2) { - if (u1.endsWith("/")) u1 = u1.substring(0, u1.length() - 1); - if (u2.startsWith("/")) u2 = u2.substring(1); - return u1 + "/" + u2; - } - - private boolean isBaseUrlRelative() { - String baseUrl = getClient().getBaseUrl(); - if (baseUrl.equals("")) return false; - if (baseUrl.startsWith("/")) return true; - if (baseUrl.startsWith("./")) return true; - if (baseUrl.startsWith("../")) return true; - return false; + return ResolveRelative.resolveRelativeUri(session, getClient().getRootUrl(), getClient().getBaseUrl()); } public ClientModel getClient() { diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java index b549989cfd..a51cdf6c76 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -378,7 +378,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { URI baseUriWithCodeAndClientId = baseUriBuilder.build(); if (client != null) { - attributes.put("client", new ClientBean(client, baseUri)); + attributes.put("client", new ClientBean(session, client)); } if (realm != null) { diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/ClientBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/ClientBean.java index 43dbdc0eed..56a2f47aa4 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/ClientBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/ClientBean.java @@ -18,6 +18,7 @@ package org.keycloak.forms.login.freemarker.model; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.services.util.ResolveRelative; import java.net.URI; @@ -29,13 +30,12 @@ import java.util.Map; */ public class ClientBean { + private KeycloakSession session; protected ClientModel client; - private URI requestUri; - - public ClientBean(ClientModel client, URI requestUri) { + public ClientBean(KeycloakSession session, ClientModel client) { + this.session = session; this.client = client; - this.requestUri = requestUri; } public String getClientId() { @@ -51,7 +51,7 @@ public class ClientBean { } public String getBaseUrl() { - return ResolveRelative.resolveRelativeUri(requestUri, client.getRootUrl(), client.getBaseUrl()); + return ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), client.getBaseUrl()); } public Map getAttributes(){ diff --git a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java index 6ec2884e1f..9a35d7bd4d 100644 --- a/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java +++ b/services/src/main/java/org/keycloak/keys/loader/ClientPublicKeyLoader.java @@ -71,7 +71,7 @@ public class ClientPublicKeyLoader implements PublicKeyLoader { OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientModel(client); if (config.isUseJwksUrl()) { String jwksUrl = config.getJwksUrl(); - jwksUrl = ResolveRelative.resolveRelativeUri(session.getContext().getUri().getRequestUri(), client.getRootUrl(), jwksUrl); + jwksUrl = ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), jwksUrl); JSONWebKeySet jwks = JWKSHttpUtils.sendJwksRequest(session, jwksUrl); return JWKSUtils.getKeyWrappersForUse(jwks, keyUse); } else if (keyUse == JWK.Use.SIG) { diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java index 72ade3116c..831ba380bc 100644 --- a/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java +++ b/services/src/main/java/org/keycloak/protocol/docker/installation/DockerComposeYamlInstallationProvider.java @@ -58,7 +58,7 @@ public class DockerComposeYamlInstallationProvider implements ClientInstallation final ZipOutputStream zipOutput = new ZipOutputStream(byteStream); try { - return generateInstallation(zipOutput, byteStream, session.keys().getActiveRsaKey(realm).getCertificate(), session.getContext().getAuthServerUrl().toURL(), realm.getName(), client.getClientId()); + return generateInstallation(zipOutput, byteStream, session.keys().getActiveRsaKey(realm).getCertificate(), session.getContext().getUri().getBaseUri().toURL(), realm.getName(), client.getClientId()); } catch (final IOException e) { try { zipOutput.close(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index ea018dc21c..fafc4b8ddf 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -310,7 +310,7 @@ public class OIDCLoginProtocol implements LoginProtocol { @Override public void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); - new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, client, clientSession); + new ResourceAdminManager(session).logoutClientSession(realm, client, clientSession); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index b5eed4745a..df8dbfc858 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -37,6 +37,7 @@ import org.keycloak.services.Urls; import org.keycloak.services.clientregistration.ClientRegistrationService; import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory; import org.keycloak.services.resources.RealmsResource; +import org.keycloak.urls.UrlType; import org.keycloak.wellknown.WellKnownProvider; import javax.ws.rs.core.UriBuilder; @@ -75,21 +76,24 @@ public class OIDCWellKnownProvider implements WellKnownProvider { @Override public Object getConfig() { - UriInfo uriInfo = session.getContext().getUri(); + UriInfo frontendUriInfo = session.getContext().getUri(UrlType.FRONTEND); + UriInfo backendUriInfo = session.getContext().getUri(UrlType.BACKEND); + RealmModel realm = session.getContext().getRealm(); - UriBuilder uriBuilder = RealmsResource.protocolUrl(uriInfo); + UriBuilder frontendUriBuilder = RealmsResource.protocolUrl(frontendUriInfo); + UriBuilder backendUriBuilder = RealmsResource.protocolUrl(backendUriInfo); OIDCConfigurationRepresentation config = new OIDCConfigurationRepresentation(); - config.setIssuer(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); - config.setAuthorizationEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "auth").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); - config.setTokenEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "token").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); - config.setTokenIntrospectionEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "token").path(TokenEndpoint.class, "introspect").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); - config.setUserinfoEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); - config.setLogoutEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); - config.setJwksUri(uriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); - config.setCheckSessionIframe(uriBuilder.clone().path(OIDCLoginProtocolService.class, "getLoginStatusIframe").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); - config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(uriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString()); + config.setIssuer(Urls.realmIssuer(frontendUriInfo.getBaseUri(), realm.getName())); + config.setAuthorizationEndpoint(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "auth").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setTokenEndpoint(backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "token").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setTokenIntrospectionEndpoint(backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "token").path(TokenEndpoint.class, "introspect").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setUserinfoEndpoint(backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setLogoutEndpoint(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setJwksUri(backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setCheckSessionIframe(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "getLoginStatusIframe").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setRegistrationEndpoint(RealmsResource.clientRegistrationUrl(backendUriInfo).path(ClientRegistrationService.class, "provider").build(realm.getName(), OIDCClientRegistrationProviderFactory.ID).toString()); config.setIdTokenSigningAlgValuesSupported(getSupportedSigningAlgorithms(false)); config.setIdTokenEncryptionAlgValuesSupported(getSupportedIdTokenEncryptionAlg(false)); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index b85391d788..f7e1ff5796 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -410,7 +410,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { event.detail(Details.REDIRECT_URI, redirectUriParam); // redirect_uri parameter is required per OpenID Connect, but optional per OAuth2 - redirectUri = RedirectUtils.verifyRedirectUri(session.getContext().getUri(), redirectUriParam, realm, client, isOIDCRequest); + redirectUri = RedirectUtils.verifyRedirectUri(session, redirectUriParam, client, isOIDCRequest); if (redirectUri == null) { event.error(Errors.INVALID_REDIRECT_URI); throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java index 5122bf341d..2a568641f0 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LoginStatusIframeEndpoint.java @@ -77,7 +77,7 @@ public class LoginStatusIframeEndpoint { RealmModel realm = session.getContext().getRealm(); ClientModel client = session.realms().getClientByClientId(clientId, realm); if (client != null && client.isEnabled()) { - Set validWebOrigins = WebOriginsUtils.resolveValidWebOrigins(uriInfo, client); + Set validWebOrigins = WebOriginsUtils.resolveValidWebOrigins(session, client); validWebOrigins.add(UriUtils.getOrigin(uriInfo.getRequestUri())); if (validWebOrigins.contains("*") || validWebOrigins.contains(origin)) { return Response.noContent().build(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index 473e8261bb..eb2bfde9d9 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -105,7 +105,7 @@ public class LogoutEndpoint { String redirect = postLogoutRedirectUri != null ? postLogoutRedirectUri : redirectUri; if (redirect != null) { - String validatedUri = RedirectUtils.verifyRealmRedirectUri(session.getContext().getUri(), redirect, realm); + String validatedUri = RedirectUtils.verifyRealmRedirectUri(session, redirect); if (validatedUri == null) { event.event(EventType.LOGOUT); event.detail(Details.REDIRECT_URI, redirect); @@ -216,7 +216,7 @@ public class LogoutEndpoint { } } - return Cors.add(request, Response.noContent()).auth().allowedOrigins(session.getContext().getUri(), client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); + return Cors.add(request, Response.noContent()).auth().allowedOrigins(session, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); } private void logout(UserSessionModel userSession, boolean offline) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 2f248477f6..b47a6e7223 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -233,7 +233,7 @@ public class TokenEndpoint { client = clientAuth.getClient(); clientAuthAttributes = clientAuth.getClientAuthAttributes(); - cors.allowedOrigins(session.getContext().getUri(), client); + cors.allowedOrigins(session, client); if (client.isBearerOnly()) { throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Bearer-only not allowed", Response.Status.BAD_REQUEST); @@ -1093,7 +1093,7 @@ public class TokenEndpoint { session.getContext().setClient(client); - cors.allowedOrigins(session.getContext().getUri(), client); + cors.allowedOrigins(session, client); } String claimToken = null; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java index 65c004416a..f3a042add3 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AllowedWebOriginsProtocolMapper.java @@ -75,11 +75,10 @@ public class AllowedWebOriginsProtocolMapper extends AbstractOIDCProtocolMapper public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) { ClientModel client = clientSessionCtx.getClientSession().getClient(); - UriInfo uriInfo = session.getContext().getUri(); Set allowedOrigins = client.getWebOrigins(); if (allowedOrigins != null && !allowedOrigins.isEmpty()) { - token.setAllowedOrigins(WebOriginsUtils.resolveValidWebOrigins(uriInfo, client)); + token.setAllowedOrigins(WebOriginsUtils.resolveValidWebOrigins(session, client)); } return token; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java index 97d1de97ee..5f89335010 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java @@ -21,10 +21,12 @@ import org.jboss.logging.Logger; import org.keycloak.common.util.UriUtils; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakUriInfo; import org.keycloak.models.RealmModel; import org.keycloak.services.Urls; +import org.keycloak.services.util.ResolveRelative; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; @@ -38,46 +40,49 @@ public class RedirectUtils { private static final Logger logger = Logger.getLogger(RedirectUtils.class); - public static String verifyRealmRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm) { - Set validRedirects = getValidateRedirectUris(uriInfo, realm); - return verifyRedirectUri(uriInfo, null, redirectUri, realm, validRedirects, true); + public static String verifyRealmRedirectUri(KeycloakSession session, String redirectUri) { + Set validRedirects = getValidateRedirectUris(session); + return verifyRedirectUri(session, null, redirectUri, validRedirects, true); } - public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client) { - return verifyRedirectUri(uriInfo, redirectUri, realm, client, true); + public static String verifyRedirectUri(KeycloakSession session, String redirectUri, ClientModel client) { + return verifyRedirectUri(session, redirectUri, client, true); } - public static String verifyRedirectUri(UriInfo uriInfo, String redirectUri, RealmModel realm, ClientModel client, boolean requireRedirectUri) { + public static String verifyRedirectUri(KeycloakSession session, String redirectUri, ClientModel client, boolean requireRedirectUri) { if (client != null) - return verifyRedirectUri(uriInfo, client.getRootUrl(), redirectUri, realm, client.getRedirectUris(), requireRedirectUri); + return verifyRedirectUri(session, client.getRootUrl(), redirectUri, client.getRedirectUris(), requireRedirectUri); return null; } - public static Set resolveValidRedirects(UriInfo uriInfo, String rootUrl, Set validRedirects) { + public static Set resolveValidRedirects(KeycloakSession session, String rootUrl, Set validRedirects) { // If the valid redirect URI is relative (no scheme, host, port) then use the request's scheme, host, and port Set resolveValidRedirects = new HashSet<>(); for (String validRedirect : validRedirects) { - resolveValidRedirects.add(validRedirect); // add even relative urls. if (validRedirect.startsWith("/")) { - validRedirect = relativeToAbsoluteURI(uriInfo, rootUrl, validRedirect); + validRedirect = relativeToAbsoluteURI(session, rootUrl, validRedirect); logger.debugv("replacing relative valid redirect with: {0}", validRedirect); resolveValidRedirects.add(validRedirect); + } else { + resolveValidRedirects.add(validRedirect); } } return resolveValidRedirects; } - private static Set getValidateRedirectUris(UriInfo uriInfo, RealmModel realm) { + private static Set getValidateRedirectUris(KeycloakSession session) { Set redirects = new HashSet<>(); - for (ClientModel client : realm.getClients()) { + for (ClientModel client : session.getContext().getRealm().getClients()) { if (client.isEnabled()) { - redirects.addAll(resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())); + redirects.addAll(resolveValidRedirects(session, client.getRootUrl(), client.getRedirectUris())); } } return redirects; } - private static String verifyRedirectUri(UriInfo uriInfo, String rootUrl, String redirectUri, RealmModel realm, Set validRedirects, boolean requireRedirectUri) { + private static String verifyRedirectUri(KeycloakSession session, String rootUrl, String redirectUri, Set validRedirects, boolean requireRedirectUri) { + KeycloakUriInfo uriInfo = session.getContext().getUri(); + RealmModel realm = session.getContext().getRealm(); if (redirectUri != null) redirectUri = normalizeUrl(redirectUri); @@ -98,7 +103,7 @@ public class RedirectUtils { redirectUri = lowerCaseHostname(redirectUri); String r = redirectUri; - Set resolveValidRedirects = resolveValidRedirects(uriInfo, rootUrl, validRedirects); + Set resolveValidRedirects = resolveValidRedirects(session, rootUrl, validRedirects); boolean valid = matchesRedirects(resolveValidRedirects, r); @@ -118,7 +123,7 @@ public class RedirectUtils { valid = matchesRedirects(resolveValidRedirects, r); } if (valid && redirectUri.startsWith("/")) { - redirectUri = relativeToAbsoluteURI(uriInfo, rootUrl, redirectUri); + redirectUri = relativeToAbsoluteURI(session, rootUrl, redirectUri); } redirectUri = valid ? redirectUri : null; } @@ -139,9 +144,13 @@ public class RedirectUtils { } } - private static String relativeToAbsoluteURI(UriInfo uriInfo, String rootUrl, String relative) { + private static String relativeToAbsoluteURI(KeycloakSession session, String rootUrl, String relative) { + if (rootUrl != null) { + rootUrl = ResolveRelative.resolveRootUrl(session, rootUrl); + } + if (rootUrl == null || rootUrl.isEmpty()) { - rootUrl = UriUtils.getOrigin(uriInfo.getBaseUri()); + rootUrl = UriUtils.getOrigin(session.getContext().getUri().getBaseUri()); } StringBuilder sb = new StringBuilder(); sb.append(rootUrl); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java index 6d15380038..380019fb06 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/WebOriginsUtils.java @@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.utils; import org.keycloak.common.util.UriUtils; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import javax.ws.rs.core.UriInfo; import java.util.HashSet; @@ -31,14 +32,14 @@ public class WebOriginsUtils { public static final String INCLUDE_REDIRECTS = "+"; - public static Set resolveValidWebOrigins(UriInfo uriInfo, ClientModel client) { + public static Set resolveValidWebOrigins(KeycloakSession session, ClientModel client) { Set origins = new HashSet<>(); if (client.getWebOrigins() != null) { origins.addAll(client.getWebOrigins()); } if (origins.contains(INCLUDE_REDIRECTS)) { origins.remove(INCLUDE_REDIRECTS); - for (String redirectUri : RedirectUtils.resolveValidRedirects(uriInfo, client.getRootUrl(), client.getRedirectUris())) { + for (String redirectUri : RedirectUtils.resolveValidRedirects(session, client.getRootUrl(), client.getRedirectUris())) { if (redirectUri.startsWith("http://") || redirectUri.startsWith("https://")) { origins.add(UriUtils.getOrigin(redirectUri)); } diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 016d02b13f..697ac2fa3e 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -546,7 +546,7 @@ public class SamlProtocol implements LoginProtocol { roleListMapper.mapper.mapRoles(existingAttributeStatement, roleListMapper.model, session, userSession, clientSessionCtx); } - public static String getLogoutServiceUrl(UriInfo uriInfo, ClientModel client, String bindingType) { + public static String getLogoutServiceUrl(KeycloakSession session, ClientModel client, String bindingType) { String logoutServiceUrl = null; if (SAML_POST_BINDING.equals(bindingType)) { logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE); @@ -557,7 +557,7 @@ public class SamlProtocol implements LoginProtocol { logoutServiceUrl = client.getManagementUrl(); if (logoutServiceUrl == null || logoutServiceUrl.trim().equals("")) return null; - return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl); + return ResourceAdminManager.resolveUri(session, client.getRootUrl(), logoutServiceUrl); } @@ -567,7 +567,7 @@ public class SamlProtocol implements LoginProtocol { SamlClient samlClient = new SamlClient(client); try { boolean postBinding = isLogoutPostBindingForClient(clientSession); - String bindingUri = getLogoutServiceUrl(uriInfo, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING); + String bindingUri = getLogoutServiceUrl(session, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING); if (bindingUri == null) { logger.warnf("Failed to logout client %s, skipping this client. Please configure the logout service url in the admin console for your client applications.", client.getClientId()); return null; @@ -672,7 +672,7 @@ public class SamlProtocol implements LoginProtocol { public void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); SamlClient samlClient = new SamlClient(client); - String logoutUrl = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING); + String logoutUrl = getLogoutServiceUrl(session, client, SAML_POST_BINDING); if (logoutUrl == null) { logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s", client.getClientId()); return; diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java index b5a969c89b..6d19c45934 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -299,7 +299,7 @@ public class SamlService extends AuthorizationEndpointBase { String redirect; URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL(); if (redirectUri != null && ! "null".equals(redirectUri.toString())) { // "null" is for testing purposes - redirect = RedirectUtils.verifyRedirectUri(session.getContext().getUri(), redirectUri.toString(), realm, client); + redirect = RedirectUtils.verifyRedirectUri(session, redirectUri.toString(), client); } else { if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) { redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE); @@ -413,12 +413,12 @@ public class SamlService extends AuthorizationEndpointBase { AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false); if (authResult != null) { String logoutBinding = getBindingType(); - String postBindingUri = SamlProtocol.getLogoutServiceUrl(session.getContext().getUri(), client, SamlProtocol.SAML_POST_BINDING); + String postBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, SamlProtocol.SAML_POST_BINDING); if (samlClient.forcePostBinding() && postBindingUri != null && ! postBindingUri.trim().isEmpty()) logoutBinding = SamlProtocol.SAML_POST_BINDING; boolean postBinding = Objects.equals(SamlProtocol.SAML_POST_BINDING, logoutBinding); - String bindingUri = SamlProtocol.getLogoutServiceUrl(session.getContext().getUri(), client, logoutBinding); + String bindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding); UserSessionModel userSession = authResult.getSession(); userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri); if (samlClient.requiresRealmSignature()) { @@ -474,7 +474,7 @@ public class SamlService extends AuthorizationEndpointBase { // default String logoutBinding = getBindingType(); - String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(session.getContext().getUri(), client, logoutBinding); + String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding); String logoutRelayState = relayState; SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder(); builder.logoutRequestID(logoutRequest.getID()); diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java index ec44a7db57..cd22b0a6dc 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java @@ -28,11 +28,14 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.urls.UrlType; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.UriInfo; import java.net.URI; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; /** * @author Stian Thorgersen @@ -47,8 +50,8 @@ public class DefaultKeycloakContext implements KeycloakContext { private KeycloakSession session; - private KeycloakUriInfo uriInfo; - + private Map uriInfo; + private AuthenticationSessionModel authenticationSession; public DefaultKeycloakContext(KeycloakSession session) { @@ -57,24 +60,30 @@ public class DefaultKeycloakContext implements KeycloakContext { @Override public URI getAuthServerUrl() { - UriInfo uri = getUri(); - KeycloakApplication keycloakApplication = getContextObject(KeycloakApplication.class); - return keycloakApplication.getBaseUri(uri); + return getUri(UrlType.FRONTEND).getBaseUri(); } @Override public String getContextPath() { - KeycloakApplication app = getContextObject(KeycloakApplication.class); - if (app == null) return null; - return app.getContextPath(); + return getUri(UrlType.FRONTEND).getBaseUri().getPath(); + } + + @Override + public KeycloakUriInfo getUri(UrlType type) { + if (uriInfo == null || !uriInfo.containsKey(type)) { + if (uriInfo == null) { + uriInfo = new HashMap<>(); + } + + UriInfo originalUriInfo = getContextObject(UriInfo.class); + uriInfo.put(type, new KeycloakUriInfo(session, type, originalUriInfo)); + } + return uriInfo.get(type); } @Override public KeycloakUriInfo getUri() { - if (uriInfo == null) { - uriInfo = new KeycloakUriInfo(session, getContextObject(UriInfo.class)); - } - return uriInfo; + return getUri(UrlType.FRONTEND); } @Override diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java index 54af568492..7ec4096411 100644 --- a/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/AdapterInstallationClientRegistrationProvider.java @@ -54,7 +54,7 @@ public class AdapterInstallationClientRegistrationProvider implements ClientRegi auth.requireView(client); ClientManager clientManager = new ClientManager(new RealmManager(session)); - Object rep = clientManager.toInstallationRepresentation(session.getContext().getRealm(), client, session.getContext().getAuthServerUrl()); + Object rep = clientManager.toInstallationRepresentation(session.getContext().getRealm(), client, session.getContext().getUri().getBaseUri()); event.client(client.getClientId()).success(); return Response.ok(rep).build(); diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index 84ce6f1c48..7a571dc41b 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -55,7 +55,7 @@ public class ApplianceBootstrap { return session.users().getUsersCount(realm) == 0; } - public boolean createMasterRealm(String contextPath) { + public boolean createMasterRealm() { if (!isNewInstall()) { throw new IllegalStateException("Can't create default realm as realms already exists"); } @@ -64,7 +64,6 @@ public class ApplianceBootstrap { ServicesLogger.LOGGER.initializingAdminRealm(adminRealmName); RealmManager manager = new RealmManager(session); - manager.setContextPath(contextPath); RealmModel realm = manager.createRealm(adminRealmName, adminRealmName); realm.setName(adminRealmName); realm.setDisplayName(Version.NAME); diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index e866ba6aae..c720214853 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -67,15 +67,6 @@ public class RealmManager { protected KeycloakSession session; protected RealmProvider model; - protected String contextPath = ""; - - public String getContextPath() { - return contextPath; - } - - public void setContextPath(String contextPath) { - this.contextPath = contextPath; - } public RealmManager(KeycloakSession session) { this.session = session; @@ -165,11 +156,15 @@ public class RealmManager { ClientModel adminConsole = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID); if (adminConsole == null) adminConsole = KeycloakModelUtils.createClient(realm, Constants.ADMIN_CONSOLE_CLIENT_ID); adminConsole.setName("${client_" + Constants.ADMIN_CONSOLE_CLIENT_ID + "}"); - String baseUrl = contextPath + "/admin/" + realm.getName() + "/console"; - adminConsole.setBaseUrl(baseUrl + "/index.html"); + + adminConsole.setRootUrl(Constants.AUTH_ADMIN_URL_PROP); + String baseUrl = "/admin/" + realm.getName() + "/console/"; + adminConsole.setBaseUrl(baseUrl); + adminConsole.addRedirectUri(baseUrl + "*"); + adminConsole.setWebOrigins(Collections.singleton("+")); + adminConsole.setEnabled(true); adminConsole.setPublicClient(true); - adminConsole.addRedirectUri(baseUrl + "/*"); adminConsole.setFullScopeAllowed(false); adminConsole.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); } @@ -412,10 +407,12 @@ public class RealmManager { client.setName("${client_" + Constants.ACCOUNT_MANAGEMENT_CLIENT_ID + "}"); client.setEnabled(true); client.setFullScopeAllowed(false); - String base = contextPath + "/realms/" + realm.getName() + "/account"; - String redirectUri = base + "/*"; - client.addRedirectUri(redirectUri); - client.setBaseUrl(base); + + client.setRootUrl(Constants.AUTH_BASE_URL_PROP); + String baseUrl = "/realms/" + realm.getName() + "/account/"; + client.setBaseUrl(baseUrl); + client.addRedirectUri(baseUrl + "*"); + client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); for (String role : AccountRoles.ALL) { diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index 457138a006..1cb7d70eb5 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -64,19 +64,19 @@ public class ResourceAdminManager { this.session = session; } - public static String resolveUri(URI requestUri, String rootUrl, String uri) { - String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, rootUrl, uri); + public static String resolveUri(KeycloakSession session, String rootUrl, String uri) { + String absoluteURI = ResolveRelative.resolveRelativeUri(session, rootUrl, uri); return StringPropertyReplacer.replaceProperties(absoluteURI); } - public static String getManagementUrl(URI requestUri, ClientModel client) { + public static String getManagementUrl(KeycloakSession session, ClientModel client) { String mgmtUrl = client.getManagementUrl(); if (mgmtUrl == null || mgmtUrl.equals("")) { return null; } - String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, client.getRootUrl(), mgmtUrl); + String absoluteURI = ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), mgmtUrl); // this is for resolving URI like "http://${jboss.host.name}:8080/..." in order to send request to same machine and avoid request to LB in cluster environment return StringPropertyReplacer.replaceProperties(absoluteURI); @@ -84,8 +84,8 @@ public class ResourceAdminManager { // For non-cluster setup, return just single configured managementUrls // For cluster setup, return the management Urls corresponding to all registered cluster nodes - private List getAllManagementUrls(URI requestUri, ClientModel client) { - String baseMgmtUrl = getManagementUrl(requestUri, client); + private List getAllManagementUrls(ClientModel client) { + String baseMgmtUrl = getManagementUrl(session, client); if (baseMgmtUrl == null) { return Collections.emptyList(); } @@ -107,14 +107,14 @@ public class ResourceAdminManager { return result; } - public void logoutUser(URI requestUri, RealmModel realm, UserModel user, KeycloakSession keycloakSession) { + public void logoutUser(RealmModel realm, UserModel user, KeycloakSession keycloakSession) { keycloakSession.users().setNotBeforeForUser(realm, user, Time.currentTime()); List userSessions = keycloakSession.sessions().getUserSessions(realm, user); - logoutUserSessions(requestUri, realm, userSessions); + logoutUserSessions(realm, userSessions); } - protected void logoutUserSessions(URI requestUri, RealmModel realm, List userSessions) { + protected void logoutUserSessions(RealmModel realm, List userSessions) { // Map from "app" to clientSessions for this app MultivaluedHashMap clientSessions = new MultivaluedHashMap<>(); for (UserSessionModel userSession : userSessions) { @@ -128,7 +128,7 @@ public class ResourceAdminManager { if (entry.getValue().size() == 0) { continue; } - logoutClientSessions(requestUri, realm, entry.getValue().get(0).getClient(), entry.getValue()); + logoutClientSessions(realm, entry.getValue().get(0).getClient(), entry.getValue()); } } @@ -139,12 +139,12 @@ public class ResourceAdminManager { } - public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, AuthenticatedClientSessionModel clientSession) { - return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession)); + public boolean logoutClientSession(RealmModel realm, ClientModel resource, AuthenticatedClientSessionModel clientSession) { + return logoutClientSessions(realm, resource, Arrays.asList(clientSession)); } - protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ClientModel resource, List clientSessions) { - String managementUrl = getManagementUrl(requestUri, resource); + protected boolean logoutClientSessions(RealmModel realm, ClientModel resource, List clientSessions) { + String managementUrl = getManagementUrl(session, resource); if (managementUrl != null) { // Key is host, value is list of http sessions for this host @@ -195,27 +195,27 @@ public class ResourceAdminManager { // Methods for logout all - public GlobalRequestResult logoutAll(URI requestUri, RealmModel realm) { + public GlobalRequestResult logoutAll(RealmModel realm) { realm.setNotBefore(Time.currentTime()); List resources = realm.getClients(); logger.debugv("logging out {0} resources ", resources.size()); GlobalRequestResult finalResult = new GlobalRequestResult(); for (ClientModel resource : resources) { - GlobalRequestResult currentResult = logoutClient(requestUri, realm, resource, realm.getNotBefore()); + GlobalRequestResult currentResult = logoutClient(realm, resource, realm.getNotBefore()); finalResult.addAll(currentResult); } return finalResult; } - public GlobalRequestResult logoutClient(URI requestUri, RealmModel realm, ClientModel resource) { + public GlobalRequestResult logoutClient(RealmModel realm, ClientModel resource) { resource.setNotBefore(Time.currentTime()); - return logoutClient(requestUri, realm, resource, resource.getNotBefore()); + return logoutClient(realm, resource, resource.getNotBefore()); } - protected GlobalRequestResult logoutClient(URI requestUri, RealmModel realm, ClientModel resource, int notBefore) { - List mgmtUrls = getAllManagementUrls(requestUri, resource); + protected GlobalRequestResult logoutClient(RealmModel realm, ClientModel resource, int notBefore) { + List mgmtUrls = getAllManagementUrls(resource); if (mgmtUrls.isEmpty()) { logger.debug("No management URL or no registered cluster nodes for the client " + resource.getClientId()); return new GlobalRequestResult(); @@ -251,22 +251,22 @@ public class ResourceAdminManager { } } - public GlobalRequestResult pushRealmRevocationPolicy(URI requestUri, RealmModel realm) { + public GlobalRequestResult pushRealmRevocationPolicy(RealmModel realm) { GlobalRequestResult finalResult = new GlobalRequestResult(); for (ClientModel client : realm.getClients()) { - GlobalRequestResult currentResult = pushRevocationPolicy(requestUri, realm, client, realm.getNotBefore()); + GlobalRequestResult currentResult = pushRevocationPolicy(realm, client, realm.getNotBefore()); finalResult.addAll(currentResult); } return finalResult; } - public GlobalRequestResult pushClientRevocationPolicy(URI requestUri, RealmModel realm, ClientModel client) { - return pushRevocationPolicy(requestUri, realm, client, client.getNotBefore()); + public GlobalRequestResult pushClientRevocationPolicy(RealmModel realm, ClientModel client) { + return pushRevocationPolicy(realm, client, client.getNotBefore()); } - protected GlobalRequestResult pushRevocationPolicy(URI requestUri, RealmModel realm, ClientModel resource, int notBefore) { - List mgmtUrls = getAllManagementUrls(requestUri, resource); + protected GlobalRequestResult pushRevocationPolicy(RealmModel realm, ClientModel resource, int notBefore) { + List mgmtUrls = getAllManagementUrls(resource); if (mgmtUrls.isEmpty()) { logger.debugf("No management URL or no registered cluster nodes for the client %s", resource.getClientId()); return new GlobalRequestResult(); @@ -297,8 +297,8 @@ public class ResourceAdminManager { : loginProtocol.sendPushRevocationPolicyRequest(realm, resource, notBefore, managementUrl); } - public GlobalRequestResult testNodesAvailability(URI requestUri, RealmModel realm, ClientModel client) { - List mgmtUrls = getAllManagementUrls(requestUri, client); + public GlobalRequestResult testNodesAvailability(RealmModel realm, ClientModel client) { + List mgmtUrls = getAllManagementUrls(client); if (mgmtUrls.isEmpty()) { logger.debug("No management URL or no registered cluster nodes for the application " + client.getClientId()); return new GlobalRequestResult(); diff --git a/services/src/main/java/org/keycloak/services/resources/Cors.java b/services/src/main/java/org/keycloak/services/resources/Cors.java index 66bf6b1320..33c90e79f9 100755 --- a/services/src/main/java/org/keycloak/services/resources/Cors.java +++ b/services/src/main/java/org/keycloak/services/resources/Cors.java @@ -30,6 +30,7 @@ import org.jboss.resteasy.spi.HttpResponse; import org.keycloak.common.util.CollectionUtil; import org.keycloak.common.util.UriUtils; import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.protocol.oidc.utils.WebOriginsUtils; import org.keycloak.representations.AccessToken; @@ -103,9 +104,9 @@ public class Cors { return this; } - public Cors allowedOrigins(UriInfo uriInfo, ClientModel client) { + public Cors allowedOrigins(KeycloakSession session, ClientModel client) { if (client != null) { - allowedOrigins = WebOriginsUtils.resolveValidWebOrigins(uriInfo, client); + allowedOrigins = WebOriginsUtils.resolveValidWebOrigins(session, client); } return this; } diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 485b2ce96d..d967efd510 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -213,7 +213,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal this.event.event(EventType.CLIENT_INITIATED_ACCOUNT_LINKING); checkRealm(); ClientModel client = checkClient(clientId); - redirectUri = RedirectUtils.verifyRedirectUri(session.getContext().getUri(), redirectUri, realmModel, client); + redirectUri = RedirectUtils.verifyRedirectUri(session, redirectUri, client); if (redirectUri == null) { event.error(Errors.INVALID_REDIRECT_URI); throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST); @@ -1258,7 +1258,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } private Response corsResponse(Response response, ClientModel clientModel) { - return Cors.add(this.request, Response.fromResponse(response)).auth().allowedOrigins(session.getContext().getUri(), clientModel).build(); + return Cors.add(this.request, Response.fromResponse(response)).auth().allowedOrigins(session, clientModel).build(); } private void fireErrorEvent(String message, Throwable throwable) { diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index c80fae3f49..2996ce4a5d 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -107,7 +107,6 @@ public class KeycloakApplication extends Application { protected Set> classes = new HashSet>(); protected KeycloakSessionFactory sessionFactory; - protected String contextPath; public KeycloakApplication() { @@ -123,7 +122,6 @@ public class KeycloakApplication extends Application { loadConfig(context); - this.contextPath = context.getContextPath(); this.sessionFactory = createSessionFactory(); Resteasy.pushDefaultContextObject(KeycloakApplication.class, this); @@ -243,7 +241,7 @@ public class KeycloakApplication extends Application { } if (createMasterRealm) { - applianceBootstrap.createMasterRealm(contextPath); + applianceBootstrap.createMasterRealm(); } session.getTransactionManager().commit(); } catch (RuntimeException re) { @@ -281,20 +279,6 @@ public class KeycloakApplication extends Application { } } - public String getContextPath() { - return contextPath; - } - - /** - * Get base URI of WAR distribution, not JAX-RS - * - * @param uriInfo - * @return - */ - public URI getBaseUri(UriInfo uriInfo) { - return uriInfo.getBaseUriBuilder().replacePath(getContextPath()).build(); - } - public static void loadConfig(ServletContext context) { try { JsonNode node = null; @@ -410,7 +394,6 @@ public class KeycloakApplication extends Application { try { RealmManager manager = new RealmManager(session); - manager.setContextPath(getContextPath()); if (rep.getId() != null && manager.getRealm(rep.getId()) != null) { ServicesLogger.LOGGER.realmExists(rep.getRealm(), from); diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java index dfa224dc93..87050a6ae0 100644 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java @@ -222,7 +222,7 @@ public class LoginActionsServiceChecks { ClientModel client = context.getAuthenticationSession().getClient(); - if (RedirectUtils.verifyRedirectUri(context.getUriInfo(), redirectUri, context.getRealm(), client) == null) { + if (RedirectUtils.verifyRedirectUri(context.getSession(), redirectUri, client) == null) { throw new ExplainedTokenVerificationException(t, Errors.INVALID_REDIRECT_URI, Messages.INVALID_REDIRECT_URI); } diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 8ac3c046eb..806e9dde01 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -160,7 +160,7 @@ public class RealmsResource { if (client.getRootUrl() != null && (client.getBaseUrl() == null || client.getBaseUrl().isEmpty())) { targetUri = KeycloakUriBuilder.fromUri(client.getRootUrl()).build(); } else { - targetUri = KeycloakUriBuilder.fromUri(ResolveRelative.resolveRelativeUri(session.getContext().getUri().getRequestUri(), client.getRootUrl(), client.getBaseUrl())).build(); + targetUri = KeycloakUriBuilder.fromUri(ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), client.getBaseUrl())).build(); } return Response.seeOther(targetUri).build(); diff --git a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java index 3f3d73a15e..074cfda452 100755 --- a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java @@ -33,6 +33,7 @@ import org.keycloak.services.util.CookieHelper; import org.keycloak.theme.BrowserSecurityHeaderSetup; import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.Theme; +import org.keycloak.urls.UrlType; import org.keycloak.utils.MediaType; import javax.ws.rs.Consumes; @@ -182,10 +183,9 @@ public class WelcomeResource { map.put("productNameFull", Version.NAME_FULL); map.put("properties", theme.getProperties()); + map.put("adminUrl", session.getContext().getUri(UrlType.ADMIN).getBaseUriBuilder().path("/admin/").build()); - URI uri = Urls.themeRoot(session.getContext().getUri().getBaseUri()); - String resourcesPath = uri.getPath() + "/" + theme.getType().toString().toLowerCase() + "/" + theme.getName(); - map.put("resourcesPath", resourcesPath); + map.put("resourcesPath", "resources/" + Version.RESOURCES_VERSION + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName()); boolean bootstrap = shouldBootstrap(); map.put("bootstrap", bootstrap); @@ -210,7 +210,7 @@ public class WelcomeResource { ResponseBuilder rb = Response.status(errorMessage == null ? Status.OK : Status.BAD_REQUEST) .entity(result) .cacheControl(CacheControlUtil.noCache()); - BrowserSecurityHeaderSetup.headers(rb, BrowserSecurityHeaders.defaultHeaders); + BrowserSecurityHeaderSetup.headers(rb); return rb.build(); } catch (Exception e) { throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR); diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java index 804e287c41..0cda7d4fd6 100644 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountConsole.java @@ -24,6 +24,7 @@ import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.Theme; import org.keycloak.theme.beans.MessageFormatterMethod; +import org.keycloak.urls.UrlType; import org.keycloak.utils.MediaType; import javax.json.Json; @@ -77,7 +78,7 @@ public class AccountConsole { @GET @NoCache - public Response getMainPage() throws URISyntaxException, IOException, FreeMarkerException { + public Response getMainPage() throws IOException, FreeMarkerException { if (!session.getContext().getUri().getRequestUri().getPath().endsWith("/")) { return Response.status(302).location(session.getContext().getUri().getRequestUriBuilder().path("/").build()).build(); } else { @@ -85,8 +86,8 @@ public class AccountConsole { URI baseUri = session.getContext().getUri().getBaseUri(); - map.put("authUrl", session.getContext().getContextPath()); - map.put("baseUrl", session.getContext().getContextPath() + "/realms/" + realm.getName() + "/account"); + map.put("authUrl", session.getContext().getUri(UrlType.FRONTEND).getBaseUri().toString()); + map.put("baseUrl", session.getContext().getUri(UrlType.FRONTEND).getBaseUriBuilder().replacePath("/realms/" + realm.getName() + "/account").build().toString()); map.put("realm", realm); map.put("resourceUrl", Urls.themeRoot(baseUri).getPath() + "/account/" + theme.getName()); map.put("resourceVersion", Version.RESOURCES_VERSION); @@ -195,9 +196,9 @@ public class AccountConsole { ClientModel referrerClient = realm.getClientByClientId(referrer); if (referrerClient != null) { if (referrerUri != null) { - referrerUri = RedirectUtils.verifyRedirectUri(session.getContext().getUri(), referrerUri, realm, referrerClient); + referrerUri = RedirectUtils.verifyRedirectUri(session, referrerUri, referrerClient); } else { - referrerUri = ResolveRelative.resolveRelativeUri(session.getContext().getUri().getRequestUri(), client.getRootUrl(), referrerClient.getBaseUrl()); + referrerUri = ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), referrerClient.getBaseUrl()); } if (referrerUri != null) { @@ -210,7 +211,7 @@ public class AccountConsole { } else if (referrerUri != null) { referrerClient = realm.getClientByClientId(referrer); if (client != null) { - referrerUri = RedirectUtils.verifyRedirectUri(session.getContext().getUri(), referrerUri, realm, referrerClient); + referrerUri = RedirectUtils.verifyRedirectUri(session, referrerUri, referrerClient); if (referrerUri != null) { return new String[]{referrer, referrer, referrerUri}; diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java index 8ec8d43c4d..7f0e00827c 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java @@ -996,9 +996,9 @@ public class AccountFormService extends AbstractSecuredLocalService { ClientModel referrerClient = realm.getClientByClientId(referrer); if (referrerClient != null) { if (referrerUri != null) { - referrerUri = RedirectUtils.verifyRedirectUri(session.getContext().getUri(), referrerUri, realm, referrerClient); + referrerUri = RedirectUtils.verifyRedirectUri(session, referrerUri, referrerClient); } else { - referrerUri = ResolveRelative.resolveRelativeUri(session.getContext().getUri().getRequestUri(), referrerClient.getRootUrl(), referrerClient.getBaseUrl()); + referrerUri = ResolveRelative.resolveRelativeUri(session, referrerClient.getRootUrl(), referrerClient.getBaseUrl()); } if (referrerUri != null) { @@ -1010,7 +1010,7 @@ public class AccountFormService extends AbstractSecuredLocalService { } } else if (referrerUri != null) { if (client != null) { - referrerUri = RedirectUtils.verifyRedirectUri(session.getContext().getUri(), referrerUri, realm, client); + referrerUri = RedirectUtils.verifyRedirectUri(session, referrerUri, client); if (referrerUri != null) { return new String[]{referrer, referrerUri}; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index 46bd1017a7..47ce0cdc20 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -25,6 +25,7 @@ import javax.ws.rs.NotFoundException; import org.keycloak.Config; import org.keycloak.common.ClientConnection; import org.keycloak.common.Version; +import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -33,6 +34,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.protocol.oidc.utils.WebOriginsUtils; import org.keycloak.services.Urls; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager; @@ -42,6 +44,7 @@ import org.keycloak.theme.BrowserSecurityHeaderSetup; import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerUtil; import org.keycloak.theme.Theme; +import org.keycloak.urls.UrlType; import org.keycloak.utils.MediaType; import javax.ws.rs.GET; @@ -51,6 +54,8 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; import java.io.IOException; import java.net.URI; @@ -169,9 +174,7 @@ public class AdminConsole { if (consoleApp == null) { throw new NotFoundException("Could not find admin console client"); } - return new ClientManager(new RealmManager(session)).toInstallationRepresentation(realm, consoleApp, session.getContext().getAuthServerUrl()); - - } + return new ClientManager(new RealmManager(session)).toInstallationRepresentation(realm, consoleApp, session.getContext().getUri().getBaseUri()); } /** * Permission information @@ -255,10 +258,10 @@ public class AdminConsole { @GET @NoCache public Response logout() { - URI redirect = AdminRoot.adminConsoleUrl(session.getContext().getUri()).build(realm.getName()); + URI redirect = AdminRoot.adminConsoleUrl(session.getContext().getUri(UrlType.ADMIN)).build(realm.getName()); return Response.status(302).location( - OIDCLoginProtocolService.logoutUrl(session.getContext().getUri()).queryParam("redirect_uri", redirect.toString()).build(realm.getName()) + OIDCLoginProtocolService.logoutUrl(session.getContext().getUri(UrlType.ADMIN)).queryParam("redirect_uri", redirect.toString()).build(realm.getName()) ).build(); } @@ -274,19 +277,30 @@ public class AdminConsole { */ @GET @NoCache - public Response getMainPage() throws URISyntaxException, IOException, FreeMarkerException { - if (!session.getContext().getUri().getRequestUri().getPath().endsWith("/")) { - return Response.status(302).location(session.getContext().getUri().getRequestUriBuilder().path("/").build()).build(); + public Response getMainPage() throws IOException, FreeMarkerException { + if (!session.getContext().getUri(UrlType.ADMIN).getRequestUri().getPath().endsWith("/")) { + return Response.status(302).location(session.getContext().getUri(UrlType.ADMIN).getRequestUriBuilder().path("/").build()).build(); } else { Theme theme = AdminRoot.getTheme(session, realm); Map map = new HashMap<>(); - URI baseUri = session.getContext().getUri().getBaseUri(); + URI adminBaseUri = session.getContext().getUri(UrlType.ADMIN).getBaseUri(); + String adminBaseUrl = adminBaseUri.toString(); + if (adminBaseUrl.endsWith("/")) { + adminBaseUrl = adminBaseUrl.substring(0, adminBaseUrl.length() - 1); + } - map.put("authUrl", session.getContext().getContextPath()); - map.put("consoleBaseUrl", Urls.adminConsoleRoot(baseUri, realm.getName()).getPath()); - map.put("resourceUrl", Urls.themeRoot(baseUri).getPath() + "/admin/" + theme.getName()); + URI authServerBaseUri = session.getContext().getUri(UrlType.FRONTEND).getBaseUri(); + String authServerBaseUrl = authServerBaseUri.toString(); + if (authServerBaseUrl.endsWith("/")) { + authServerBaseUrl = authServerBaseUrl.substring(0, authServerBaseUrl.length() - 1); + } + + map.put("authServerUrl", authServerBaseUrl); + map.put("authUrl", adminBaseUrl); + map.put("consoleBaseUrl", Urls.adminConsoleRoot(adminBaseUri, realm.getName()).getPath()); + map.put("resourceUrl", Urls.themeRoot(adminBaseUri).getPath() + "/admin/" + theme.getName()); map.put("masterRealm", Config.getAdminRealm()); map.put("resourceVersion", Version.RESOURCES_VERSION); map.put("properties", theme.getProperties()); @@ -294,7 +308,16 @@ public class AdminConsole { FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil(); String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme); Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result); - BrowserSecurityHeaderSetup.headers(builder, realm); + + BrowserSecurityHeaderSetup.Options headerOptions = null; + + // Replace CSP if admin is hosted on different URL + if (!adminBaseUri.equals(authServerBaseUri)) { + headerOptions = BrowserSecurityHeaderSetup.Options.create().allowFrameSrc(UriBuilder.fromUri(authServerBaseUri).replacePath("").build().toString()).build(); + } + + BrowserSecurityHeaderSetup.headers(builder, realm, headerOptions); + return builder.build(); } } @@ -302,7 +325,7 @@ public class AdminConsole { @GET @Path("{indexhtml: index.html}") // this expression is a hack to get around jaxdoclet generation bug. Doesn't like index.html public Response getIndexHtmlRedirect() { - return Response.status(302).location(session.getContext().getUri().getRequestUriBuilder().path("../").build()).build(); + return Response.status(302).location(session.getContext().getUri(UrlType.ADMIN).getRequestUriBuilder().path("../").build()).build(); } @GET diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index 984ff67a83..858e91dab0 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -38,6 +38,7 @@ import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.admin.info.ServerInfoAdminResource; import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.theme.Theme; +import org.keycloak.urls.UrlType; import javax.ws.rs.GET; import javax.ws.rs.HttpMethod; @@ -100,7 +101,7 @@ public class AdminRoot { public Response masterRealmAdminConsoleRedirect() { RealmModel master = new RealmManager(session).getKeycloakAdminstrationRealm(); return Response.status(302).location( - session.getContext().getUri().getBaseUriBuilder().path(AdminRoot.class).path(AdminRoot.class, "getAdminConsole").path("/").build(master.getName()) + session.getContext().getUri(UrlType.ADMIN).getBaseUriBuilder().path(AdminRoot.class).path(AdminRoot.class, "getAdminConsole").path("/").build(master.getName()) ).build(); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 795d6b8cbb..0dfa9a0366 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -195,7 +195,7 @@ public class ClientResource { ClientInstallationProvider provider = session.getProvider(ClientInstallationProvider.class, providerId); if (provider == null) throw new NotFoundException("Unknown Provider"); - return provider.generateInstallation(session, realm, client, keycloak.getBaseUri(session.getContext().getUri())); + return provider.generateInstallation(session, realm, client, session.getContext().getUri().getBaseUri()); } /** @@ -424,7 +424,7 @@ public class ClientResource { auth.clients().requireConfigure(client); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).resource(ResourceType.CLIENT).success(); - return new ResourceAdminManager(session).pushClientRevocationPolicy(session.getContext().getUri().getRequestUri(), realm, client); + return new ResourceAdminManager(session).pushClientRevocationPolicy(realm, client); } @@ -598,7 +598,7 @@ public class ClientResource { auth.clients().requireConfigure(client); logger.debug("Test availability of cluster nodes"); - GlobalRequestResult result = new ResourceAdminManager(session).testNodesAvailability(session.getContext().getUri().getRequestUri(), realm, client); + GlobalRequestResult result = new ResourceAdminManager(session).testNodesAvailability(realm, client); adminEvent.operation(OperationType.ACTION).resource(ResourceType.CLUSTER_NODE).resourcePath(session.getContext().getUri()).representation(result).success(); return result; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 2cb30163bc..f738aeeacf 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -551,7 +551,7 @@ public class RealmAdminResource { public GlobalRequestResult pushRevocation() { auth.realm().requireManageRealm(); - GlobalRequestResult result = new ResourceAdminManager(session).pushRealmRevocationPolicy(session.getContext().getUri().getRequestUri(), realm); + GlobalRequestResult result = new ResourceAdminManager(session).pushRealmRevocationPolicy(realm); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(result).success(); return result; } @@ -567,7 +567,7 @@ public class RealmAdminResource { auth.users().requireManage(); session.sessions().removeUserSessions(realm); - GlobalRequestResult result = new ResourceAdminManager(session).logoutAll(session.getContext().getUri().getRequestUri(), realm); + GlobalRequestResult result = new ResourceAdminManager(session).logoutAll(realm); adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(result).success(); return result; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java index 3681b92ea7..75405bb734 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java @@ -131,7 +131,6 @@ public class RealmsAdminResource { @Consumes(MediaType.APPLICATION_JSON) public Response importRealm(final RealmRepresentation rep) { RealmManager realmManager = new RealmManager(session); - realmManager.setContextPath(keycloak.getContextPath()); AdminPermissions.realms(session, auth).requireCreateRealm(); logger.debugv("importRealm: {0}", rep.getRealm()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index 3508bbbdeb..a43b7c7c27 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -699,7 +699,7 @@ public class UserResource { String redirect; if (redirectUri != null) { - redirect = RedirectUtils.verifyRedirectUri(session.getContext().getUri(), redirectUri, realm, client); + redirect = RedirectUtils.verifyRedirectUri(session, redirectUri, client); if (redirect == null) { throw new WebApplicationException( ErrorResponse.error("Invalid redirect uri.", Status.BAD_REQUEST)); diff --git a/services/src/main/java/org/keycloak/services/util/ResolveRelative.java b/services/src/main/java/org/keycloak/services/util/ResolveRelative.java index 2dad0a66f9..27ea55f7f4 100755 --- a/services/src/main/java/org/keycloak/services/util/ResolveRelative.java +++ b/services/src/main/java/org/keycloak/services/util/ResolveRelative.java @@ -17,6 +17,10 @@ package org.keycloak.services.util; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.urls.UrlType; + import javax.ws.rs.core.UriBuilder; import java.net.URI; @@ -25,19 +29,30 @@ import java.net.URI; * @version $Revision: 1 $ */ public class ResolveRelative { - public static String resolveRelativeUri(URI requestUri, String rootUrl, String url) { - if (url == null || !url.startsWith("/")) return url; - if (rootUrl != null) { - return rootUrl + url; - } else if (requestUri != null) { - UriBuilder builder = UriBuilder.fromPath(url).host(requestUri.getHost()); - builder.scheme(requestUri.getScheme()); - if (requestUri.getPort() != -1) { - builder.port(requestUri.getPort()); - } - return builder.build().toString(); + public static String resolveRelativeUri(KeycloakSession session, String rootUrl, String url) { + if (url == null || !url.startsWith("/")) { + return url; + } else if (rootUrl != null) { + return resolveRootUrl(session, rootUrl) + url; } else { - return null; + return session.getContext().getUri().getBaseUriBuilder().replacePath(url).build().toString(); } } + + public static String resolveRootUrl(KeycloakSession session, String rootUrl) { + if (rootUrl != null) { + if (rootUrl.equals(Constants.AUTH_BASE_URL_PROP)) { + rootUrl = session.getContext().getUri(UrlType.FRONTEND).getBaseUri().toString(); + if (rootUrl.endsWith("/")) { + rootUrl = rootUrl.substring(0, rootUrl.length() - 1); + } + } else if (rootUrl.equals(Constants.AUTH_ADMIN_URL_PROP)) { + rootUrl = session.getContext().getUri(UrlType.ADMIN).getBaseUri().toString(); + if (rootUrl.endsWith("/")) { + rootUrl = rootUrl.substring(0, rootUrl.length() - 1); + } + } + } + return rootUrl; + } } diff --git a/services/src/main/java/org/keycloak/theme/BrowserSecurityHeaderSetup.java b/services/src/main/java/org/keycloak/theme/BrowserSecurityHeaderSetup.java index d4c209fd22..477d79b3d9 100755 --- a/services/src/main/java/org/keycloak/theme/BrowserSecurityHeaderSetup.java +++ b/services/src/main/java/org/keycloak/theme/BrowserSecurityHeaderSetup.java @@ -20,6 +20,7 @@ package org.keycloak.theme; import org.keycloak.models.BrowserSecurityHeaders; import org.keycloak.models.RealmModel; +import javax.swing.text.html.Option; import javax.ws.rs.core.Response; import java.util.Map; @@ -29,15 +30,50 @@ import java.util.Map; */ public class BrowserSecurityHeaderSetup { - public static Response.ResponseBuilder headers(Response.ResponseBuilder builder, RealmModel realm) { - return headers(builder, realm.getBrowserSecurityHeaders()); + public static class Options { + + private String allowedFrameSrc; + + public static Options create() { + return new Options(); + } + + public Options allowFrameSrc(String source) { + allowedFrameSrc = source; + return this; + } + + public Options build() { + return this; + } } - public static Response.ResponseBuilder headers(Response.ResponseBuilder builder, Map headers) { + public static Response.ResponseBuilder headers(Response.ResponseBuilder builder, RealmModel realm) { + return headers(builder, realm.getBrowserSecurityHeaders(), null); + } + + + public static Response.ResponseBuilder headers(Response.ResponseBuilder builder, RealmModel realm, Options options) { + return headers(builder, realm.getBrowserSecurityHeaders(), options); + } + + public static Response.ResponseBuilder headers(Response.ResponseBuilder builder) { + return headers(builder, BrowserSecurityHeaders.defaultHeaders, null); + } + + private static Response.ResponseBuilder headers(Response.ResponseBuilder builder, Map headers, Options options) { for (Map.Entry entry : headers.entrySet()) { - String headerName = BrowserSecurityHeaders.headerAttributeMap.get(entry.getKey()); - if (headerName != null && entry.getValue() != null && entry.getValue().length() > 0) { - builder.header(headerName, entry.getValue()); + String header = BrowserSecurityHeaders.headerAttributeMap.get(entry.getKey()); + String value = entry.getValue(); + + if (options != null) { + if (header.equals(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY) && value.equals(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY_DEFAULT) && options.allowedFrameSrc != null) { + value = "frame-src " + options.allowedFrameSrc + "; frame-ancestors 'self'; object-src 'none';"; + } + } + + if (header != null && value != null && !value.isEmpty()) { + builder.header(header, value); } } return builder; diff --git a/services/src/main/java/org/keycloak/url/DefaultHostnameProvider.java b/services/src/main/java/org/keycloak/url/DefaultHostnameProvider.java new file mode 100644 index 0000000000..1472a10dd9 --- /dev/null +++ b/services/src/main/java/org/keycloak/url/DefaultHostnameProvider.java @@ -0,0 +1,112 @@ +package org.keycloak.url; + +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.urls.HostnameProvider; +import org.keycloak.urls.UrlType; + +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.net.URISyntaxException; + +public class DefaultHostnameProvider implements HostnameProvider { + + private static final Logger LOGGER = Logger.getLogger(DefaultHostnameProvider.class); + + private final KeycloakSession session; + + private final URI frontendUri; + + private String currentRealm; + + private URI realmUri; + + private URI adminUri; + + private final boolean forceBackendUrlToFrontendUrl; + + public DefaultHostnameProvider(KeycloakSession session, URI frontendUri, URI adminUri, boolean forceBackendUrlToFrontendUrl) { + this.session = session; + this.frontendUri = frontendUri; + this.adminUri = adminUri; + this.forceBackendUrlToFrontendUrl = forceBackendUrlToFrontendUrl; + } + + @Override + public String getScheme(UriInfo originalUriInfo, UrlType type) { + return resolveUri(originalUriInfo, type).getScheme(); + } + + @Override + public String getHostname(UriInfo originalUriInfo, UrlType type) { + return resolveUri(originalUriInfo, type).getHost(); + } + + @Override + public int getPort(UriInfo originalUriInfo, UrlType type) { + return resolveUri(originalUriInfo, type).getPort(); + } + + @Override + public String getContextPath(UriInfo originalUriInfo, UrlType type) { + return resolveUri(originalUriInfo, type).getPath(); + } + + private URI resolveUri(UriInfo originalUriInfo, UrlType type) { + URI realmUri = getRealmUri(); + URI frontendUri = realmUri != null ? realmUri : this.frontendUri; + + // Use frontend URI for backend requests if forceBackendUrlToFrontendUrl is true + if (type.equals(UrlType.BACKEND) && forceBackendUrlToFrontendUrl) { + type = UrlType.FRONTEND; + } + + // Use frontend URI for backend requests if request hostname matches frontend hostname + if (type.equals(UrlType.BACKEND) && frontendUri != null && originalUriInfo.getBaseUri().getHost().equals(frontendUri.getHost())) { + type = UrlType.FRONTEND; + } + + // Use frontend URI for admin requests if adminUrl not set + if (type.equals(UrlType.ADMIN)) { + if (adminUri != null) { + return adminUri; + } else { + type = UrlType.FRONTEND; + } + } + + if (type.equals(UrlType.FRONTEND) && frontendUri != null) { + return frontendUri; + } + + return originalUriInfo.getBaseUri(); + } + + private URI getRealmUri() { + RealmModel realm = session.getContext().getRealm(); + if (realm == null) { + currentRealm = null; + realmUri = null; + + return null; + } else if (realm.getId().equals(currentRealm)) { + return realmUri; + } else { + currentRealm = realm.getId(); + realmUri = null; + + String realmFrontendUrl = session.getContext().getRealm().getAttribute("frontendUrl"); + if (realmFrontendUrl != null) { + try { + realmUri = new URI(realmFrontendUrl); + } catch (URISyntaxException e) { + LOGGER.error("Failed to parse realm frontendUrl. Falling back to global value.", e); + } + } + + return realmUri; + } + } + +} diff --git a/services/src/main/java/org/keycloak/url/DefaultHostnameProviderFactory.java b/services/src/main/java/org/keycloak/url/DefaultHostnameProviderFactory.java new file mode 100644 index 0000000000..fc09c0dd18 --- /dev/null +++ b/services/src/main/java/org/keycloak/url/DefaultHostnameProviderFactory.java @@ -0,0 +1,56 @@ +package org.keycloak.url; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.urls.HostnameProvider; +import org.keycloak.urls.HostnameProviderFactory; + +import java.net.URI; +import java.net.URISyntaxException; + +public class DefaultHostnameProviderFactory implements HostnameProviderFactory { + + private static final Logger LOGGER = Logger.getLogger(DefaultHostnameProviderFactory.class); + + private URI frontendUri; + private URI adminUri; + private boolean forceBackendUrlToFrontendUrl; + + @Override + public HostnameProvider create(KeycloakSession session) { + return new DefaultHostnameProvider(session, frontendUri, adminUri, forceBackendUrlToFrontendUrl); + } + + @Override + public void init(Config.Scope config) { + String frontendUrl = config.get("frontendUrl"); + String adminUrl = config.get("adminUrl"); + + if (frontendUrl != null && !frontendUrl.isEmpty()) { + try { + frontendUri = new URI(frontendUrl); + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid value for frontendUrl", e); + } + } + + if (adminUrl != null && !adminUrl.isEmpty()) { + try { + adminUri = new URI(adminUrl); + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid value for adminUrl", e); + } + } + + forceBackendUrlToFrontendUrl = config.getBoolean("forceBackendUrlToFrontendUrl", false); + + LOGGER.infov("Frontend: {0}, Admin: {1}, Backend: {2}", frontendUri != null ? frontendUri.toString() : "", adminUri != null ? adminUri.toString() : "", forceBackendUrlToFrontendUrl ? "" : ""); + } + + @Override + public String getId() { + return "default"; + } + +} diff --git a/services/src/main/java/org/keycloak/url/FixedHostnameProvider.java b/services/src/main/java/org/keycloak/url/FixedHostnameProvider.java index e438e87491..bd618a03b3 100644 --- a/services/src/main/java/org/keycloak/url/FixedHostnameProvider.java +++ b/services/src/main/java/org/keycloak/url/FixedHostnameProvider.java @@ -6,6 +6,7 @@ import org.keycloak.urls.HostnameProvider; import javax.ws.rs.core.UriInfo; +@Deprecated public class FixedHostnameProvider implements HostnameProvider { private final KeycloakSession session; diff --git a/services/src/main/java/org/keycloak/url/FixedHostnameProviderFactory.java b/services/src/main/java/org/keycloak/url/FixedHostnameProviderFactory.java index e7e96d906e..83d4a7af33 100644 --- a/services/src/main/java/org/keycloak/url/FixedHostnameProviderFactory.java +++ b/services/src/main/java/org/keycloak/url/FixedHostnameProviderFactory.java @@ -1,12 +1,18 @@ package org.keycloak.url; +import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.urls.HostnameProvider; import org.keycloak.urls.HostnameProviderFactory; +@Deprecated public class FixedHostnameProviderFactory implements HostnameProviderFactory { + private static final Logger LOGGER = Logger.getLogger(RequestHostnameProviderFactory.class); + + private boolean loggedDeprecatedWarning = false; + private String hostname; private int httpPort; private int httpsPort; @@ -14,16 +20,17 @@ public class FixedHostnameProviderFactory implements HostnameProviderFactory { @Override public HostnameProvider create(KeycloakSession session) { + if (!loggedDeprecatedWarning) { + loggedDeprecatedWarning = true; + LOGGER.warn("fixed hostname provider is deprecated, please switch to the default hostname provider"); + } + return new FixedHostnameProvider(session, alwaysHttps, hostname, httpPort, httpsPort); } @Override public void init(Config.Scope config) { this.hostname = config.get("hostname"); - if (this.hostname == null) { - throw new RuntimeException("hostname not set"); - } - this.httpPort = config.getInt("httpPort", -1); this.httpsPort = config.getInt("httpsPort", -1); this.alwaysHttps = config.getBoolean("alwaysHttps", false); diff --git a/services/src/main/java/org/keycloak/url/RequestHostnameProvider.java b/services/src/main/java/org/keycloak/url/RequestHostnameProvider.java index e561efbc29..b47a09d8e7 100644 --- a/services/src/main/java/org/keycloak/url/RequestHostnameProvider.java +++ b/services/src/main/java/org/keycloak/url/RequestHostnameProvider.java @@ -4,6 +4,7 @@ import org.keycloak.urls.HostnameProvider; import javax.ws.rs.core.UriInfo; +@Deprecated public class RequestHostnameProvider implements HostnameProvider { @Override diff --git a/services/src/main/java/org/keycloak/url/RequestHostnameProviderFactory.java b/services/src/main/java/org/keycloak/url/RequestHostnameProviderFactory.java index d5cc384ba5..f0bdfffe59 100644 --- a/services/src/main/java/org/keycloak/url/RequestHostnameProviderFactory.java +++ b/services/src/main/java/org/keycloak/url/RequestHostnameProviderFactory.java @@ -1,15 +1,26 @@ package org.keycloak.url; +import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; import org.keycloak.urls.HostnameProvider; import org.keycloak.urls.HostnameProviderFactory; import javax.ws.rs.core.UriInfo; +@Deprecated public class RequestHostnameProviderFactory implements HostnameProviderFactory { + private static final Logger LOGGER = Logger.getLogger(RequestHostnameProviderFactory.class); + + private boolean loggedDeprecatedWarning = false; + @Override public HostnameProvider create(KeycloakSession session) { + if (!loggedDeprecatedWarning) { + loggedDeprecatedWarning = true; + LOGGER.warn("request hostname provider is deprecated, please switch to the default hostname provider"); + } + return new RequestHostnameProvider(); } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.urls.HostnameProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.urls.HostnameProviderFactory index 9177b0f466..21bc79d425 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.urls.HostnameProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.urls.HostnameProviderFactory @@ -1,2 +1,3 @@ +org.keycloak.url.DefaultHostnameProviderFactory org.keycloak.url.FixedHostnameProviderFactory org.keycloak.url.RequestHostnameProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md index ae89fbeb6f..99755098d3 100644 --- a/testsuite/integration-arquillian/HOW-TO-RUN.md +++ b/testsuite/integration-arquillian/HOW-TO-RUN.md @@ -354,6 +354,7 @@ This test will: -Djdbc.mvn.artifactId=mysql-connector-java \ -Djdbc.mvn.version=8.0.12 \ -Djdbc.mvn.version.legacy=5.1.38 \ + -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver \ -Dkeycloak.connectionsJpa.url=jdbc:mysql://$DB_HOST/keycloak \ -Dkeycloak.connectionsJpa.user=keycloak \ -Dkeycloak.connectionsJpa.password=keycloak diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/Creator.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/Creator.java index cf062d9e07..7358de9b07 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/Creator.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/updaters/Creator.java @@ -17,6 +17,8 @@ package org.keycloak.testsuite.updaters; import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.ComponentResource; import org.keycloak.admin.client.resource.ComponentsResource; import org.keycloak.admin.client.resource.GroupResource; @@ -24,6 +26,7 @@ import org.keycloak.admin.client.resource.GroupsResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -57,6 +60,16 @@ public class Creator implements AutoCloseable { } } + public static Creator create(RealmResource realmResource, ClientRepresentation rep) { + final ClientsResource clients = realmResource.clients(); + try (Response response = clients.create(rep)) { + String createdId = getCreatedId(response); + final ClientResource r = clients.get(createdId); + LOG.debugf("Created client ID %s", createdId); + return new Creator(createdId, r, r::remove); + } + } + public static Creator create(RealmResource realmResource, UserRepresentation rep) { final UsersResource users = realmResource.users(); try (Response response = users.create(rep)) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java index b4c141a2ab..3cd50e759b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java @@ -1165,7 +1165,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest { "Offline access" )); Assert.assertThat(accountEntry.getClientScopesGranted(), containsInAnyOrder("Full Access")); - Assert.assertEquals(oauth.AUTH_SERVER_ROOT + "/realms/test/account", accountEntry.getHref()); + Assert.assertEquals(oauth.AUTH_SERVER_ROOT + "/realms/test/account/", accountEntry.getHref()); AccountApplicationsPage.AppEntry testAppEntry = apps.get("test-app"); Assert.assertEquals(6, testAppEntry.getRolesAvailable().size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleLandingPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleLandingPageTest.java index 4f6c691291..595b91e949 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleLandingPageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AdminConsoleLandingPageTest.java @@ -43,7 +43,7 @@ public class AdminConsoleLandingPageTest extends AbstractKeycloakTest { String authUrl = body.substring(body.indexOf("var authUrl = '") + 15); authUrl = authUrl.substring(0, authUrl.indexOf("'")); - Assert.assertEquals("/auth", authUrl); + Assert.assertEquals(suiteContext.getAuthServerInfo().getContextRoot() + "/auth", authUrl); String resourceUrl = body.substring(body.indexOf("var resourceUrl = '") + 19); resourceUrl = resourceUrl.substring(0, resourceUrl.indexOf("'")); @@ -67,7 +67,7 @@ public class AdminConsoleLandingPageTest extends AbstractKeycloakTest { while(m.find()) { String url = m.group(1); if (url.contains("keycloak.js")) { - Assert.assertTrue(url, url.startsWith("/auth/js/")); + Assert.assertTrue(url, url.startsWith(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/js/")); } else { Assert.assertTrue(url, url.startsWith("/auth/resources/")); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java index d1e7adbaea..e5ccef0609 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java @@ -131,12 +131,14 @@ public class RealmTest extends AbstractAdminTest { Assert.assertEquals(1, adminClient.realm("master").clients().findByClientId("new-realm").size()); ClientRepresentation adminConsoleClient = adminClient.realm("new").clients().findByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID).get(0); - assertEquals("/auth/admin/new/console/index.html", adminConsoleClient.getBaseUrl()); - assertEquals("/auth/admin/new/console/*", adminConsoleClient.getRedirectUris().get(0)); + assertEquals(Constants.AUTH_ADMIN_URL_PROP, adminConsoleClient.getRootUrl()); + assertEquals("/admin/new/console/", adminConsoleClient.getBaseUrl()); + assertEquals("/admin/new/console/*", adminConsoleClient.getRedirectUris().get(0)); ClientRepresentation accountClient = adminClient.realm("new").clients().findByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).get(0); - assertEquals("/auth/realms/new/account", accountClient.getBaseUrl()); - assertEquals("/auth/realms/new/account/*", accountClient.getRedirectUris().get(0)); + assertEquals(Constants.AUTH_BASE_URL_PROP, accountClient.getRootUrl()); + assertEquals("/realms/new/account/", accountClient.getBaseUrl()); + assertEquals("/realms/new/account/*", accountClient.getRedirectUris().get(0)); } finally { adminClient.realms().realm(rep.getRealm()).remove(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java index 1c445bcbc5..53d179596e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java @@ -373,7 +373,7 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest { waitForPage(driver, "sorry", false); errorPage.assertCurrent(); String link = errorPage.getBackToApplicationLink(); - Assert.assertTrue(link.endsWith("/auth/realms/consumer/account")); + Assert.assertTrue(link.endsWith("/auth/realms/consumer/account/")); } /** diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java index 22d47310a9..e89fe48ea1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java @@ -89,7 +89,7 @@ public class AdapterInstallationConfigTest extends AbstractClientRegistrationTes AdapterConfig config = reg.getAdapterConfig(client.getClientId()); assertNotNull(config); - assertEquals(suiteContext.getAuthServerInfo().getContextRoot() + "/auth", config.getAuthServerUrl()); + assertEquals(suiteContext.getAuthServerInfo().getContextRoot() + "/auth/", config.getAuthServerUrl()); assertEquals("test", config.getRealm()); assertEquals(1, config.getCredentials().size()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRedirectTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRedirectTest.java index 34b20c7a20..c42140caf6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRedirectTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientRedirectTest.java @@ -75,7 +75,7 @@ public class ClientRedirectTest extends AbstractTestRealmKeycloakTest { assertEquals("http://example.org/dummy/base-path", driver.getCurrentUrl()); driver.get(getAuthServerRoot().toString() + "realms/test/clients/account/redirect"); - assertEquals(getAuthServerRoot().toString() + "realms/test/account", driver.getCurrentUrl()); + assertEquals(getAuthServerRoot().toString() + "realms/test/account/", driver.getCurrentUrl()); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMultipleAttributesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMultipleAttributesTest.java index a465799e17..db00e2b1bb 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMultipleAttributesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMultipleAttributesTest.java @@ -200,7 +200,7 @@ public class LDAPMultipleAttributesTest extends AbstractLDAPTest { public void ldapPortalEndToEndTest() { // Login as bwilson oauth.clientId("ldap-portal"); - oauth.redirectUri("/ldap-portal"); + oauth.redirectUri(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/ldap-portal"); loginPage.open(); loginPage.login("bwilson", "Password1"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java index b353964fbf..d0ffeee352 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/AbstractMigrationTest.java @@ -26,8 +26,10 @@ import org.keycloak.component.PrioritizedComponentModel; import org.keycloak.keys.KeyProvider; import org.keycloak.models.AdminRoles; import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; import org.keycloak.models.LDAPConstants; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; @@ -55,6 +57,7 @@ import org.keycloak.testsuite.util.OAuthClient; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -240,6 +243,35 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { testMicroprofileJWTScopeAddedToClient(); } + protected void testMigrationTo8_0_0() { + testAdminClientUrls(masterRealm); + testAdminClientUrls(migrationRealm); + testAccountClientUrls(masterRealm); + testAccountClientUrls(migrationRealm); + } + + private void testAdminClientUrls(RealmResource realm) { + ClientRepresentation adminConsoleClient = realm.clients().findByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID).get(0); + + assertEquals(Constants.AUTH_ADMIN_URL_PROP, adminConsoleClient.getRootUrl()); + String baseUrl = "/admin/" + realm.toRepresentation().getRealm() + "/console/"; + assertEquals(baseUrl, adminConsoleClient.getBaseUrl()); + assertEquals(baseUrl + "*", adminConsoleClient.getRedirectUris().iterator().next()); + assertEquals(1, adminConsoleClient.getRedirectUris().size()); + assertEquals("+", adminConsoleClient.getWebOrigins().iterator().next()); + assertEquals(1, adminConsoleClient.getWebOrigins().size()); + } + + private void testAccountClientUrls(RealmResource realm) { + ClientRepresentation accountConsoleClient = realm.clients().findByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).get(0); + + assertEquals(Constants.AUTH_BASE_URL_PROP, accountConsoleClient.getRootUrl()); + String baseUrl = "/realms/" + realm.toRepresentation().getRealm() + "/account/"; + assertEquals(baseUrl, accountConsoleClient.getBaseUrl()); + assertEquals(baseUrl + "*", accountConsoleClient.getRedirectUris().iterator().next()); + assertEquals(1, accountConsoleClient.getRedirectUris().size()); + } + private void testDecisionStrategySetOnResourceServer() { ClientsResource clients = migrationRealm.clients(); ClientRepresentation clientRepresentation = clients.findByClientId("authz-servlet").get(0); @@ -609,6 +641,10 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest { testMigrationTo6_0_0(); } + protected void testMigrationTo8_x() { + testMigrationTo8_0_0(); + } + protected void testMigrationTo7_x(boolean supportedAuthzServices) { if (supportedAuthzServices) { testDecisionStrategySetOnResourceServer(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java index 0ce83fba53..a8410e15b3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport198MigrationTest.java @@ -76,6 +76,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo5_x(); testMigrationTo6_x(); testMigrationTo7_x(false); + testMigrationTo8_x(); } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java index aca9b1983e..134f744b95 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport255MigrationTest.java @@ -69,6 +69,7 @@ public class JsonFileImport255MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo5_x(); testMigrationTo6_x(); testMigrationTo7_x(true); + testMigrationTo8_x(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java index 975db5cfd3..c0a664594b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport343MigrationTest.java @@ -68,6 +68,7 @@ public class JsonFileImport343MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo5_x(); testMigrationTo6_x(); testMigrationTo7_x(true); + testMigrationTo8_x(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java index 8116244f62..f822daf548 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/JsonFileImport483MigrationTest.java @@ -62,6 +62,7 @@ public class JsonFileImport483MigrationTest extends AbstractJsonFileImportMigrat testMigrationTo5_x(); testMigrationTo6_x(); testMigrationTo7_x(true); + testMigrationTo8_x(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java index 38dcfdc1a6..b14f2432bf 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/migration/MigrationTest.java @@ -72,6 +72,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo5_x(); testMigrationTo6_x(); testMigrationTo7_x(true); + testMigrationTo8_x(); } @Test @@ -82,6 +83,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo5_x(); testMigrationTo6_x(); testMigrationTo7_x(true); + testMigrationTo8_x(); } @Test @@ -93,6 +95,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo5_x(); testMigrationTo6_x(); testMigrationTo7_x(true); + testMigrationTo8_x(); } @Test @@ -105,6 +108,7 @@ public class MigrationTest extends AbstractMigrationTest { testMigrationTo5_x(); testMigrationTo6_x(); testMigrationTo7_x(false); + testMigrationTo8_x(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/AbstractHostnameTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/AbstractHostnameTest.java new file mode 100644 index 0000000000..63711dd1f1 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/AbstractHostnameTest.java @@ -0,0 +1,116 @@ +package org.keycloak.testsuite.url; + +import org.jboss.arquillian.container.test.api.ContainerController; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.logging.Logger; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; +import org.wildfly.extras.creaper.core.online.ModelNodeResult; +import org.wildfly.extras.creaper.core.online.OnlineManagementClient; +import org.wildfly.extras.creaper.core.online.operations.admin.Administration; + +public abstract class AbstractHostnameTest extends AbstractKeycloakTest { + + private static final Logger LOGGER = Logger.getLogger(AbstractHostnameTest.class); + + @ArquillianResource + protected ContainerController controller; + + void reset() throws Exception { + LOGGER.info("Reset hostname config to default"); + + if (suiteContext.getAuthServerInfo().isUndertow()) { + controller.stop(suiteContext.getAuthServerInfo().getQualifier()); + removeProperties("keycloak.hostname.provider", + "keycloak.frontendUrl", + "keycloak.adminUrl", + "keycloak.hostname.default.forceBackendUrlToFrontendUrl", + "keycloak.hostname.fixed.hostname", + "keycloak.hostname.fixed.httpPort", + "keycloak.hostname.fixed.httpsPort", + "keycloak.hostname.fixed.alwaysHttps"); + controller.start(suiteContext.getAuthServerInfo().getQualifier()); + } else if (suiteContext.getAuthServerInfo().isJBossBased()) { + executeCli("/subsystem=keycloak-server/spi=hostname:remove", + "/subsystem=keycloak-server/spi=hostname/:add(default-provider=default)", + "/subsystem=keycloak-server/spi=hostname/provider=default/:add(properties={frontendUrl => \"${keycloak.frontendUrl:}\",forceBackendUrlToFrontendUrl => \"false\"},enabled=true)"); + } else { + throw new RuntimeException("Don't know how to config"); + } + + reconnectAdminClient(); + } + + void configureDefault(String frontendUrl, boolean forceBackendUrlToFrontendUrl, String adminUrl) throws Exception { + LOGGER.infov("Configuring default hostname provider: frontendUrl={0}, forceBackendUrlToFrontendUrl={1}, adminUrl={3}", frontendUrl, forceBackendUrlToFrontendUrl, adminUrl); + + if (suiteContext.getAuthServerInfo().isUndertow()) { + controller.stop(suiteContext.getAuthServerInfo().getQualifier()); + System.setProperty("keycloak.hostname.provider", "default"); + System.setProperty("keycloak.frontendUrl", frontendUrl); + if (adminUrl != null){ + System.setProperty("keycloak.adminUrl", adminUrl); + } + System.setProperty("keycloak.hostname.default.forceBackendUrlToFrontendUrl", String.valueOf(forceBackendUrlToFrontendUrl)); + controller.start(suiteContext.getAuthServerInfo().getQualifier()); + } else if (suiteContext.getAuthServerInfo().isJBossBased()) { + executeCli("/subsystem=keycloak-server/spi=hostname:remove", + "/subsystem=keycloak-server/spi=hostname/:add(default-provider=default)", + "/subsystem=keycloak-server/spi=hostname/provider=default/:add(properties={" + + "frontendUrl => \"" + frontendUrl + "\"" + + ",forceBackendUrlToFrontendUrl => \"" + forceBackendUrlToFrontendUrl + "\"" + + (adminUrl != null ? ",adminUrl=\"" + adminUrl + "\"" : "") + "},enabled=true)"); + } else { + throw new RuntimeException("Don't know how to config"); + } + + reconnectAdminClient(); + } + + void configureFixed(String hostname, int httpPort, int httpsPort, boolean alwaysHttps) throws Exception { + + + if (suiteContext.getAuthServerInfo().isUndertow()) { + controller.stop(suiteContext.getAuthServerInfo().getQualifier()); + System.setProperty("keycloak.hostname.provider", "fixed"); + System.setProperty("keycloak.hostname.fixed.hostname", hostname); + System.setProperty("keycloak.hostname.fixed.httpPort", String.valueOf(httpPort)); + System.setProperty("keycloak.hostname.fixed.httpsPort", String.valueOf(httpsPort)); + System.setProperty("keycloak.hostname.fixed.alwaysHttps", String.valueOf(alwaysHttps)); + controller.start(suiteContext.getAuthServerInfo().getQualifier()); + } else if (suiteContext.getAuthServerInfo().isJBossBased()) { + executeCli("/subsystem=keycloak-server/spi=hostname:remove", + "/subsystem=keycloak-server/spi=hostname/:add(default-provider=fixed)", + "/subsystem=keycloak-server/spi=hostname/provider=fixed/:add(properties={hostname => \"" + hostname + "\",httpPort => \"" + httpPort + "\",httpsPort => \"" + httpsPort + "\",alwaysHttps => \"" + alwaysHttps + "\"},enabled=true)"); + + } else { + throw new RuntimeException("Don't know how to config"); + } + + reconnectAdminClient(); + } + + private void executeCli(String... commands) throws Exception { + OnlineManagementClient client = AuthServerTestEnricher.getManagementClient(); + Administration administration = new Administration(client); + + LOGGER.debug("Running CLI commands:"); + for (String c : commands) { + LOGGER.debug(c); + client.execute(c).assertSuccess(); + } + LOGGER.debug("Done"); + + administration.reload(); + + client.close(); + } + + private void removeProperties(String... keys) { + for (String k : keys) { + System.getProperties().remove(k); + } + } + + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/DefaultHostnameTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/DefaultHostnameTest.java new file mode 100644 index 0000000000..e1cdbe3789 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/DefaultHostnameTest.java @@ -0,0 +1,239 @@ +package org.keycloak.testsuite.url; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.jboss.arquillian.container.test.api.ContainerController; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.client.registration.Auth; +import org.keycloak.client.registration.ClientRegistration; +import org.keycloak.client.registration.ClientRegistrationException; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; +import org.keycloak.representations.idm.ClientInitialAccessPresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.ContainerAssume; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.UserBuilder; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT; + +public class DefaultHostnameTest extends AbstractHostnameTest { + + @ArquillianResource + protected ContainerController controller; + + private String expectedBackendUrl; + + private String globalFrontEndUrl = "https://keycloak.127.0.0.1.nip.io/custom"; + + private String realmFrontEndUrl = "https://my-realm.127.0.0.1.nip.io"; + + @Override + public void addTestRealms(List testRealms) { + RealmRepresentation test = RealmBuilder.create().name("test") + .client(ClientBuilder.create().name("direct-grant").clientId("direct-grant").enabled(true).secret("password").directAccessGrants()) + .user(UserBuilder.create().username("test-user@localhost").password("password")) + .build(); + testRealms.add(test); + + RealmRepresentation customHostname = RealmBuilder.create().name("frontendUrl") + .client(ClientBuilder.create().name("direct-grant").clientId("direct-grant").enabled(true).secret("password").directAccessGrants()) + .user(UserBuilder.create().username("test-user@localhost").password("password")) + .attribute("frontendUrl", realmFrontEndUrl) + .build(); + testRealms.add(customHostname); + } + + @BeforeClass + public static void enabled() { + ContainerAssume.assumeNotAuthServerRemote(); + } + + @Test + public void fixedFrontendUrl() throws Exception { + expectedBackendUrl = AUTH_SERVER_ROOT; + + oauth.clientId("direct-grant"); + + try (Keycloak testAdminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), AuthServerTestEnricher.getAuthServerContextRoot())) { + assertWellKnown("test", expectedBackendUrl); + + configureDefault(globalFrontEndUrl, false, null); + + assertWellKnown("test", globalFrontEndUrl); + assertTokenIssuer("test", globalFrontEndUrl); + assertInitialAccessTokenFromMasterRealm(testAdminClient,"test", globalFrontEndUrl); + assertBackendForcedToFrontendWithMatchingHostname("test", globalFrontEndUrl); + + assertWelcomePage(globalFrontEndUrl); + assertAdminPage("master", globalFrontEndUrl, globalFrontEndUrl); + + assertWellKnown("frontendUrl", realmFrontEndUrl); + assertTokenIssuer("frontendUrl", realmFrontEndUrl); + assertInitialAccessTokenFromMasterRealm(testAdminClient,"frontendUrl", realmFrontEndUrl); + assertBackendForcedToFrontendWithMatchingHostname("frontendUrl", realmFrontEndUrl); + + assertAdminPage("frontendUrl", realmFrontEndUrl, realmFrontEndUrl); + } finally { + reset(); + } + } + + @Test + public void fixedAdminUrl() throws Exception { + expectedBackendUrl = AUTH_SERVER_ROOT; + String adminUrl = "https://admin.127.0.0.1.nip.io/custom-admin"; + + oauth.clientId("direct-grant"); + + try { + assertWellKnown("test", expectedBackendUrl); + + configureDefault(globalFrontEndUrl, false, adminUrl); + + assertWelcomePage(adminUrl); + + assertAdminPage("master", globalFrontEndUrl, adminUrl); + assertAdminPage("frontendUrl", realmFrontEndUrl, adminUrl); + } finally { + reset(); + } + } + + @Test + public void forceBackendUrlToFrontendUrl() throws Exception { + expectedBackendUrl = AUTH_SERVER_ROOT; + + oauth.clientId("direct-grant"); + + try (Keycloak testAdminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), AuthServerTestEnricher.getAuthServerContextRoot())) { + assertWellKnown("test", expectedBackendUrl); + + configureDefault(globalFrontEndUrl, true, null); + + expectedBackendUrl = globalFrontEndUrl; + + assertWellKnown("test", globalFrontEndUrl); + assertTokenIssuer("test", globalFrontEndUrl); + assertInitialAccessTokenFromMasterRealm(testAdminClient,"test", globalFrontEndUrl); + + expectedBackendUrl = realmFrontEndUrl; + + assertWellKnown("frontendUrl", realmFrontEndUrl); + assertTokenIssuer("frontendUrl", realmFrontEndUrl); + assertInitialAccessTokenFromMasterRealm(testAdminClient,"frontendUrl", realmFrontEndUrl); + } finally { + reset(); + } + } + + private void assertInitialAccessTokenFromMasterRealm(Keycloak testAdminClient, String realm, String expectedBaseUrl) throws JWSInputException, ClientRegistrationException { + ClientInitialAccessCreatePresentation rep = new ClientInitialAccessCreatePresentation(); + rep.setCount(1); + rep.setExpiration(10000); + + ClientInitialAccessPresentation initialAccess = testAdminClient.realm(realm).clientInitialAccess().create(rep); + JsonWebToken token = new JWSInput(initialAccess.getToken()).readJsonContent(JsonWebToken.class); + assertEquals(expectedBaseUrl + "/realms/" + realm, token.getIssuer()); + + ClientRegistration clientReg = ClientRegistration.create().url(AUTH_SERVER_ROOT, realm).build(); + clientReg.auth(Auth.token(initialAccess.getToken())); + + ClientRepresentation client = new ClientRepresentation(); + client.setEnabled(true); + ClientRepresentation response = clientReg.create(client); + + String registrationAccessToken = response.getRegistrationAccessToken(); + JsonWebToken registrationToken = new JWSInput(registrationAccessToken).readJsonContent(JsonWebToken.class); + assertEquals(expectedBaseUrl + "/realms/" + realm, registrationToken.getIssuer()); + } + + private void assertTokenIssuer(String realm, String expectedBaseUrl) throws Exception { + oauth.realm(realm); + + OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password"); + + AccessToken token = new JWSInput(tokenResponse.getAccessToken()).readJsonContent(AccessToken.class); + assertEquals(expectedBaseUrl + "/realms/" + realm, token.getIssuer()); + + String introspection = oauth.introspectAccessTokenWithClientCredential(oauth.getClientId(), "password", tokenResponse.getAccessToken()); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode introspectionNode = objectMapper.readTree(introspection); + assertTrue(introspectionNode.get("active").asBoolean()); + assertEquals(expectedBaseUrl + "/realms/" + realm, introspectionNode.get("iss").asText()); + } + + private void assertWellKnown(String realm, String expectedFrontendUrl) throws URISyntaxException { + OIDCConfigurationRepresentation config = oauth.doWellKnownRequest(realm); + assertEquals(expectedFrontendUrl + "/realms/" + realm, config.getIssuer()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/protocol/openid-connect/auth", config.getAuthorizationEndpoint()); + assertEquals(expectedBackendUrl + "/realms/" + realm + "/protocol/openid-connect/token", config.getTokenEndpoint()); + assertEquals(expectedBackendUrl + "/realms/" + realm + "/protocol/openid-connect/userinfo", config.getUserinfoEndpoint()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/protocol/openid-connect/logout", config.getLogoutEndpoint()); + assertEquals(expectedBackendUrl + "/realms/" + realm + "/protocol/openid-connect/certs", config.getJwksUri()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/protocol/openid-connect/login-status-iframe.html", config.getCheckSessionIframe()); + assertEquals(expectedBackendUrl + "/realms/" + realm + "/clients-registrations/openid-connect", config.getRegistrationEndpoint()); + } + + // Test backend is forced to frontend if the request hostname matches the frontend + private void assertBackendForcedToFrontendWithMatchingHostname(String realm, String expectedFrontendUrl) throws URISyntaxException { + String host = new URI(expectedFrontendUrl).getHost(); + + // Scheme and port doesn't matter as we force based on hostname only, so using http and bind port as we can't make requests on configured frontend URL since reverse proxy is not available + oauth.baseUrl("http://" + host + ":" + System.getProperty("auth.server.http.port") + "/auth"); + + OIDCConfigurationRepresentation config = oauth.doWellKnownRequest(realm); + + assertEquals(expectedFrontendUrl + "/realms/" + realm, config.getIssuer()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/protocol/openid-connect/auth", config.getAuthorizationEndpoint()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/protocol/openid-connect/token", config.getTokenEndpoint()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/protocol/openid-connect/userinfo", config.getUserinfoEndpoint()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/protocol/openid-connect/logout", config.getLogoutEndpoint()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/protocol/openid-connect/certs", config.getJwksUri()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/protocol/openid-connect/login-status-iframe.html", config.getCheckSessionIframe()); + assertEquals(expectedFrontendUrl + "/realms/" + realm + "/clients-registrations/openid-connect", config.getRegistrationEndpoint()); + + oauth.baseUrl(AUTH_SERVER_ROOT); + } + + private void assertWelcomePage(String expectedAdminUrl) throws IOException { + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + String welcomePage = SimpleHttp.doGet(AUTH_SERVER_ROOT + "/", client).asString(); + assertTrue(welcomePage.contains("")); + } + } + + private void assertAdminPage(String realm, String expectedFrontendUrl, String expectedAdminUrl) throws IOException, URISyntaxException { + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + String indexPage = SimpleHttp.doGet(AUTH_SERVER_ROOT + "/admin/" + realm +"/console/", client).asString(); + + assertTrue(indexPage.contains("authServerUrl = '" + expectedFrontendUrl +"'")); + assertTrue(indexPage.contains("authUrl = '" + expectedAdminUrl +"'")); + assertTrue(indexPage.contains("consoleBaseUrl = '" + new URI(expectedAdminUrl).getPath() +"/admin/" + realm + "/console/'")); + assertTrue(indexPage.contains("resourceUrl = '" + new URI(expectedAdminUrl).getPath() +"/resources/")); + } + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/FixedHostnameTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/FixedHostnameTest.java index b922646e8b..ae0066727f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/FixedHostnameTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/url/FixedHostnameTest.java @@ -2,56 +2,84 @@ package org.keycloak.testsuite.url; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.jboss.arquillian.container.test.api.ContainerController; -import org.jboss.arquillian.test.api.ArquillianResource; import org.junit.BeforeClass; import org.junit.Test; import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; import org.keycloak.client.registration.Auth; import org.keycloak.client.registration.ClientRegistration; import org.keycloak.client.registration.ClientRegistrationException; +import org.keycloak.dom.saml.v2.metadata.EndpointType; +import org.keycloak.dom.saml.v2.metadata.EntitiesDescriptorType; +import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType; +import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType; +import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; +import org.keycloak.protocol.saml.SamlConfigAttributes; +import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.representations.AccessToken; import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.saml.common.constants.GeneralConstants; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; +import org.keycloak.saml.processing.core.parsers.saml.SAMLParser; +import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; +import org.keycloak.testsuite.updaters.Creator; import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ContainerAssume; import org.keycloak.testsuite.util.OAuthClient; -import org.wildfly.extras.creaper.core.online.OnlineManagementClient; -import org.wildfly.extras.creaper.core.online.operations.admin.Administration; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClientBuilder; +import org.keycloak.testsuite.util.UserBuilder; -import java.util.HashMap; +import java.io.ByteArrayInputStream; +import java.net.URI; import java.util.List; +import java.util.stream.Collectors; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Matchers; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; -public class FixedHostnameTest extends AbstractKeycloakTest { +public class FixedHostnameTest extends AbstractHostnameTest { - @ArquillianResource - protected ContainerController controller; + public static final String SAML_CLIENT_ID = "http://whatever.hostname:8280/app/"; private String authServerUrl; @Override public void addTestRealms(List testRealms) { - RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); - testRealms.add(realm); - - RealmRepresentation customHostname = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); - customHostname.setId("hostname"); - customHostname.setRealm("hostname"); - customHostname.setAttributes(new HashMap<>()); - customHostname.getAttributes().put("hostname", "custom-domain.127.0.0.1.nip.io"); + RealmRepresentation test = RealmBuilder.create().name("test") + .client(ClientBuilder.create().name("direct-grant").clientId("direct-grant").enabled(true).secret("password").directAccessGrants()) + .user(UserBuilder.create().username("test-user@localhost").password("password")) + .build(); + testRealms.add(test); + RealmRepresentation customHostname = RealmBuilder.create().name("hostname") + .client(ClientBuilder.create().name("direct-grant").clientId("direct-grant").enabled(true).secret("password").directAccessGrants()) + .user(UserBuilder.create().username("test-user@localhost").password("password")) + .attribute("hostname", "custom-domain.127.0.0.1.nip.io") + .build(); testRealms.add(customHostname); } @@ -69,19 +97,24 @@ public class FixedHostnameTest extends AbstractKeycloakTest { try (Keycloak testAdminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), AuthServerTestEnricher.getAuthServerContextRoot())) { assertWellKnown("test", AUTH_SERVER_SCHEME + "://localhost:" + AUTH_SERVER_PORT); + assertSamlIdPDescriptor("test", AUTH_SERVER_SCHEME + "://localhost:" + AUTH_SERVER_PORT); - configureFixedHostname(-1, -1, false); + configureFixed("keycloak.127.0.0.1.nip.io", -1, -1, false); assertWellKnown("test", AUTH_SERVER_SCHEME + "://keycloak.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); + assertSamlIdPDescriptor("test", AUTH_SERVER_SCHEME + "://keycloak.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); assertWellKnown("hostname", AUTH_SERVER_SCHEME + "://custom-domain.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); + assertSamlIdPDescriptor("hostname", AUTH_SERVER_SCHEME + "://custom-domain.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); assertTokenIssuer("test", AUTH_SERVER_SCHEME + "://keycloak.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); assertTokenIssuer("hostname", AUTH_SERVER_SCHEME + "://custom-domain.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); assertInitialAccessTokenFromMasterRealm(testAdminClient,"test", AUTH_SERVER_SCHEME + "://keycloak.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); + assertSamlLogin(testAdminClient,"test", AUTH_SERVER_SCHEME + "://keycloak.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); assertInitialAccessTokenFromMasterRealm(testAdminClient,"hostname", AUTH_SERVER_SCHEME + "://custom-domain.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); + assertSamlLogin(testAdminClient,"hostname", AUTH_SERVER_SCHEME + "://custom-domain.127.0.0.1.nip.io:" + AUTH_SERVER_PORT); } finally { - clearFixedHostname(); + reset(); } } @@ -95,19 +128,24 @@ public class FixedHostnameTest extends AbstractKeycloakTest { try (Keycloak testAdminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), "http://localhost:8180")) { assertWellKnown("test", "http://localhost:8180"); + assertSamlIdPDescriptor("test", "http://localhost:8180"); - configureFixedHostname(80, -1, false); + configureFixed("keycloak.127.0.0.1.nip.io", 80, -1, false); assertWellKnown("test", "http://keycloak.127.0.0.1.nip.io"); + assertSamlIdPDescriptor("test", "http://keycloak.127.0.0.1.nip.io"); assertWellKnown("hostname", "http://custom-domain.127.0.0.1.nip.io"); + assertSamlIdPDescriptor("hostname", "http://custom-domain.127.0.0.1.nip.io"); assertTokenIssuer("test", "http://keycloak.127.0.0.1.nip.io"); assertTokenIssuer("hostname", "http://custom-domain.127.0.0.1.nip.io"); assertInitialAccessTokenFromMasterRealm(testAdminClient,"test", "http://keycloak.127.0.0.1.nip.io"); + assertSamlLogin(testAdminClient,"test", "http://keycloak.127.0.0.1.nip.io"); assertInitialAccessTokenFromMasterRealm(testAdminClient,"hostname", "http://custom-domain.127.0.0.1.nip.io"); + assertSamlLogin(testAdminClient,"hostname", "http://custom-domain.127.0.0.1.nip.io"); } finally { - clearFixedHostname(); + reset(); } } @@ -121,19 +159,24 @@ public class FixedHostnameTest extends AbstractKeycloakTest { try (Keycloak testAdminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), "http://localhost:8180")) { assertWellKnown("test", "http://localhost:8180"); + assertSamlIdPDescriptor("test", "http://localhost:8180"); - configureFixedHostname(-1, 443, true); + configureFixed("keycloak.127.0.0.1.nip.io", -1, 443, true); assertWellKnown("test", "https://keycloak.127.0.0.1.nip.io"); + assertSamlIdPDescriptor("test", "https://keycloak.127.0.0.1.nip.io"); assertWellKnown("hostname", "https://custom-domain.127.0.0.1.nip.io"); + assertSamlIdPDescriptor("hostname", "https://custom-domain.127.0.0.1.nip.io"); assertTokenIssuer("test", "https://keycloak.127.0.0.1.nip.io"); assertTokenIssuer("hostname", "https://custom-domain.127.0.0.1.nip.io"); assertInitialAccessTokenFromMasterRealm(testAdminClient, "test", "https://keycloak.127.0.0.1.nip.io"); + assertSamlLogin(testAdminClient, "test", "https://keycloak.127.0.0.1.nip.io"); assertInitialAccessTokenFromMasterRealm(testAdminClient, "hostname", "https://custom-domain.127.0.0.1.nip.io"); + assertSamlLogin(testAdminClient, "hostname", "https://custom-domain.127.0.0.1.nip.io"); } finally { - clearFixedHostname(); + reset(); } } @@ -178,56 +221,63 @@ public class FixedHostnameTest extends AbstractKeycloakTest { assertEquals(expectedBaseUrl + "/auth/realms/" + realm + "/protocol/openid-connect/token", config.getTokenEndpoint()); } - private void configureFixedHostname(int httpPort, int httpsPort, boolean alwaysHttps) throws Exception { - if (suiteContext.getAuthServerInfo().isUndertow()) { - configureUndertow("fixed", "keycloak.127.0.0.1.nip.io", httpPort, httpsPort, alwaysHttps); - } else if (suiteContext.getAuthServerInfo().isJBossBased()) { - configureWildFly("fixed", "keycloak.127.0.0.1.nip.io", httpPort, httpsPort, alwaysHttps); - } else { - throw new RuntimeException("Don't know how to config"); + private void assertSamlIdPDescriptor(String realm, String expectedBaseUrl) throws Exception { + final String realmUrl = expectedBaseUrl + "/auth/realms/" + realm; + final String baseSamlEndpointUrl = realmUrl + "/protocol/saml"; + String entityDescriptor = null; + try ( + CloseableHttpClient client = HttpClientBuilder.create().build(); + CloseableHttpResponse resp = client.execute(new HttpGet(baseSamlEndpointUrl + "/descriptor")) + ) { + entityDescriptor = EntityUtils.toString(resp.getEntity(), GeneralConstants.SAML_CHARSET); + Object metadataO = SAMLParser.getInstance().parse(new ByteArrayInputStream(entityDescriptor.getBytes(GeneralConstants.SAML_CHARSET))); + assertThat(metadataO, instanceOf(EntitiesDescriptorType.class)); + EntitiesDescriptorType metadata = (EntitiesDescriptorType) metadataO; + + assertThat(metadata.getEntityDescriptor(), hasSize(1)); + assertThat(metadata.getEntityDescriptor().get(0), instanceOf(EntityDescriptorType.class)); + EntityDescriptorType ed = (EntityDescriptorType) metadata.getEntityDescriptor().get(0); + assertThat(ed.getEntityID(), is(realmUrl)); + + IDPSSODescriptorType idpDescriptor = ed.getChoiceType().get(0).getDescriptors().get(0).getIdpDescriptor(); + assertThat(idpDescriptor, notNullValue()); + final List locations = idpDescriptor.getSingleSignOnService().stream() + .map(EndpointType::getLocation) + .map(URI::toString) + .collect(Collectors.toList()); + assertThat(locations, Matchers.everyItem(is(baseSamlEndpointUrl))); + } catch (Exception e) { + log.errorf("Caught exception while parsing SAML descriptor %s", entityDescriptor); } - - reconnectAdminClient(); - } - private void clearFixedHostname() throws Exception { - if (suiteContext.getAuthServerInfo().isUndertow()) { - configureUndertow("request", "localhost", -1, -1,false); - } else if (suiteContext.getAuthServerInfo().isJBossBased()) { - configureWildFly("request", "localhost", -1, -1, false); - } else { - throw new RuntimeException("Don't know how to config"); + private void assertSamlLogin(Keycloak testAdminClient, String realm, String expectedBaseUrl) throws Exception { + final String realmUrl = expectedBaseUrl + "/auth/realms/" + realm; + final String baseSamlEndpointUrl = realmUrl + "/protocol/saml"; + String entityDescriptor = null; + RealmResource realmResource = testAdminClient.realm(realm); + ClientRepresentation clientRep = ClientBuilder.create() + .protocol(SamlProtocol.LOGIN_PROTOCOL) + .clientId(SAML_CLIENT_ID) + .enabled(true) + .attribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false") + .redirectUris("http://foo.bar/") + .build(); + try (Creator c = Creator.create(realmResource, clientRep); + Creator u = Creator.create(realmResource, UserBuilder.create().username("bicycle").password("race").enabled(true).build())) { + SAMLDocumentHolder samlResponse = new SamlClientBuilder() + .authnRequest(new URI(baseSamlEndpointUrl), SAML_CLIENT_ID, "http://foo.bar/", Binding.POST).build() + .login().user("bicycle", "race").build() + .getSamlResponse(Binding.POST); + + assertThat(samlResponse.getSamlObject(), org.keycloak.testsuite.util.Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + ResponseType response = (ResponseType) samlResponse.getSamlObject(); + + assertThat(response.getAssertions(), hasSize(1)); + assertThat(response.getAssertions().get(0).getAssertion().getIssuer().getValue(), is(realmUrl)); + } catch (Exception e) { + log.errorf("Caught exception while parsing SAML descriptor %s", entityDescriptor); } - - reconnectAdminClient(); - } - - private void configureUndertow(String provider, String hostname, int httpPort, int httpsPort, boolean alwaysHttps) { - controller.stop(suiteContext.getAuthServerInfo().getQualifier()); - - System.setProperty("keycloak.hostname.provider", provider); - System.setProperty("keycloak.hostname.fixed.hostname", hostname); - System.setProperty("keycloak.hostname.fixed.httpPort", String.valueOf(httpPort)); - System.setProperty("keycloak.hostname.fixed.httpsPort", String.valueOf(httpsPort)); - System.setProperty("keycloak.hostname.fixed.alwaysHttps", String.valueOf(alwaysHttps)); - - controller.start(suiteContext.getAuthServerInfo().getQualifier()); - } - - private void configureWildFly(String provider, String hostname, int httpPort, int httpsPort, boolean alwaysHttps) throws Exception { - OnlineManagementClient client = AuthServerTestEnricher.getManagementClient(); - Administration administration = new Administration(client); - - client.execute("/subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value=" + provider + ")"); - client.execute("/subsystem=keycloak-server/spi=hostname/provider=fixed:write-attribute(name=properties.hostname,value=" + hostname + ")"); - client.execute("/subsystem=keycloak-server/spi=hostname/provider=fixed:write-attribute(name=properties.httpPort,value=" + httpPort + ")"); - client.execute("/subsystem=keycloak-server/spi=hostname/provider=fixed:write-attribute(name=properties.httpsPort,value=" + httpsPort + ")"); - client.execute("/subsystem=keycloak-server/spi=hostname/provider=fixed:write-attribute(name=properties.alwaysHttps,value=" + alwaysHttps + ")"); - - administration.reloadIfRequired(); - - client.close(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java index cb5a0cf7bc..0e1c6071a3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java @@ -81,6 +81,14 @@ public class RealmBuilder { return this; } + public RealmBuilder attribute(String key, String value) { + if (rep.getAttributes() == null) { + rep.setAttributes(new HashMap<>()); + } + rep.getAttributes().put(key, value); + return this; + } + public RealmBuilder testMail() { Map config = new HashMap<>(); config.put("from", MailServerConfiguration.FROM); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 57105221ef..ac03b0aed5 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -1,13 +1,19 @@ { "hostname": { - "provider": "${keycloak.hostname.provider:request}", + "provider": "${keycloak.hostname.provider:default}", "fixed": { "hostname": "${keycloak.hostname.fixed.hostname:localhost}", "httpPort": "${keycloak.hostname.fixed.httpPort:-1}", "httpsPort": "${keycloak.hostname.fixed.httpsPort:-1}", "alwaysHttps": "${keycloak.hostname.fixed.alwaysHttps:false}" + }, + + "default": { + "frontendUrl": "${keycloak.frontendUrl:}", + "adminUrl": "${keycloak.adminUrl:}", + "forceBackendUrlToFrontendUrl": "${keycloak.hostname.default.forceBackendUrlToFrontendUrl:false}" } }, diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java index 01a9f87f9e..b7f72ad73d 100755 --- a/testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java +++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java @@ -346,7 +346,6 @@ public class KeycloakServer { info("Not importing realm " + rep.getRealm() + " realm already exists"); return; } - manager.setContextPath("/auth"); RealmModel realm = manager.importRealm(rep); info("Imported realm " + realm.getName()); diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index 13f9b5764c..13e9a90480 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -1,12 +1,12 @@ { "hostname": { - "provider": "request", + "provider": "${keycloak.hostname.provider:default}", - "fixed": { - "hostname": "localhost", - "httpPort": "-1", - "httpsPort": "-1" + "default": { + "frontendUrl": "${keycloak.frontendUrl:}", + "adminUrl": "${keycloak.adminUrl:}", + "forceBackendUrlToFrontendUrl": "${keycloak.hostname.default.forceBackendUrlToFrontendUrl:false}" } }, diff --git a/themes/src/main/resources/theme/base/admin/index.ftl b/themes/src/main/resources/theme/base/admin/index.ftl index c5c0e84efd..b54cebf496 100755 --- a/themes/src/main/resources/theme/base/admin/index.ftl +++ b/themes/src/main/resources/theme/base/admin/index.ftl @@ -16,6 +16,7 @@