Skip processing HEAD requests for action tokens

Closes #41834

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor
2025-08-19 09:42:33 -03:00
parent 0ff7d551dd
commit c7fedb77e3
3 changed files with 50 additions and 3 deletions

View File

@@ -30,6 +30,8 @@ import jakarta.ws.rs.core.MultivaluedMap;
import java.util.Collections;
import java.util.Map;
import static jakarta.ws.rs.HttpMethod.HEAD;
import static jakarta.ws.rs.HttpMethod.OPTIONS;
import static org.keycloak.models.BrowserSecurityHeaders.CONTENT_SECURITY_POLICY;
public class DefaultSecurityHeadersProvider implements SecurityHeadersProvider {
@@ -147,10 +149,17 @@ public class DefaultSecurityHeadersProvider implements SecurityHeadersProvider {
status == 400 || status == 401 || status == 403 || status == 404) {
return true;
}
if (requestContext.getMethod().equalsIgnoreCase("OPTIONS")) {
return true;
String method = requestContext.getMethod().toUpperCase();
switch (method) {
case OPTIONS:
return true;
case HEAD:
return status == 200;
}
}
return false;
}

View File

@@ -16,6 +16,7 @@
*/
package org.keycloak.services.resources;
import jakarta.ws.rs.HEAD;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
@@ -548,6 +549,19 @@ public class LoginActionsService {
return handleActionToken(key, execution, clientId, tabId, clientData, null);
}
/**
* Skip processing {@link jakarta.ws.rs.HttpMethod#HEAD} requests for action tokens
* as they are usually used by mail servers to validate links. The actual request will eventually be
* processed by the {@link #executeActionToken} method.
*
* @return a {@link Response.Status#OK} response with no message body
*/
@Path("action-token")
@HEAD
public Response executeActionTokenHead() {
return Response.ok().build();
}
protected <T extends JsonWebToken & SingleUseObjectKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId, String clientData,
TriFunction<ActionTokenHandler<T>, T, ActionTokenContext<T>, Response> preHandleToken) {
T token;

View File

@@ -30,8 +30,10 @@ import java.io.IOException;
import java.util.List;
import java.util.UUID;
import jakarta.ws.rs.core.Response.Status;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jboss.arquillian.graphene.page.Page;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
@@ -39,6 +41,7 @@ import org.keycloak.admin.client.resource.AuthenticationManagementResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.actiontoken.updateemail.UpdateEmailActionToken;
import org.keycloak.authentication.requiredactions.UpdateEmail;
import org.keycloak.broker.provider.util.SimpleHttp.Response;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
@@ -48,6 +51,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.testsuite.broker.util.SimpleHttpDefault;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.util.GreenMailRule;
@@ -174,6 +178,26 @@ public class RequiredActionUpdateEmailTestWithVerificationTest extends AbstractR
assertTrue(ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost").getRequiredActions().contains(UserModel.RequiredAction.UPDATE_EMAIL.name()));
}
@Test
public void testSkipHeadRequestWhenFollowingVerificationLink() throws MessagingException, IOException {
oauth.openLoginForm();
loginPage.login("test-user@localhost", "password");
updateEmailPage.assertCurrent();
updateEmailPage.changeEmail("new@localhost");
String confirmationLink = fetchEmailConfirmationLink("new@localhost");
try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
try (Response response = SimpleHttpDefault.doHead(confirmationLink, httpClient).asResponse()) {
assertEquals(Status.OK.getStatusCode(), response.getStatus());
}
}
driver.navigate().to(confirmationLink);
infoPage.assertCurrent();
}
@Test
public void testForceEmailVerification() throws MessagingException, IOException {
// disables verify email at the realm level