Aligns the logic in the welcome resources
as a result the quarkus one can be removed closes keycloak#23243
This commit is contained in:
parent
3d42573813
commit
fb69936f14
7 changed files with 84 additions and 365 deletions
|
@ -19,6 +19,7 @@ package org.keycloak.quarkus.runtime.hostname;
|
||||||
|
|
||||||
import static org.keycloak.common.util.UriUtils.checkUrl;
|
import static org.keycloak.common.util.UriUtils.checkUrl;
|
||||||
import static org.keycloak.urls.UrlType.ADMIN;
|
import static org.keycloak.urls.UrlType.ADMIN;
|
||||||
|
import static org.keycloak.urls.UrlType.LOCAL_ADMIN;
|
||||||
import static org.keycloak.urls.UrlType.BACKEND;
|
import static org.keycloak.urls.UrlType.BACKEND;
|
||||||
import static org.keycloak.urls.UrlType.FRONTEND;
|
import static org.keycloak.urls.UrlType.FRONTEND;
|
||||||
import static org.keycloak.utils.StringUtil.isNotBlank;
|
import static org.keycloak.utils.StringUtil.isNotBlank;
|
||||||
|
@ -61,12 +62,16 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
|
||||||
private int hostnamePort;
|
private int hostnamePort;
|
||||||
private URI frontEndBaseUri;
|
private URI frontEndBaseUri;
|
||||||
private URI adminBaseUri;
|
private URI adminBaseUri;
|
||||||
|
private URI localAdminUri;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getScheme(UriInfo originalUriInfo, UrlType urlType) {
|
public String getScheme(UriInfo originalUriInfo, UrlType urlType) {
|
||||||
if (ADMIN.equals(urlType)) {
|
if (ADMIN.equals(urlType)) {
|
||||||
return fromBaseUriOrDefault(URI::getScheme, adminBaseUri, getScheme(originalUriInfo));
|
return fromBaseUriOrDefault(URI::getScheme, adminBaseUri, getScheme(originalUriInfo));
|
||||||
}
|
}
|
||||||
|
if (LOCAL_ADMIN.equals(urlType)) {
|
||||||
|
return fromBaseUriOrDefault(URI::getHost, localAdminUri, getScheme(originalUriInfo));
|
||||||
|
}
|
||||||
|
|
||||||
String scheme = forNonStrictBackChannel(originalUriInfo, urlType, this::getScheme, this::getScheme);
|
String scheme = forNonStrictBackChannel(originalUriInfo, urlType, this::getScheme, this::getScheme);
|
||||||
|
|
||||||
|
@ -82,6 +87,9 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
|
||||||
if (ADMIN.equals(urlType)) {
|
if (ADMIN.equals(urlType)) {
|
||||||
return fromBaseUriOrDefault(URI::getHost, adminBaseUri, adminHostName == null ? getHostname(originalUriInfo) : adminHostName);
|
return fromBaseUriOrDefault(URI::getHost, adminBaseUri, adminHostName == null ? getHostname(originalUriInfo) : adminHostName);
|
||||||
}
|
}
|
||||||
|
if (LOCAL_ADMIN.equals(urlType)) {
|
||||||
|
return fromBaseUriOrDefault(URI::getHost, localAdminUri, getHostname(originalUriInfo));
|
||||||
|
}
|
||||||
|
|
||||||
String hostname = forNonStrictBackChannel(originalUriInfo, urlType, this::getHostname, this::getHostname);
|
String hostname = forNonStrictBackChannel(originalUriInfo, urlType, this::getHostname, this::getHostname);
|
||||||
|
|
||||||
|
@ -97,6 +105,9 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
|
||||||
if (ADMIN.equals(urlType)) {
|
if (ADMIN.equals(urlType)) {
|
||||||
return fromBaseUriOrDefault(URI::getPath, adminBaseUri, getContextPath(originalUriInfo));
|
return fromBaseUriOrDefault(URI::getPath, adminBaseUri, getContextPath(originalUriInfo));
|
||||||
}
|
}
|
||||||
|
if (LOCAL_ADMIN.equals(urlType)) {
|
||||||
|
return fromBaseUriOrDefault(URI::getPath, localAdminUri, getContextPath(originalUriInfo));
|
||||||
|
}
|
||||||
|
|
||||||
String path = forNonStrictBackChannel(originalUriInfo, urlType, this::getContextPath, this::getContextPath);
|
String path = forNonStrictBackChannel(originalUriInfo, urlType, this::getContextPath, this::getContextPath);
|
||||||
|
|
||||||
|
@ -112,6 +123,9 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
|
||||||
if (ADMIN.equals(urlType)) {
|
if (ADMIN.equals(urlType)) {
|
||||||
return fromBaseUriOrDefault(URI::getPort, adminBaseUri, getRequestPort(originalUriInfo));
|
return fromBaseUriOrDefault(URI::getPort, adminBaseUri, getRequestPort(originalUriInfo));
|
||||||
}
|
}
|
||||||
|
if (LOCAL_ADMIN.equals(urlType)) {
|
||||||
|
return fromBaseUriOrDefault(URI::getPort, localAdminUri, getRequestPort(originalUriInfo));
|
||||||
|
}
|
||||||
|
|
||||||
Integer port = forNonStrictBackChannel(originalUriInfo, urlType, this::getPort, this::getRequestPort);
|
Integer port = forNonStrictBackChannel(originalUriInfo, urlType, this::getPort, this::getRequestPort);
|
||||||
|
|
||||||
|
@ -223,6 +237,20 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
boolean isHttpEnabled = Boolean.parseBoolean(Configuration.getConfigValue("kc.http-enabled").getValue());
|
||||||
|
String configPath = Configuration.getConfigValue("kc.http-relative-path").getValue();
|
||||||
|
|
||||||
|
if (!configPath.startsWith("/")) {
|
||||||
|
configPath = "/" + configPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
String httpPort = Configuration.getConfigValue("kc.https-port").getValue();
|
||||||
|
String configPort = isHttpEnabled ? Configuration.getConfigValue("kc.http-port").getValue() : httpPort ;
|
||||||
|
|
||||||
|
String scheme = isHttpEnabled ? "http://" : "https://";
|
||||||
|
|
||||||
|
localAdminUri = URI.create(scheme + "localhost:" + configPort + configPath);
|
||||||
|
|
||||||
frontEndHostName = config.get("hostname");
|
frontEndHostName = config.get("hostname");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -258,7 +286,7 @@ public final class DefaultHostnameProvider implements HostnameProvider, Hostname
|
||||||
|
|
||||||
defaultPath = config.get("path", frontEndBaseUri == null ? null : frontEndBaseUri.getPath());
|
defaultPath = config.get("path", frontEndBaseUri == null ? null : frontEndBaseUri.getPath());
|
||||||
noProxy = Configuration.getConfigValue("kc.proxy").getValue().equals("false");
|
noProxy = Configuration.getConfigValue("kc.proxy").getValue().equals("false");
|
||||||
defaultTlsPort = Integer.parseInt(Configuration.getConfigValue("kc.https-port").getValue());
|
defaultTlsPort = Integer.parseInt(httpPort);
|
||||||
|
|
||||||
if (defaultTlsPort == DEFAULT_HTTPS_PORT_VALUE) {
|
if (defaultTlsPort == DEFAULT_HTTPS_PORT_VALUE) {
|
||||||
defaultTlsPort = RESTEASY_DEFAULT_PORT_VALUE;
|
defaultTlsPort = RESTEASY_DEFAULT_PORT_VALUE;
|
||||||
|
|
|
@ -26,13 +26,10 @@ import jakarta.ws.rs.ApplicationPath;
|
||||||
import org.keycloak.config.HostnameOptions;
|
import org.keycloak.config.HostnameOptions;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.platform.Platform;
|
import org.keycloak.platform.Platform;
|
||||||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
|
||||||
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
|
import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory;
|
||||||
import org.keycloak.quarkus.runtime.integration.QuarkusPlatform;
|
import org.keycloak.quarkus.runtime.integration.QuarkusPlatform;
|
||||||
import org.keycloak.quarkus.runtime.services.resources.DebugHostnameSettingsResource;
|
import org.keycloak.quarkus.runtime.services.resources.DebugHostnameSettingsResource;
|
||||||
import org.keycloak.services.resources.KeycloakApplication;
|
import org.keycloak.services.resources.KeycloakApplication;
|
||||||
import org.keycloak.quarkus.runtime.services.resources.QuarkusWelcomeResource;
|
|
||||||
import org.keycloak.services.resources.WelcomeResource;
|
|
||||||
|
|
||||||
import io.quarkus.runtime.ShutdownEvent;
|
import io.quarkus.runtime.ShutdownEvent;
|
||||||
import io.quarkus.runtime.StartupEvent;
|
import io.quarkus.runtime.StartupEvent;
|
||||||
|
@ -74,9 +71,6 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
|
||||||
public Set<Class<?>> getClasses() {
|
public Set<Class<?>> getClasses() {
|
||||||
Set<Class<?>> classes = new HashSet<>(super.getClasses());
|
Set<Class<?>> classes = new HashSet<>(super.getClasses());
|
||||||
|
|
||||||
classes.remove(WelcomeResource.class);
|
|
||||||
classes.add(QuarkusWelcomeResource.class);
|
|
||||||
|
|
||||||
classes.add(QuarkusObjectMapperResolver.class);
|
classes.add(QuarkusObjectMapperResolver.class);
|
||||||
classes.add(CloseSessionHandler.class);
|
classes.add(CloseSessionHandler.class);
|
||||||
|
|
||||||
|
|
|
@ -1,311 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2021 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.quarkus.runtime.services.resources;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.keycloak.common.ClientConnection;
|
|
||||||
import org.keycloak.common.Profile;
|
|
||||||
import org.keycloak.common.Version;
|
|
||||||
import org.keycloak.common.util.Base64Url;
|
|
||||||
import org.keycloak.common.util.MimeTypeUtil;
|
|
||||||
import org.keycloak.common.util.SecretGenerator;
|
|
||||||
import org.keycloak.http.HttpRequest;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
|
||||||
import org.keycloak.services.ForbiddenException;
|
|
||||||
import org.keycloak.services.ServicesLogger;
|
|
||||||
import org.keycloak.services.managers.ApplianceBootstrap;
|
|
||||||
import org.keycloak.services.util.CacheControlUtil;
|
|
||||||
import org.keycloak.services.util.CookieHelper;
|
|
||||||
import org.keycloak.theme.Theme;
|
|
||||||
import org.keycloak.theme.freemarker.FreeMarkerProvider;
|
|
||||||
import org.keycloak.urls.UrlType;
|
|
||||||
import org.keycloak.utils.MediaType;
|
|
||||||
|
|
||||||
import io.quarkus.logging.Log;
|
|
||||||
|
|
||||||
import jakarta.ws.rs.Consumes;
|
|
||||||
import jakarta.ws.rs.GET;
|
|
||||||
import jakarta.ws.rs.POST;
|
|
||||||
import jakarta.ws.rs.Path;
|
|
||||||
import jakarta.ws.rs.PathParam;
|
|
||||||
import jakarta.ws.rs.Produces;
|
|
||||||
import jakarta.ws.rs.WebApplicationException;
|
|
||||||
import jakarta.ws.rs.core.Context;
|
|
||||||
import jakarta.ws.rs.core.Cookie;
|
|
||||||
import jakarta.ws.rs.core.HttpHeaders;
|
|
||||||
import jakarta.ws.rs.core.MultivaluedMap;
|
|
||||||
import jakarta.ws.rs.core.Response;
|
|
||||||
import jakarta.ws.rs.core.Response.ResponseBuilder;
|
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
|
||||||
*/
|
|
||||||
@Path("/")
|
|
||||||
public class QuarkusWelcomeResource {
|
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(QuarkusWelcomeResource.class);
|
|
||||||
private static final String KEYCLOAK_STATE_CHECKER = "WELCOME_STATE_CHECKER";
|
|
||||||
|
|
||||||
private AtomicBoolean shouldBootstrap;
|
|
||||||
|
|
||||||
@Context
|
|
||||||
KeycloakSession session;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Welcome page of Keycloak
|
|
||||||
*/
|
|
||||||
@GET
|
|
||||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
|
||||||
public Response getWelcomePage() throws URISyntaxException {
|
|
||||||
String requestUri = session.getContext().getUri().getRequestUri().toString();
|
|
||||||
if (!requestUri.endsWith("/")) {
|
|
||||||
return Response.seeOther(new URI(requestUri + "/")).build();
|
|
||||||
} else {
|
|
||||||
return createWelcomePage(null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
|
||||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
|
||||||
public Response createUser() {
|
|
||||||
HttpRequest request = session.getContext().getHttpRequest();
|
|
||||||
MultivaluedMap<String, String> formData = request.getDecodedFormParameters();
|
|
||||||
|
|
||||||
if (!shouldBootstrap()) {
|
|
||||||
return createWelcomePage(null, null);
|
|
||||||
} else {
|
|
||||||
if (!isLocal()) {
|
|
||||||
ServicesLogger.LOGGER.rejectedNonLocalAttemptToCreateInitialUser(session.getContext().getConnection().getRemoteAddr());
|
|
||||||
throw new WebApplicationException(Response.Status.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
csrfCheck(formData);
|
|
||||||
|
|
||||||
String username = formData.getFirst("username");
|
|
||||||
String password = formData.getFirst("password");
|
|
||||||
String passwordConfirmation = formData.getFirst("passwordConfirmation");
|
|
||||||
|
|
||||||
if (username != null) {
|
|
||||||
username = username.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username == null || username.length() == 0) {
|
|
||||||
return createWelcomePage(null, "Username is missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password == null || password.length() == 0) {
|
|
||||||
return createWelcomePage(null, "Password is missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password.equals(passwordConfirmation)) {
|
|
||||||
return createWelcomePage(null, "Password and confirmation doesn't match");
|
|
||||||
}
|
|
||||||
|
|
||||||
expireCsrfCookie();
|
|
||||||
|
|
||||||
ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
|
|
||||||
applianceBootstrap.createMasterRealmUser(username, password);
|
|
||||||
|
|
||||||
shouldBootstrap.set(false);
|
|
||||||
ServicesLogger.LOGGER.createdInitialAdminUser(username);
|
|
||||||
return createWelcomePage("User created", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resources for welcome page
|
|
||||||
*/
|
|
||||||
@GET
|
|
||||||
@Path("/welcome-content/{path}")
|
|
||||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
|
||||||
public Response getResource(@PathParam("path") String path) {
|
|
||||||
try {
|
|
||||||
InputStream resource = getTheme().getResourceAsStream(path);
|
|
||||||
if (resource != null) {
|
|
||||||
String contentType = MimeTypeUtil.getContentType(path);
|
|
||||||
Response.ResponseBuilder builder = Response.ok(resource).type(contentType).cacheControl(CacheControlUtil.getDefaultCacheControl());
|
|
||||||
return builder.build();
|
|
||||||
} else {
|
|
||||||
return Response.status(Response.Status.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Response createWelcomePage(String successMessage, String errorMessage) {
|
|
||||||
try {
|
|
||||||
Theme theme = getTheme();
|
|
||||||
|
|
||||||
if(Objects.isNull(theme)) {
|
|
||||||
Log.error("Theme is null please check the \"--spi-theme-default\" parameter");
|
|
||||||
errorMessage = "The theme is null";
|
|
||||||
ResponseBuilder rb = Response.status(Status.BAD_REQUEST)
|
|
||||||
.entity(errorMessage)
|
|
||||||
.cacheControl(CacheControlUtil.noCache());
|
|
||||||
return rb.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> map = new HashMap<>();
|
|
||||||
|
|
||||||
map.put("adminConsoleEnabled", isAdminConsoleEnabled());
|
|
||||||
map.put("productName", Version.NAME);
|
|
||||||
|
|
||||||
map.put("properties", theme.getProperties());
|
|
||||||
map.put("adminUrl", session.getContext().getUri(UrlType.ADMIN).getBaseUriBuilder().path("/admin/").build());
|
|
||||||
|
|
||||||
map.put("resourcesPath", "resources/" + Version.RESOURCES_VERSION + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName());
|
|
||||||
map.put("resourcesCommonPath", "resources/" + Version.RESOURCES_VERSION + "/common/keycloak");
|
|
||||||
|
|
||||||
boolean bootstrap = shouldBootstrap();
|
|
||||||
map.put("bootstrap", bootstrap);
|
|
||||||
if (bootstrap) {
|
|
||||||
boolean isLocal = isLocal();
|
|
||||||
map.put("localUser", isLocal);
|
|
||||||
|
|
||||||
String localAdminUrl = getLocalAdminUrl();
|
|
||||||
|
|
||||||
map.put("localAdminUrl", localAdminUrl);
|
|
||||||
map.put("adminUserCreationMessage", "or set the environment variables KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD before starting the server");
|
|
||||||
|
|
||||||
if (isLocal) {
|
|
||||||
String stateChecker = setCsrfCookie();
|
|
||||||
map.put("stateChecker", stateChecker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (successMessage != null) {
|
|
||||||
map.put("successMessage", successMessage);
|
|
||||||
}
|
|
||||||
if (errorMessage != null) {
|
|
||||||
map.put("errorMessage", errorMessage);
|
|
||||||
}
|
|
||||||
FreeMarkerProvider freeMarkerUtil = session.getProvider(FreeMarkerProvider.class);
|
|
||||||
String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme);
|
|
||||||
|
|
||||||
ResponseBuilder rb = Response.status(errorMessage == null ? Status.OK : Status.BAD_REQUEST)
|
|
||||||
.entity(result)
|
|
||||||
.cacheControl(CacheControlUtil.noCache());
|
|
||||||
return rb.build();
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getLocalAdminUrl() {
|
|
||||||
boolean isHttpEnabled = Boolean.parseBoolean(Configuration.getConfigValue("kc.http-enabled").getValue());
|
|
||||||
String configPath = Configuration.getConfigValue("kc.http-relative-path").getValue();
|
|
||||||
|
|
||||||
if (!configPath.startsWith("/")) {
|
|
||||||
configPath = "/" + configPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
String configPort = isHttpEnabled ? Configuration.getConfigValue("kc.http-port").getValue() : Configuration.getConfigValue("kc.https-port").getValue() ;
|
|
||||||
|
|
||||||
String scheme = isHttpEnabled ? "http://" : "https://";
|
|
||||||
|
|
||||||
return scheme + "localhost:" + configPort + configPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Theme getTheme() {
|
|
||||||
try {
|
|
||||||
return session.theme().getTheme(Theme.Type.WELCOME);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldBootstrap() {
|
|
||||||
if (shouldBootstrap == null) {
|
|
||||||
synchronized (this) {
|
|
||||||
if (shouldBootstrap == null) {
|
|
||||||
shouldBootstrap = new AtomicBoolean(new ApplianceBootstrap(session).isNoMasterUser());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return shouldBootstrap.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isAdminConsoleEnabled() {
|
|
||||||
return Profile.isFeatureEnabled(Profile.Feature.ADMIN2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isLocal() {
|
|
||||||
try {
|
|
||||||
ClientConnection clientConnection = session.getContext().getConnection();
|
|
||||||
InetAddress remoteInetAddress = InetAddress.getByName(clientConnection.getRemoteAddr());
|
|
||||||
InetAddress localInetAddress = InetAddress.getByName(clientConnection.getLocalAddr());
|
|
||||||
HttpRequest request = session.getContext().getHttpRequest();
|
|
||||||
HttpHeaders headers = request.getHttpHeaders();
|
|
||||||
String xForwardedFor = headers.getHeaderString("X-Forwarded-For");
|
|
||||||
logger.debugf("Checking WelcomePage. Remote address: %s, Local address: %s, X-Forwarded-For header: %s", remoteInetAddress.toString(), localInetAddress.toString(), xForwardedFor);
|
|
||||||
|
|
||||||
// Access through AJP protocol (loadbalancer) may cause that remoteAddress is "127.0.0.1".
|
|
||||||
// So consider that welcome page accessed locally just if it was accessed really through "localhost" URL and without loadbalancer (x-forwarded-for header is empty).
|
|
||||||
return isLocalAddress(remoteInetAddress) && isLocalAddress(localInetAddress) && xForwardedFor == null;
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isLocalAddress(InetAddress inetAddress) {
|
|
||||||
return inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String setCsrfCookie() {
|
|
||||||
String stateChecker = Base64Url.encode(SecretGenerator.getInstance().randomBytes());
|
|
||||||
String cookiePath = session.getContext().getUri().getPath();
|
|
||||||
boolean secureOnly = session.getContext().getUri().getRequestUri().getScheme().equalsIgnoreCase("https");
|
|
||||||
CookieHelper.addCookie(KEYCLOAK_STATE_CHECKER, stateChecker, cookiePath, null, null, 300, secureOnly, true, session);
|
|
||||||
return stateChecker;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void expireCsrfCookie() {
|
|
||||||
String cookiePath = session.getContext().getUri().getPath();
|
|
||||||
boolean secureOnly = session.getContext().getUri().getRequestUri().getScheme().equalsIgnoreCase("https");
|
|
||||||
CookieHelper.addCookie(KEYCLOAK_STATE_CHECKER, "", cookiePath, null, null, 0, secureOnly, true, session);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void csrfCheck(final MultivaluedMap<String, String> formData) {
|
|
||||||
String formStateChecker = formData.getFirst("stateChecker");
|
|
||||||
HttpRequest request = session.getContext().getHttpRequest();
|
|
||||||
HttpHeaders headers = request.getHttpHeaders();
|
|
||||||
Cookie cookie = headers.getCookies().get(KEYCLOAK_STATE_CHECKER);
|
|
||||||
if (cookie == null) {
|
|
||||||
throw new ForbiddenException();
|
|
||||||
}
|
|
||||||
|
|
||||||
String cookieStateChecker = cookie.getValue();
|
|
||||||
|
|
||||||
if (cookieStateChecker == null || !cookieStateChecker.equals(formStateChecker)) {
|
|
||||||
throw new ForbiddenException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -2,6 +2,6 @@ package org.keycloak.urls;
|
||||||
|
|
||||||
public enum UrlType {
|
public enum UrlType {
|
||||||
|
|
||||||
FRONTEND, BACKEND, ADMIN
|
FRONTEND, BACKEND, ADMIN, LOCAL_ADMIN
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,6 @@ import java.util.List;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.ServiceLoader;
|
import java.util.ServiceLoader;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -74,8 +73,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
*/
|
*/
|
||||||
public class KeycloakApplication extends Application {
|
public class KeycloakApplication extends Application {
|
||||||
|
|
||||||
public static final AtomicBoolean BOOTSTRAP_ADMIN_USER = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(KeycloakApplication.class);
|
private static final Logger logger = Logger.getLogger(KeycloakApplication.class);
|
||||||
|
|
||||||
protected final PlatformProvider platform = Platform.getPlatform();
|
protected final PlatformProvider platform = Platform.getPlatform();
|
||||||
|
@ -153,16 +150,6 @@ public class KeycloakApplication extends Application {
|
||||||
exportImportManager[0].runExport();
|
exportImportManager[0].runExport();
|
||||||
}
|
}
|
||||||
|
|
||||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(KeycloakSession session) {
|
|
||||||
boolean shouldBootstrapAdmin = new ApplianceBootstrap(session).isNoMasterUser();
|
|
||||||
BOOTSTRAP_ADMIN_USER.set(shouldBootstrapAdmin);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
sessionFactory.publish(new PostMigrationEvent(sessionFactory));
|
sessionFactory.publish(new PostMigrationEvent(sessionFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.common.Version;
|
||||||
import org.keycloak.common.util.Base64Url;
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.common.util.MimeTypeUtil;
|
import org.keycloak.common.util.MimeTypeUtil;
|
||||||
import org.keycloak.common.util.SecretGenerator;
|
import org.keycloak.common.util.SecretGenerator;
|
||||||
|
import org.keycloak.http.HttpRequest;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.services.ForbiddenException;
|
import org.keycloak.services.ForbiddenException;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
|
@ -56,6 +57,8 @@ import java.net.URISyntaxException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -67,11 +70,10 @@ public class WelcomeResource {
|
||||||
|
|
||||||
private static final String KEYCLOAK_STATE_CHECKER = "WELCOME_STATE_CHECKER";
|
private static final String KEYCLOAK_STATE_CHECKER = "WELCOME_STATE_CHECKER";
|
||||||
|
|
||||||
@Context
|
private AtomicBoolean shouldBootstrap;
|
||||||
private KeycloakSession session;
|
|
||||||
|
|
||||||
public WelcomeResource() {
|
@Context
|
||||||
}
|
KeycloakSession session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Welcome page of Keycloak
|
* Welcome page of Keycloak
|
||||||
|
@ -82,8 +84,6 @@ public class WelcomeResource {
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
@Produces(MediaType.TEXT_HTML_UTF_8)
|
||||||
public Response getWelcomePage() throws URISyntaxException {
|
public Response getWelcomePage() throws URISyntaxException {
|
||||||
checkBootstrap();
|
|
||||||
|
|
||||||
String requestUri = session.getContext().getUri().getRequestUri().toString();
|
String requestUri = session.getContext().getUri().getRequestUri().toString();
|
||||||
if (!requestUri.endsWith("/")) {
|
if (!requestUri.endsWith("/")) {
|
||||||
return Response.seeOther(new URI(requestUri + "/")).build();
|
return Response.seeOther(new URI(requestUri + "/")).build();
|
||||||
|
@ -95,8 +95,9 @@ public class WelcomeResource {
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
@Produces(MediaType.TEXT_HTML_UTF_8)
|
||||||
public Response createUser(final MultivaluedMap<String, String> formData) {
|
public Response createUser() {
|
||||||
checkBootstrap();
|
HttpRequest request = session.getContext().getHttpRequest();
|
||||||
|
MultivaluedMap<String, String> formData = request.getDecodedFormParameters();
|
||||||
|
|
||||||
if (!shouldBootstrap()) {
|
if (!shouldBootstrap()) {
|
||||||
return createWelcomePage(null, null);
|
return createWelcomePage(null, null);
|
||||||
|
@ -131,16 +132,11 @@ public class WelcomeResource {
|
||||||
expireCsrfCookie();
|
expireCsrfCookie();
|
||||||
|
|
||||||
ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
|
ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
|
||||||
if (applianceBootstrap.isNoMasterUser()) {
|
applianceBootstrap.createMasterRealmUser(username, password);
|
||||||
setBootstrap(false);
|
|
||||||
applianceBootstrap.createMasterRealmUser(username, password);
|
|
||||||
|
|
||||||
ServicesLogger.LOGGER.createdInitialAdminUser(username);
|
shouldBootstrap.set(false);
|
||||||
return createWelcomePage("User created", null);
|
ServicesLogger.LOGGER.createdInitialAdminUser(username);
|
||||||
} else {
|
return createWelcomePage("User created", null);
|
||||||
ServicesLogger.LOGGER.initialUserAlreadyCreated();
|
|
||||||
return createWelcomePage(null, "Users already exists");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +160,7 @@ public class WelcomeResource {
|
||||||
return Response.status(Response.Status.NOT_FOUND).build();
|
return Response.status(Response.Status.NOT_FOUND).build();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
|
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,6 +168,15 @@ public class WelcomeResource {
|
||||||
try {
|
try {
|
||||||
Theme theme = getTheme();
|
Theme theme = getTheme();
|
||||||
|
|
||||||
|
if(Objects.isNull(theme)) {
|
||||||
|
logger.error("Theme is null please check the \"--spi-theme-default\" parameter");
|
||||||
|
errorMessage = "The theme is null";
|
||||||
|
ResponseBuilder rb = Response.status(Status.BAD_REQUEST)
|
||||||
|
.entity(errorMessage)
|
||||||
|
.cacheControl(CacheControlUtil.noCache());
|
||||||
|
return rb.build();
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> map = new HashMap<>();
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
|
||||||
map.put("adminConsoleEnabled", isAdminConsoleEnabled());
|
map.put("adminConsoleEnabled", isAdminConsoleEnabled());
|
||||||
|
@ -179,6 +184,7 @@ public class WelcomeResource {
|
||||||
|
|
||||||
map.put("properties", theme.getProperties());
|
map.put("properties", theme.getProperties());
|
||||||
map.put("adminUrl", session.getContext().getUri(UrlType.ADMIN).getBaseUriBuilder().path("/admin/").build());
|
map.put("adminUrl", session.getContext().getUri(UrlType.ADMIN).getBaseUriBuilder().path("/admin/").build());
|
||||||
|
|
||||||
map.put("resourcesPath", "resources/" + Version.RESOURCES_VERSION + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName());
|
map.put("resourcesPath", "resources/" + Version.RESOURCES_VERSION + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName());
|
||||||
map.put("resourcesCommonPath", "resources/" + Version.RESOURCES_VERSION + "/common/keycloak");
|
map.put("resourcesCommonPath", "resources/" + Version.RESOURCES_VERSION + "/common/keycloak");
|
||||||
|
|
||||||
|
@ -187,8 +193,11 @@ public class WelcomeResource {
|
||||||
if (bootstrap) {
|
if (bootstrap) {
|
||||||
boolean isLocal = isLocal();
|
boolean isLocal = isLocal();
|
||||||
map.put("localUser", isLocal);
|
map.put("localUser", isLocal);
|
||||||
map.put("localAdminUrl", "http://localhost:8080/auth");
|
|
||||||
map.put("adminUserCreationMessage","or use the add-user-keycloak script");
|
String localAdminUrl = session.getContext().getUri(UrlType.LOCAL_ADMIN).getBaseUri().toString();
|
||||||
|
String adminCreationMessage = getAdminCreationMessage();
|
||||||
|
map.put("localAdminUrl", localAdminUrl);
|
||||||
|
map.put("adminUserCreationMessage", adminCreationMessage);
|
||||||
|
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
String stateChecker = setCsrfCookie();
|
String stateChecker = setCsrfCookie();
|
||||||
|
@ -209,7 +218,7 @@ public class WelcomeResource {
|
||||||
.cacheControl(CacheControlUtil.noCache());
|
.cacheControl(CacheControlUtil.noCache());
|
||||||
return rb.build();
|
return rb.build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
|
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,21 +230,23 @@ public class WelcomeResource {
|
||||||
try {
|
try {
|
||||||
return session.theme().getTheme(Theme.Type.WELCOME);
|
return session.theme().getTheme(Theme.Type.WELCOME);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
|
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkBootstrap() {
|
protected String getAdminCreationMessage() {
|
||||||
if (shouldBootstrap())
|
return "or set the environment variables KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD before starting the server";
|
||||||
KeycloakApplication.BOOTSTRAP_ADMIN_USER.compareAndSet(true, new ApplianceBootstrap(session).isNoMasterUser());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldBootstrap() {
|
private boolean shouldBootstrap() {
|
||||||
return KeycloakApplication.BOOTSTRAP_ADMIN_USER.get();
|
if (shouldBootstrap == null) {
|
||||||
}
|
synchronized (this) {
|
||||||
|
if (shouldBootstrap == null) {
|
||||||
private void setBootstrap(boolean value) {
|
shouldBootstrap = new AtomicBoolean(new ApplianceBootstrap(session).isNoMasterUser());
|
||||||
KeycloakApplication.BOOTSTRAP_ADMIN_USER.set(value);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shouldBootstrap.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLocal() {
|
private boolean isLocal() {
|
||||||
|
@ -243,14 +254,16 @@ public class WelcomeResource {
|
||||||
ClientConnection clientConnection = session.getContext().getConnection();
|
ClientConnection clientConnection = session.getContext().getConnection();
|
||||||
InetAddress remoteInetAddress = InetAddress.getByName(clientConnection.getRemoteAddr());
|
InetAddress remoteInetAddress = InetAddress.getByName(clientConnection.getRemoteAddr());
|
||||||
InetAddress localInetAddress = InetAddress.getByName(clientConnection.getLocalAddr());
|
InetAddress localInetAddress = InetAddress.getByName(clientConnection.getLocalAddr());
|
||||||
String xForwardedFor = session.getContext().getRequestHeaders().getHeaderString("X-Forwarded-For");
|
HttpRequest request = session.getContext().getHttpRequest();
|
||||||
|
HttpHeaders headers = request.getHttpHeaders();
|
||||||
|
String xForwardedFor = headers.getHeaderString("X-Forwarded-For");
|
||||||
logger.debugf("Checking WelcomePage. Remote address: %s, Local address: %s, X-Forwarded-For header: %s", remoteInetAddress.toString(), localInetAddress.toString(), xForwardedFor);
|
logger.debugf("Checking WelcomePage. Remote address: %s, Local address: %s, X-Forwarded-For header: %s", remoteInetAddress.toString(), localInetAddress.toString(), xForwardedFor);
|
||||||
|
|
||||||
// Access through AJP protocol (loadbalancer) may cause that remoteAddress is "127.0.0.1".
|
// Access through AJP protocol (loadbalancer) may cause that remoteAddress is "127.0.0.1".
|
||||||
// So consider that welcome page accessed locally just if it was accessed really through "localhost" URL and without loadbalancer (x-forwarded-for header is empty).
|
// So consider that welcome page accessed locally just if it was accessed really through "localhost" URL and without loadbalancer (x-forwarded-for header is empty).
|
||||||
return isLocalAddress(remoteInetAddress) && isLocalAddress(localInetAddress) && xForwardedFor == null;
|
return isLocalAddress(remoteInetAddress) && isLocalAddress(localInetAddress) && xForwardedFor == null;
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
|
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,7 +287,9 @@ public class WelcomeResource {
|
||||||
|
|
||||||
private void csrfCheck(final MultivaluedMap<String, String> formData) {
|
private void csrfCheck(final MultivaluedMap<String, String> formData) {
|
||||||
String formStateChecker = formData.getFirst("stateChecker");
|
String formStateChecker = formData.getFirst("stateChecker");
|
||||||
Cookie cookie = session.getContext().getRequestHeaders().getCookies().get(KEYCLOAK_STATE_CHECKER);
|
HttpRequest request = session.getContext().getHttpRequest();
|
||||||
|
HttpHeaders headers = request.getHttpHeaders();
|
||||||
|
Cookie cookie = headers.getCookies().get(KEYCLOAK_STATE_CHECKER);
|
||||||
if (cookie == null) {
|
if (cookie == null) {
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ public class DefaultHostnameProvider implements HostnameProvider {
|
||||||
|
|
||||||
private URI adminUri;
|
private URI adminUri;
|
||||||
|
|
||||||
|
private URI localAdminUri = URI.create("http://localhost:8080/auth");
|
||||||
|
|
||||||
private final boolean forceBackendUrlToFrontendUrl;
|
private final boolean forceBackendUrlToFrontendUrl;
|
||||||
|
|
||||||
public DefaultHostnameProvider(KeycloakSession session, URI frontendUri, URI adminUri, boolean forceBackendUrlToFrontendUrl) {
|
public DefaultHostnameProvider(KeycloakSession session, URI frontendUri, URI adminUri, boolean forceBackendUrlToFrontendUrl) {
|
||||||
|
@ -57,6 +59,10 @@ public class DefaultHostnameProvider implements HostnameProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private URI resolveUri(UriInfo originalUriInfo, UrlType type) {
|
private URI resolveUri(UriInfo originalUriInfo, UrlType type) {
|
||||||
|
if (type.equals(UrlType.LOCAL_ADMIN)) {
|
||||||
|
return localAdminUri;
|
||||||
|
}
|
||||||
|
|
||||||
URI realmUri = getRealmUri();
|
URI realmUri = getRealmUri();
|
||||||
URI frontendUri = realmUri != null ? realmUri : this.frontendUri;
|
URI frontendUri = realmUri != null ? realmUri : this.frontendUri;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue