KEYCLOAK-7967 Introduce Hostname SPI
This commit is contained in:
parent
ae47b7fa80
commit
f99299ee39
30 changed files with 704 additions and 27 deletions
|
@ -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 ***
|
|
@ -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 ***
|
|
@ -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 ***
|
|
@ -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 ***
|
|
@ -33,7 +33,7 @@ public interface KeycloakContext {
|
|||
|
||||
String getContextPath();
|
||||
|
||||
UriInfo getUri();
|
||||
KeycloakUriInfo getUri();
|
||||
|
||||
HttpHeaders getRequestHeaders();
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
45
server-spi/src/main/java/org/keycloak/urls/HostnameSpi.java
Normal file
45
server-spi/src/main/java/org/keycloak/urls/HostnameSpi.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -35,3 +35,4 @@
|
|||
org.keycloak.storage.UserStorageProviderSpi
|
||||
org.keycloak.theme.ThemeResourceSpi
|
||||
org.keycloak.theme.ThemeSelectorSpi
|
||||
org.keycloak.urls.HostnameSpi
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
org.keycloak.url.FixedHostnameProviderFactory
|
||||
org.keycloak.url.RequestHostnameProviderFactory
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>\
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue