diff --git a/docbook/auth-server-docs/reference/en/en-US/master.xml b/docbook/auth-server-docs/reference/en/en-US/master.xml index 7693aee922..a44b126597 100755 --- a/docbook/auth-server-docs/reference/en/en-US/master.xml +++ b/docbook/auth-server-docs/reference/en/en-US/master.xml @@ -42,6 +42,7 @@ + @@ -140,6 +141,7 @@ This one is short &IdentityBroker; &Themes; + &Clients; &Recaptcha; diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/clients.xml b/docbook/auth-server-docs/reference/en/en-US/modules/clients.xml new file mode 100644 index 0000000000..2d15ad062b --- /dev/null +++ b/docbook/auth-server-docs/reference/en/en-US/modules/clients.xml @@ -0,0 +1,57 @@ + + + + Clients + + + Keycloak provides support for managing OAuth clients. + + +
+ Client Config + + Keycloak supports flexible configuration of OAuth Clients. + + +
+ Redirect Endpoint + + For scenarios where one wants to link from one client to another, Keycloak provides a special redirect endpoint: + /realms/realm_name/clients/client_id/redirect. + + + + If a client accesses this endpoint via an HTTP GET request, Keycloak returns the configured base URL + for the provided Client and Realm in the form of an HTTP 307 (Temporary Redirect) via the response's Location header. + + + + Thus, a client only needs to know the Realm name and the Client ID in order to link to them. + This indirection helps avoid hard-coding client base URLs. + + + + As an example, given the realm master and the client-id account: + http://keycloak-host:keycloak-port/auth/realms/master/clients/account/redirect + + Would temporarily redirect to: + http://keycloak-host:keycloak-port/auth/realms/master/account + +
+
+
\ No newline at end of file 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 f8c800cfcc..a47f2ab922 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -20,6 +20,7 @@ import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.common.ClientConnection; +import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; @@ -27,21 +28,20 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocolFactory; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.services.ServicesLogger; import org.keycloak.services.clientregistration.ClientRegistrationService; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.util.CacheControlUtil; +import org.keycloak.services.util.ResolveRelative; import org.keycloak.wellknown.WellKnownProvider; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.*; import javax.ws.rs.core.Response.ResponseBuilder; +import java.net.URI; /** * @author Bill Burke @@ -60,6 +60,9 @@ public class RealmsResource { @Context private HttpRequest request; + @Context + private UriInfo uriInfo; + public static UriBuilder realmBaseUrl(UriInfo uriInfo) { UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); return realmBaseUrl(baseUriBuilder); @@ -103,6 +106,44 @@ public class RealmsResource { return endpoint; } + /** + * Returns a temporary redirect to the client url configured for the given {@code clientId} in the given {@code realmName}. + *

+ * This allows a client to refer to other clients just by their client id in URLs, will then redirect users to the actual client url. + * The client url is derived according to the rules of the base url in the client configuration. + *

+ * + * @param realmName + * @param clientId + * @return + * @since 1.9 + */ + @GET + @Path("{realm}/clients/{client_id}/redirect") + public Response getRedirect(final @PathParam("realm") String realmName, final @PathParam("client_id") String clientId) { + + RealmModel realm = init(realmName); + + if (realm == null) { + return null; + } + + ClientModel client = realm.getClientByClientId(clientId); + + if (client == null) { + return null; + } + + if (client.getRootUrl() == null) { + + URI targetUri = KeycloakUriBuilder.fromUri(ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), client.getRootUrl(), client.getBaseUrl())).build(); + + return Response.temporaryRedirect(targetUri).build(); + } + + return Response.temporaryRedirect(URI.create(client.getRootUrl() + client.getBaseUrl())).build(); + } + @Path("{realm}/login-actions") public LoginActionsService getLoginActionsService(final @PathParam("realm") String name) { RealmModel realm = init(name); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/RealmResourceTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/RealmResourceTest.java new file mode 100644 index 0000000000..caa293016c --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/RealmResourceTest.java @@ -0,0 +1,79 @@ +package org.keycloak.testsuite; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.openqa.selenium.WebDriver; + +import static org.junit.Assert.assertEquals; + +/** + * @author Thomas Darimont + */ +public class RealmResourceTest { + + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + + RealmModel testRealm = manager.getRealmByName("test"); + + ClientModel launchpadClient = testRealm.addClient("launchpad-test"); + launchpadClient.setBaseUrl(""); + launchpadClient.setRootUrl("http://example.org/launchpad"); + + ClientModel dummyClient = testRealm.addClient("dummy-test"); + dummyClient.setRootUrl("http://example.org/dummy"); + dummyClient.setBaseUrl("/base-path"); + } + }); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected WebDriver webDriver; + + private static int getKeycloakPort() { + + String keycloakPort = System.getProperty("keycloak.port", System.getenv("KEYCLOAK_DEV_PORT")); + + try { + return Integer.parseInt(keycloakPort); + } catch (Exception ex) { + return 8081; + } + } + + /** + * Integration test for {@link org.keycloak.services.resources.RealmsResource#getRedirect(String, String)}. + * + * @throws Exception + */ + @Test + public void testClientRedirectEndpoint() throws Exception { + + oauth.doLogin("test-user@localhost", "password"); + + webDriver.get("http://localhost:" + getKeycloakPort() + "/auth/realms/test/clients/launchpad-test/redirect"); + assertEquals("http://example.org/launchpad", webDriver.getCurrentUrl()); + + webDriver.get("http://localhost:" + getKeycloakPort() + "/auth/realms/test/clients/dummy-test/redirect"); + assertEquals("http://example.org/dummy/base-path", webDriver.getCurrentUrl()); + + webDriver.get("http://localhost:" + getKeycloakPort() + "/auth/realms/test/clients/account/redirect"); + assertEquals("http://localhost:" + getKeycloakPort() + "/auth/realms/test/account", webDriver.getCurrentUrl()); + } +}