KEYCLOAK-7967 Introduce Hostname SPI

This commit is contained in:
stianst 2018-07-30 22:38:43 +02:00 committed by Marek Posolda
parent ae47b7fa80
commit f99299ee39
30 changed files with 704 additions and 27 deletions

View file

@ -439,4 +439,12 @@ if (outcome == failed) of /profile=$clusteredProfile/subsystem=keycloak-server/s
echo
end-if
# Migrate from 4.2.0 to 4.3.0
if (outcome == failed) of /subsystem=keycloak-server/spi=hostname/:read-resource
echo Adding spi=hostname...
/subsystem=keycloak-server/spi=hostname/:add(default-provider=request)
/subsystem=keycloak-server/spi=hostname/provider=fixed/:add(properties={hostname => "localhost",httpPort => "-1",httpsPort => "-1"},enabled=true)
echo
end-if
echo *** End Migration of /profile=$clusteredProfile ***

View file

@ -396,4 +396,12 @@ if (outcome == failed) of /profile=$standaloneProfile/subsystem=keycloak-server/
echo
end-if
# Migrate from 4.2.0 to 4.3.0
if (outcome == failed) of /subsystem=keycloak-server/spi=hostname/:read-resource
echo Adding spi=hostname...
/subsystem=keycloak-server/spi=hostname/:add(default-provider=request)
/subsystem=keycloak-server/spi=hostname/provider=fixed/:add(properties={hostname => "localhost",httpPort => "-1",httpsPort => "-1"},enabled=true)
echo
end-if
echo *** End Migration of /profile=$standaloneProfile ***

View file

@ -423,4 +423,12 @@ if (outcome == failed) of /subsystem=keycloak-server/spi=x509cert-lookup/:read-r
echo
end-if
# Migrate from 4.2.0 to 4.3.0
if (outcome == failed) of /subsystem=keycloak-server/spi=hostname/:read-resource
echo Adding spi=hostname...
/subsystem=keycloak-server/spi=hostname/:add(default-provider=request)
/subsystem=keycloak-server/spi=hostname/provider=fixed/:add(properties={hostname => "localhost",httpPort => "-1",httpsPort => "-1"},enabled=true)
echo
end-if
echo *** End Migration ***

View file

@ -367,4 +367,12 @@ if (outcome == failed) of /subsystem=keycloak-server/spi=x509cert-lookup/:read-r
echo
end-if
# Migrate from 4.2.0 to 4.3.0
if (outcome == failed) of /subsystem=keycloak-server/spi=hostname/:read-resource
echo Adding spi=hostname...
/subsystem=keycloak-server/spi=hostname/:add(default-provider=request)
/subsystem=keycloak-server/spi=hostname/provider=fixed/:add(properties={hostname => "localhost",httpPort => "-1",httpsPort => "-1"},enabled=true)
echo
end-if
echo *** End Migration ***

View file

