From 41358eea4ddcc9939979fac988c9bbb2012e9105 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Mon, 8 Feb 2016 13:17:22 +0100 Subject: [PATCH 1/2] KEYCLOAK-2469 - Introduced new redirect endpoint for clients. Previously one had to configure hardcoded urls to link from one client application to others since keycloak didn't provide a way to get the actual client URL by providing clientId and realm information. We now support a new endpoint with the path {realm}/clients/{client_id}/redirect that responds to GET requests with a 307 (temporary redirect) with the configured client URL. This allows to refer to any client just by the realmName and clientId and let Keycloak redirect to the actual client application. Add documentation for new redirect endpoint. --- .../reference/en/en-US/master.xml | 2 + .../reference/en/en-US/modules/clients.xml | 57 +++++++++++++ .../services/resources/RealmsResource.java | 47 ++++++++++- .../keycloak/testsuite/RealmResourceTest.java | 79 +++++++++++++++++++ 4 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 docbook/auth-server-docs/reference/en/en-US/modules/clients.xml create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/RealmResourceTest.java 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()); + } +} From ce717fa20e1283cbdc7ec7cb7540eb0a935a55c5 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Mon, 21 Mar 2016 15:45:53 +0100 Subject: [PATCH 2/2] KEYCLOAK-2469 Minor changes --- .../reference/en/en-US/modules/clients.xml | 4 ++-- .../services/resources/RealmsResource.java | 19 ++++++++++++------- ...ourceTest.java => ClientRedirectTest.java} | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) rename testsuite/integration/src/test/java/org/keycloak/testsuite/{RealmResourceTest.java => ClientRedirectTest.java} (98%) 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 index 2d15ad062b..7be201971c 100644 --- a/docbook/auth-server-docs/reference/en/en-US/modules/clients.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/clients.xml @@ -19,13 +19,13 @@ Clients - Keycloak provides support for managing OAuth clients. + Keycloak provides support for managing Clients.
Client Config - Keycloak supports flexible configuration of OAuth Clients. + Keycloak supports flexible configuration of Clients.
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 a47f2ab922..43916e7530 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -116,7 +116,7 @@ public class RealmsResource { * @param realmName * @param clientId * @return - * @since 1.9 + * @since 2.0 */ @GET @Path("{realm}/clients/{client_id}/redirect") @@ -134,14 +134,19 @@ public class RealmsResource { return null; } - if (client.getRootUrl() == null) { - - URI targetUri = KeycloakUriBuilder.fromUri(ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), client.getRootUrl(), client.getBaseUrl())).build(); - - return Response.temporaryRedirect(targetUri).build(); + if (client.getRootUrl() == null && client.getBaseUrl() == null) { + return null; } - return Response.temporaryRedirect(URI.create(client.getRootUrl() + client.getBaseUrl())).build(); + + URI targetUri; + if (client.getRootUrl() != null && (client.getBaseUrl() == null || client.getBaseUrl().isEmpty())) { + targetUri = KeycloakUriBuilder.fromUri(client.getRootUrl()).build(); + } else { + targetUri = KeycloakUriBuilder.fromUri(ResolveRelative.resolveRelativeUri(uriInfo.getRequestUri(), client.getRootUrl(), client.getBaseUrl())).build(); + } + + return Response.temporaryRedirect(targetUri).build(); } @Path("{realm}/login-actions") diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/RealmResourceTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/ClientRedirectTest.java similarity index 98% rename from testsuite/integration/src/test/java/org/keycloak/testsuite/RealmResourceTest.java rename to testsuite/integration/src/test/java/org/keycloak/testsuite/ClientRedirectTest.java index caa293016c..3c75cf6126 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/RealmResourceTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/ClientRedirectTest.java @@ -17,7 +17,7 @@ import static org.junit.Assert.assertEquals; /** * @author Thomas Darimont */ -public class RealmResourceTest { +public class ClientRedirectTest { @ClassRule public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() {