Merge pull request #2399 from stianst/KEYCLOAK-2469

Keycloak 2469
This commit is contained in:
Stian Thorgersen 2016-03-22 08:09:59 +01:00
commit 2c9172f707
4 changed files with 187 additions and 3 deletions

View file

@ -42,6 +42,7 @@
<!ENTITY JAAS SYSTEM "modules/jaas.xml"> <!ENTITY JAAS SYSTEM "modules/jaas.xml">
<!ENTITY IdentityBroker SYSTEM "modules/identity-broker.xml"> <!ENTITY IdentityBroker SYSTEM "modules/identity-broker.xml">
<!ENTITY Themes SYSTEM "modules/themes.xml"> <!ENTITY Themes SYSTEM "modules/themes.xml">
<!ENTITY Clients SYSTEM "modules/clients.xml">
<!ENTITY Migration SYSTEM "modules/MigrationFromOlderVersions.xml"> <!ENTITY Migration SYSTEM "modules/MigrationFromOlderVersions.xml">
<!ENTITY Email SYSTEM "modules/email.xml"> <!ENTITY Email SYSTEM "modules/email.xml">
<!ENTITY Roles SYSTEM "modules/roles.xml"> <!ENTITY Roles SYSTEM "modules/roles.xml">
@ -140,6 +141,7 @@ This one is short
&IdentityBroker; &IdentityBroker;
&Themes; &Themes;
&Clients;
&Recaptcha; &Recaptcha;
<chapter> <chapter>

View file

@ -0,0 +1,57 @@
<!--
~ Copyright 2016 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.
-->
<chapter id="clients">
<title>Clients</title>
<para>
Keycloak provides support for managing Clients.
</para>
<section id="client-config">
<title>Client Config</title>
<para>
Keycloak supports flexible configuration of Clients.
</para>
<section>
<title>Redirect Endpoint</title>
<para>
For scenarios where one wants to link from one client to another, Keycloak provides a special redirect endpoint:
<literal>/realms/realm_name/clients/client_id/redirect</literal>.
</para>
<para>
If a client accesses this endpoint via an <literal>HTTP GET</literal> request, Keycloak returns the configured base URL
for the provided Client and Realm in the form of an <literal>HTTP 307</literal> (Temporary Redirect) via the response's <literal>Location</literal> header.
</para>
<para>
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.
</para>
<para>
As an example, given the realm <literal>master</literal> and the client-id <literal>account</literal>:
<programlisting>http://keycloak-host:keycloak-port/auth/realms/master/clients/account/redirect</programlisting>
Would temporarily redirect to:
<programlisting>http://keycloak-host:keycloak-port/auth/realms/master/account</programlisting>
</para>
</section>
</section>
</chapter>

View file

@ -20,6 +20,7 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
@ -27,21 +28,20 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory; 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.ServicesLogger;
import org.keycloak.services.clientregistration.ClientRegistrationService; import org.keycloak.services.clientregistration.ClientRegistrationService;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.wellknown.WellKnownProvider; import org.keycloak.wellknown.WellKnownProvider;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.*; import javax.ws.rs.core.*;
import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.Response.ResponseBuilder;
import java.net.URI;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -60,6 +60,9 @@ public class RealmsResource {
@Context @Context
private HttpRequest request; private HttpRequest request;
@Context
private UriInfo uriInfo;
public static UriBuilder realmBaseUrl(UriInfo uriInfo) { public static UriBuilder realmBaseUrl(UriInfo uriInfo) {
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder(); UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return realmBaseUrl(baseUriBuilder); return realmBaseUrl(baseUriBuilder);
@ -103,6 +106,49 @@ public class RealmsResource {
return endpoint; return endpoint;
} }
/**
* Returns a temporary redirect to the client url configured for the given {@code clientId} in the given {@code realmName}.
* <p>
* 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.
* </p>
*
* @param realmName
* @param clientId
* @return
* @since 2.0
*/
@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 && client.getBaseUrl() == null) {
return null;
}
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") @Path("{realm}/login-actions")
public LoginActionsService getLoginActionsService(final @PathParam("realm") String name) { public LoginActionsService getLoginActionsService(final @PathParam("realm") String name) {
RealmModel realm = init(name); RealmModel realm = init(name);

View file

@ -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 <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
public class ClientRedirectTest {
@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());
}
}