@ -33,7 +33,7 @@ public interface KeycloakContext {
String getContextPath();
UriInfo getUri();
KeycloakUriInfo getUri();
HttpHeaders getRequestHeaders();

View file

@ -0,0 +1,161 @@
/*
* 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.
*/
package org.keycloak.models;
import org.jboss.resteasy.specimpl.ResteasyUriBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.urls.HostnameProvider;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.List;
public class KeycloakUriInfo implements UriInfo {
private final UriInfo delegate;
private final String hostname;
private final int port;
private URI absolutePath;
private URI requestURI;
private URI baseURI;
public KeycloakUriInfo(KeycloakSession session, UriInfo delegate) {
this.delegate = delegate;
HostnameProvider hostnameProvider = session.getProvider(HostnameProvider.class);
this.hostname = hostnameProvider.getHostname(delegate);
this.port = hostnameProvider.getPort(delegate);
}
public UriInfo getDelegate() {
return delegate;
}
@Override
public URI getRequestUri() {
if (requestURI == null) {
requestURI = delegate.getRequestUriBuilder().host(hostname).port(port).build();
}
return requestURI;
}
@Override
public UriBuilder getRequestUriBuilder() {
return UriBuilder.fromUri(getRequestUri());
}
@Override
public URI getAbsolutePath() {
if (absolutePath == null) {
absolutePath = delegate.getAbsolutePathBuilder().host(hostname).port(port).build();
}
return absolutePath;
}
@Override
public UriBuilder getAbsolutePathBuilder() {
return UriBuilder.fromUri(getAbsolutePath());
}
@Override
public URI getBaseUri() {
if (baseURI == null) {
baseURI = delegate.getBaseUriBuilder().host(hostname).port(port).build();
}
return baseURI;
}
@Override
public UriBuilder getBaseUriBuilder() {
return UriBuilder.fromUri(getBaseUri());
}
@Override
public URI resolve(URI uri) {
return getBaseUri().resolve(uri);
}
@Override
public URI relativize(URI uri) {
URI from = this.getRequestUri();
URI to = uri;
if (uri.getScheme() == null && uri.getHost() == null) {
to = this.getBaseUriBuilder().replaceQuery(null).path(uri.getPath()).replaceQuery(uri.getQuery()).fragment(uri.getFragment()).build(new Object[0]);
}
return ResteasyUriBuilder.relativize(from, to);
}
@Override
public String getPath() {
return delegate.getPath();
}
@Override
public String getPath(boolean decode) {
return delegate.getPath(decode);
}
@Override
public List<PathSegment> getPathSegments() {
return delegate.getPathSegments();
}
@Override
public List<PathSegment> getPathSegments(boolean decode) {
return delegate.getPathSegments(decode);
}
@Override
public MultivaluedMap<String, String> getPathParameters() {
return delegate.getPathParameters();
}
@Override
public MultivaluedMap<String, String> getPathParameters(boolean decode) {
return delegate.getPathParameters(decode);
}
@Override
public MultivaluedMap<String, String> getQueryParameters() {
return delegate.getQueryParameters();
}
@Override
public MultivaluedMap<String, String> getQueryParameters(boolean decode) {
return delegate.getQueryParameters(decode);
}
@Override
public List<String> getMatchedURIs() {
return delegate.getMatchedURIs();
}
@Override
public List<String> getMatchedURIs(boolean decode) {
return delegate.getMatchedURIs(decode);
}
@Override
public List<Object> getMatchedResources() {
return delegate.getMatchedResources();
}
}

View file

@ -0,0 +1,41 @@
/*
* 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.
*/
package org.keycloak.urls;
import org.keycloak.models.KeycloakContext;
import org.keycloak.provider.Provider;
import javax.ws.rs.core.UriInfo;
public interface HostnameProvider extends Provider {
/**
* 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!
*
* @param originalUriInfo the original UriInfo before hostname is replaced by the HostnameProvider
* @return the hostname
*/
String getHostname(UriInfo originalUriInfo);
int getPort(UriInfo originalUriInfo);
@Override
default void close() {
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.
*/
package org.keycloak.urls;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderFactory;
public interface HostnameProviderFactory extends ProviderFactory<HostnameProvider> {
@Override
default void close() {
}
@Override
default void init(Config.Scope config) {
}
@Override
default void postInit(KeycloakSessionFactory factory) {
}
@Override
default int order() {
return 0;
}
}

View file

@ -0,0 +1,45 @@
/*
* 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.
*/
package org.keycloak.urls;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class HostnameSpi implements Spi {
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
return "hostname";
}
@Override
public Class<? extends Provider> getProviderClass() {
return HostnameProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return HostnameProviderFactory.class;
}
}

View file

@ -34,4 +34,5 @@
org.keycloak.storage.UserStorageProviderSpi
org.keycloak.theme.ThemeResourceSpi
org.keycloak.theme.ThemeSelectorSpi
org.keycloak.theme.ThemeSelectorSpi
org.keycloak.urls.HostnameSpi

View file

@ -22,6 +22,7 @@ import org.keycloak.common.ClientConnection;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakUriInfo;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.resources.KeycloakApplication;
@ -45,6 +46,8 @@ public class DefaultKeycloakContext implements KeycloakContext {
private KeycloakSession session;
private KeycloakUriInfo uriInfo;
public DefaultKeycloakContext(KeycloakSession session) {
this.session = session;
}
@ -64,8 +67,11 @@ public class DefaultKeycloakContext implements KeycloakContext {
}
@Override
public UriInfo getUri() {
return getContextObject(UriInfo.class);
public KeycloakUriInfo getUri() {
if (uriInfo == null) {
uriInfo = new KeycloakUriInfo(session, getContextObject(UriInfo.class));
}
return uriInfo;
}
@Override
@ -86,6 +92,7 @@ public class DefaultKeycloakContext implements KeycloakContext {
@Override
public void setRealm(RealmModel realm) {
this.realm = realm;
this.uriInfo = null;
}
@Override

View file

@ -71,7 +71,6 @@ public class ClientRegistrationAuth {
private void init() {
realm = session.getContext().getRealm();
UriInfo uri = session.getContext().getUri();
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null) {
@ -85,7 +84,7 @@ public class ClientRegistrationAuth {
token = split[1];
ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, uri, token);
ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(session, realm, token);
if (tokenVerification.getError() != null) {
throw unauthorized(tokenVerification.getError().getMessage());
}

View file

@ -31,6 +31,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.Urls;
import org.keycloak.services.clientregistration.policy.RegistrationAuth;
import org.keycloak.urls.HostnameProvider;
import org.keycloak.util.TokenUtil;
import javax.ws.rs.core.UriInfo;
@ -66,25 +67,25 @@ public class ClientRegistrationTokenUtils {
}
public static String updateRegistrationAccessToken(KeycloakSession session, ClientModel client, RegistrationAuth registrationAuth) {
return updateRegistrationAccessToken(session, session.getContext().getRealm(), session.getContext().getUri(), client, registrationAuth);
return updateRegistrationAccessToken(session, session.getContext().getRealm(), client, registrationAuth);
}
public static String updateRegistrationAccessToken(KeycloakSession session, RealmModel realm, UriInfo uri, ClientModel client, RegistrationAuth registrationAuth) {
public static String updateRegistrationAccessToken(KeycloakSession session, RealmModel realm, ClientModel client, RegistrationAuth registrationAuth) {
String id = KeycloakModelUtils.generateId();
client.setRegistrationToken(id);
RegistrationAccessToken regToken = new RegistrationAccessToken();
regToken.setRegistrationAuth(registrationAuth.toString().toLowerCase());
return setupToken(regToken, session, realm, uri, id, TYPE_REGISTRATION_ACCESS_TOKEN, 0);
return setupToken(regToken, session, realm, id, TYPE_REGISTRATION_ACCESS_TOKEN, 0);
}
public static String createInitialAccessToken(KeycloakSession session, RealmModel realm, UriInfo uri, ClientInitialAccessModel model) {
public static String createInitialAccessToken(KeycloakSession session, RealmModel realm, ClientInitialAccessModel model) {
JsonWebToken initialToken = new JsonWebToken();
return setupToken(initialToken, session, realm, uri, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getExpiration() > 0 ? model.getTimestamp() + model.getExpiration() : 0);
return setupToken(initialToken, session, realm, model.getId(), TYPE_INITIAL_ACCESS_TOKEN, model.getExpiration() > 0 ? model.getTimestamp() + model.getExpiration() : 0);
}
public static TokenVerification verifyToken(KeycloakSession session, RealmModel realm, UriInfo uri, String token) {
public static TokenVerification verifyToken(KeycloakSession session, RealmModel realm, String token) {
if (token == null) {
return TokenVerification.error(new RuntimeException("Missing token"));
}
@ -110,7 +111,7 @@ public class ClientRegistrationTokenUtils {
return TokenVerification.error(new RuntimeException("Token is not JWT", e));
}
if (!getIssuer(realm, uri).equals(jwt.getIssuer())) {
if (!getIssuer(session, realm).equals(jwt.getIssuer())) {
return TokenVerification.error(new RuntimeException("Issuer from token don't match with the realm issuer."));
}
@ -127,8 +128,8 @@ public class ClientRegistrationTokenUtils {
return TokenVerification.success(kid, jwt);
}
private static String setupToken(JsonWebToken jwt, KeycloakSession session, RealmModel realm, UriInfo uri, String id, String type, int expiration) {
String issuer = getIssuer(realm, uri);
private static String setupToken(JsonWebToken jwt, KeycloakSession session, RealmModel realm, String id, String type, int expiration) {
String issuer = getIssuer(session, realm);
jwt.type(type);
jwt.id(id);
@ -143,8 +144,8 @@ public class ClientRegistrationTokenUtils {
return token;
}
private static String getIssuer(RealmModel realm, UriInfo uri) {
return Urls.realmIssuer(uri.getBaseUri(), realm.getName());
private static String getIssuer(KeycloakSession session, RealmModel realm) {
return Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName());
}
protected static class TokenVerification {

View file

@ -83,7 +83,7 @@ public class ClientInitialAccessResource {
ClientInitialAccessPresentation rep = wrap(clientInitialAccessModel);
String token = ClientRegistrationTokenUtils.createInitialAccessToken(session, realm, session.getContext().getUri(), clientInitialAccessModel);
String token = ClientRegistrationTokenUtils.createInitialAccessToken(session, realm, clientInitialAccessModel);
rep.setToken(token);
response.setStatus(Response.Status.CREATED.getStatusCode());

View file

@ -251,7 +251,7 @@ public class ClientResource {
public ClientRepresentation regenerateRegistrationAccessToken() {
auth.clients().requireManage(client);
String token = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, realm, session.getContext().getUri(), client, RegistrationAuth.AUTHENTICATED);
String token = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, realm, client, RegistrationAuth.AUTHENTICATED);
ClientRepresentation rep = ModelToRepresentation.toRepresentation(client);
rep.setRegistrationAccessToken(token);

View file

@ -0,0 +1,40 @@
package org.keycloak.url;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.urls.HostnameProvider;
import javax.ws.rs.core.UriInfo;
public class FixedHostnameProvider implements HostnameProvider {
private final KeycloakSession session;
private final String globalHostname;
private final int httpPort;
private final int httpsPort;
public FixedHostnameProvider(KeycloakSession session, String globalHostname, int httpPort, int httpsPort) {
this.session = session;
this.globalHostname = globalHostname;
this.httpPort = httpPort;
this.httpsPort = httpsPort;
}
@Override
public String getHostname(UriInfo originalUriInfo) {
RealmModel realm = session.getContext().getRealm();
if (realm != null) {
String realmHostname = session.getContext().getRealm().getAttribute("hostname");
if (realmHostname != null && !realmHostname.isEmpty()) {
return realmHostname;
}
}
return this.globalHostname;
}
@Override
public int getPort(UriInfo originalUriInfo) {
return originalUriInfo.getRequestUri().getScheme().equals("https") ? httpsPort : httpPort;
}
}

View file

@ -0,0 +1,35 @@
package org.keycloak.url;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.urls.HostnameProvider;
import org.keycloak.urls.HostnameProviderFactory;
public class FixedHostnameProviderFactory implements HostnameProviderFactory {
private String hostname;
private int httpPort;
private int httpsPort;
@Override
public HostnameProvider create(KeycloakSession session) {
return new FixedHostnameProvider(session, 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);
}
@Override
public String getId() {
return "fixed";
}
}

View file

@ -0,0 +1,19 @@
package org.keycloak.url;
import org.keycloak.urls.HostnameProvider;
import javax.ws.rs.core.UriInfo;
public class RequestHostnameProvider implements HostnameProvider {
@Override
public String getHostname(UriInfo originalUriInfo) {
return originalUriInfo.getBaseUri().getHost();
}
@Override
public int getPort(UriInfo originalUriInfo) {
return originalUriInfo.getRequestUri().getPort();
}
}

View file

@ -0,0 +1,21 @@
package org.keycloak.url;
import org.keycloak.models.KeycloakSession;
import org.keycloak.urls.HostnameProvider;
import org.keycloak.urls.HostnameProviderFactory;
import javax.ws.rs.core.UriInfo;
public class RequestHostnameProviderFactory implements HostnameProviderFactory {
@Override
public HostnameProvider create(KeycloakSession session) {
return new RequestHostnameProvider();
}
@Override
public String getId() {
return "request";
}
}

View file

@ -0,0 +1,2 @@
org.keycloak.url.FixedHostnameProviderFactory
org.keycloak.url.RequestHostnameProviderFactory

View file

@ -34,6 +34,7 @@ import org.junit.Assert;
import org.keycloak.OAuth2Constants;
import org.keycloak.RSATokenVerifier;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.PemUtils;
@ -46,6 +47,7 @@ import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
@ -633,6 +635,14 @@ public class OAuthClient {
}
}
public OIDCConfigurationRepresentation doWellKnownRequest(String realm) {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
return SimpleHttp.doGet(AUTH_SERVER_ROOT + "/realms/" + realm + "/.well-known/openid-configuration", client).asJson(OIDCConfigurationRepresentation.class);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
public void closeClient(CloseableHttpClient client) {
try {
client.close();
@ -729,7 +739,7 @@ public class OAuthClient {
}
public String getLoginFormUrl() {
UriBuilder b = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(AUTH_SERVER_ROOT));
UriBuilder b = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(baseUrl));
if (responseType != null) {
b.queryParam(OAuth2Constants.RESPONSE_TYPE, responseType);
}
@ -824,6 +834,11 @@ public class OAuthClient {
return b.build(realm).toString();
}
public OAuthClient baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
public OAuthClient realm(String realm) {
this.realm = realm;
return this;

View file

@ -149,9 +149,7 @@ public abstract class AbstractKeycloakTest {
public void beforeAbstractKeycloakTest() throws Exception {
adminClient = testContext.getAdminClient();
if (adminClient == null || adminClient.isClosed()) {
String authServerContextRoot = suiteContext.getAuthServerInfo().getContextRoot().toString();
adminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), authServerContextRoot);
testContext.setAdminClient(adminClient);
reconnectAdminClient();
}
getTestingClient();
@ -181,6 +179,16 @@ public abstract class AbstractKeycloakTest {
}
public void reconnectAdminClient() throws Exception {
if (adminClient != null && !adminClient.isClosed()) {
adminClient.close();
}
String authServerContextRoot = suiteContext.getAuthServerInfo().getContextRoot().toString();
adminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), authServerContextRoot);
testContext.setAdminClient(adminClient);
}
protected void beforeAbstractKeycloakTestRealmImport() throws Exception {
}
protected void postAfterAbstractKeycloak() {

View file

@ -0,0 +1,165 @@
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.Test;
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.AbstractKeycloakTest;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
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 java.io.IOException;
import java.util.HashMap;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
public class FixedHostnameTest extends AbstractKeycloakTest {
@ArquillianResource
protected ContainerController controller;
@Override
public void addTestRealms(List<RealmRepresentation> 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");
testRealms.add(customHostname);
}
@Test
public void fixedHostname() throws Exception {
try {
assertWellKnown("test", "localhost");
configureFixedHostname();
assertWellKnown("test", "keycloak.127.0.0.1.nip.io");
assertWellKnown("hostname", "custom-domain.127.0.0.1.nip.io");
assertTokenIssuer("test", "keycloak.127.0.0.1.nip.io");
assertTokenIssuer("hostname", "custom-domain.127.0.0.1.nip.io");
assertInitialAccessTokenFromMasterRealm("test", "keycloak.127.0.0.1.nip.io");
assertInitialAccessTokenFromMasterRealm("hostname", "custom-domain.127.0.0.1.nip.io");
} finally {
clearFixedHostname();
}
}
private void assertInitialAccessTokenFromMasterRealm(String realm, String expectedHostname) throws JWSInputException, ClientRegistrationException {
ClientInitialAccessCreatePresentation rep = new ClientInitialAccessCreatePresentation();
rep.setCount(1);
rep.setExpiration(10000);
ClientInitialAccessPresentation initialAccess = adminClient.realm(realm).clientInitialAccess().create(rep);
JsonWebToken token = new JWSInput(initialAccess.getToken()).readJsonContent(JsonWebToken.class);
assertEquals("http://" + expectedHostname + ":8180/auth/realms/" + realm, token.getIssuer());
ClientRegistration clientReg = ClientRegistration.create().url(suiteContext.getAuthServerInfo().getContextRoot() + "/auth", 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("http://" + expectedHostname + ":8180/auth/realms/" + realm, registrationToken.getIssuer());
}
private void assertTokenIssuer(String realm, String expectedHostname) throws JWSInputException, IOException {
oauth.baseUrl("http://" + expectedHostname + ":8180/auth");
OAuthClient.AuthorizationEndpointResponse response = oauth.realm(realm).doLogin("test-user@localhost", "password");
OAuthClient.AccessTokenResponse tokenResponse = oauth.baseUrl(OAuthClient.AUTH_SERVER_ROOT).doAccessTokenRequest(response.getCode(), "password");
AccessToken token = new JWSInput(tokenResponse.getAccessToken()).readJsonContent(AccessToken.class);
assertEquals("http://" + expectedHostname + ":8180/auth/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("http://" + expectedHostname + ":8180/auth/realms/" + realm, introspectionNode.get("iss").asText());
}
private void assertWellKnown(String realm, String expectedHostname) {
OIDCConfigurationRepresentation config = oauth.doWellKnownRequest(realm);
assertEquals("http://" + expectedHostname + ":8180/auth/realms/" + realm + "/protocol/openid-connect/token", config.getTokenEndpoint());
}
private void configureFixedHostname() throws Exception {
if (suiteContext.getAuthServerInfo().isUndertow()) {
configureUndertow("fixed", "keycloak.127.0.0.1.nip.io", "8180", "8543");
} else if (suiteContext.getAuthServerInfo().isJBossBased()) {
configureWildFly("fixed", "keycloak.127.0.0.1.nip.io", "8180", "8543");
} else {
throw new RuntimeException("Don't know how to config");
}
reconnectAdminClient();
}
private void clearFixedHostname() throws Exception {
if (suiteContext.getAuthServerInfo().isUndertow()) {
configureUndertow("request", "localhost", "-1", "-1");
} else if (suiteContext.getAuthServerInfo().isJBossBased()) {
configureWildFly("request", "localhost", "-1", "-1");
} else {
throw new RuntimeException("Don't know how to config");
}
reconnectAdminClient();
}
private void configureUndertow(String provider, String hostname, String httpPort, String httpsPort) {
controller.stop(suiteContext.getAuthServerInfo().getQualifier());
System.setProperty("keycloak.hostname.provider", provider);
System.setProperty("keycloak.hostname.fixed.hostname", hostname);
System.setProperty("keycloak.hostname.fixed.httpPort", httpPort);
System.setProperty("keycloak.hostname.fixed.httpsPort", httpsPort);
controller.start(suiteContext.getAuthServerInfo().getQualifier());
}
private void configureWildFly(String provider, String hostname, String httpPort, String httpsPort) 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 + ")");
administration.reloadIfRequired();
client.close();
}
}

View file

@ -1,4 +1,15 @@
{
"hostname": {
"provider": "${keycloak.hostname.provider:request}",
"fixed": {
"hostname": "${keycloak.hostname.fixed.hostname:localhost}",
"httpPort": "${keycloak.hostname.fixed.httpPort:-1}",
"httpsPort": "${keycloak.hostname.fixed.httpPorts:-1}"
}
},
"admin": {
"realm": "master"
},
@ -85,7 +96,7 @@
"connectionsJpa": {
"default": {
"url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test;MVCC=TRUE}",
"url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test;MVCC=TRUE;DB_CLOSE_DELAY=-1}",
"driver": "${keycloak.connectionsJpa.driver:org.h2.Driver}",
"driverDialect": "${keycloak.connectionsJpa.driverDialect:}",
"user": "${keycloak.connectionsJpa.user:sa}",

View file

@ -1,4 +1,15 @@
{
"hostname": {
"provider": "request",
"fixed": {
"hostname": "localhost",
"httpPort": "-1",
"httpsPort": "-1"
}
},
"admin": {
"realm": "master"
},
@ -61,7 +72,6 @@
"default": {}
},
"connectionsJpa": {
"default": {
"url": "${keycloak.connectionsJpa.url:jdbc:h2:mem:test;DB_CLOSE_DELAY=-1}",
@ -114,4 +124,4 @@
}
}
}
}

View file

@ -108,6 +108,8 @@ sso-session-idle.tooltip=Time a session is allowed to be idle before it expires.
sso-session-max.tooltip=Max time before a session is expired. Tokens and browser sessions are invalidated when a session is expired.
offline-session-idle=Offline Session Idle
offline-session-idle.tooltip=Time an offline session is allowed to be idle before it expires. You need to use offline token to refresh at least once within this period, otherwise offline session will expire.
realm-detail.hostname=Hostname
realm-detail.hostname.tooltip=Set the hostname for the realm. Use in combination with the fixed hostname provider to override the server hostname for a specific realm.
## KEYCLOAK-7688 Offline Session Max for Offline Token
offline-session-max-limited=Offline Session Max Limited

View file

@ -23,6 +23,14 @@
</div>
</div>
<div class="form-group" data-ng-show="serverInfo.listProviderIds('hostname').includes('fixed')">
<label class="col-md-2 control-label" for="name">{{:: 'realm-detail.hostname' | translate}}</label>
<div class="col-md-6">
<input class="form-control" type="text" id="hostname" name="hostname" data-ng-model="realm.attributes.hostname">
</div>
<kc-tooltip>{{:: 'realm-detail.hostname.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
<div class="col-md-6">

View file

@ -77,7 +77,7 @@ if [ $1 == "server-group3" ]; then
fi
if [ $1 == "server-group4" ]; then
run-server-tests org.keycloak.testsuite.k*.**.*Test,org.keycloak.testsuite.m*.**.*Test,org.keycloak.testsuite.o*.**.*Test,org.keycloak.testsuite.s*.**.*Test
run-server-tests org.keycloak.testsuite.k*.**.*Test,org.keycloak.testsuite.m*.**.*Test,org.keycloak.testsuite.o*.**.*Test,org.keycloak.testsuite.s*.**.*Test,org.keycloak.testsuite.u*.**.*Test
fi
if [ $1 == "crossdc-server" ]; then

View file

@ -77,4 +77,14 @@ keycloak.server.subsys.default.config=\
<default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>\
<provider name="default" enabled="true"/>\
</spi>\
<spi name="hostname">\
<default-provider>request</default-provider>\
<provider name="fixed" enabled="true">\
<properties>\
<property name="hostname" value="localhost"/>\
<property name="httpPort" value="-1"/>\
<property name="httpsPort" value="-1"/>\
</properties>\
</provider>\
</spi>\
</subsystem>\

View file

@ -1,5 +1,7 @@
/subsystem=keycloak-server:add(web-context=auth,master-realm-name=master,scheduled-task-interval=900)
/subsystem=keycloak-server/theme=defaults/:add(dir=${jboss.home.dir}/themes,staticMaxAge=2592000,cacheTemplates=true,cacheThemes=true)
/subsystem=keycloak-server/spi=hostname/:add(default-provider=request)
/subsystem=keycloak-server/spi=hostname/provider=fixed/:add(properties={hostname => "localhost",httpPort => "-1",httpsPort => "-1"},enabled=true)
/subsystem=keycloak-server/spi=eventsStore/:add
/subsystem=keycloak-server/spi=eventsStore/provider=jpa/:add(properties={exclude-events => "[\"REFRESH_TOKEN\"]"},enabled=true)
/subsystem=keycloak-server/spi=userCache/:add