8000 [26.8] Saml ForceAuthn requirement fix by lhanusov · Pull Request #55047 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,7 @@ protected LogoutRequestType createLogoutRequest(String logoutUrl, AuthenticatedC

@Override
public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) {
String requireReauthentication = authSession.getAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN);
String requireReauthentication = authSession.getClientNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN);
return Objects.equals(SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT, requireReauthentication);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,8 @@ protected Response loginRequest(String relayState, AuthnRequestType requestAbstr

if (null != requestAbstractType.isForceAuthn()
&& requestAbstractType.isForceAuthn()) {
authSession.setAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN, SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT);
// The request's reauthentication requirement must survive authentication flow restarts.
authSession.setClientNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN, SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT);
}

for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
import jakarta.ws.rs.core.UriBuilder;

import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.util.PemUtils;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.models.Constants;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlProtocolUtils;
Expand All @@ -33,7 +35,9 @@
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
impo 8000 rt org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.admin.AdminApiUtil;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.KeyUtils;
import org.keycloak.testsuite.util.Matchers;
Expand All @@ -43,10 +47,12 @@
import org.keycloak.testsuite.util.SamlClient.Step;
import org.keycloak.testsuite.util.SamlClientBuilder;
import org.keycloak.testsuite.util.SamlUtils;
import org.keycloak.testsuite.util.saml.LoginBuilder;
import org.keycloak.utils.StringUtil;

import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
Expand Down Expand Up @@ -235,6 +241,74 @@ public void testReauthnWithForceAuthnTrue() throws Exception {
testReauthnWithForceAuthn(true);
}

@Test
public void testForceAuthnAfterRestartPost() {
testForceAuthnAfterRestart(Binding.POST);
}

@Test
public void testForceAuthnAfterRestartRedirect() {
testForceAuthnAfterRestart(Binding.REDIRECT);
}

private void testForceAuthnAfterRestart(Binding requestBinding) {
Step assertSamlSuccess = (client, currentURI, currentResponse, context) -> {
assertThat(Binding.POST.extractResponse(currentResponse).getSamlObject(),
Matchers.isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
return null;
};
Step restartLogin = (client, currentURI, currentResponse, context) -> {
assertThat(currentResponse, statusCodeIsHC(Status.OK));
String loginPage = EntityUtils.toString(currentResponse.getEntity(), StandardCharsets.UTF_8);
assertThat("ForceAuthn must require credentials after every restart", loginPage, containsString("id=\"password\""));
URI loginAction = URI.create(SamlClient.extractFormFromPostResponse(loginPage).attr("action"));
return new HttpGet(UriBuilder.fromUri(loginAction)
.replacePath(loginAction.getPath().substring(0, loginAction.getPath().lastIndexOf('/') + 1) + LoginActionsService.RESTART_PATH)
.replaceQueryParam("session_code")
.replaceQueryParam("execution")
.replaceQueryParam(Constants.SKIP_LOGOUT, "true")
.build());
};

// Keep the same client's cookies throughout the initial login and reauthentication.
SamlClient samlClient = new SamlClientBuilder()
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, requestBinding)
.setProtocolBinding(Binding.POST.getBindingUri())
.build()
.login().user(bburkeUser).build()
.addStep(assertSamlSuccess)
.execute();

UserResource user = AdminApiUtil.findUserByUsernameId(adminClient.realm(REALM_NAME), bburkeUser.getUsername());
String userSessionId = user.getUserSessions().get(0).getId();

samlClient.execute(new SamlClientBuilder()
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, requestBinding)
.setProtocolBinding(Binding.POST.getBindingUri())
.transformObject(request -> {
request.setForceAuthn(true);
return request;
})
.build()
.addStep(restartLogin)
.addStep(restartLogin)
.addStep((client, currentURI, currentResponse, context) -> {
assertThat(currentResponse, statusCodeIsHC(Status.OK));
String loginPage = EntityUtils.toString(currentResponse.getEntity(), StandardCharsets.UTF_8);
assertThat("ForceAuthn must still require credentials after repeated restarts", loginPage, containsString("id=\"password\""));
assertThat("Restarting with skip_logout=true must preserve the original SSO session",
user.getUserSessions().stream().anyMatch(userSession -> userSessionId.equals(userSession.getId())), is(true));
return LoginBuilder.handleLoginPage(bburkeUser, loginPage);
})
.addStep(assertSamlSuccess)
// A subsequent request without ForceAuthn must still be able to use SSO.
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, requestBinding)
.setProtocolBinding(Binding.POST.getBindingUri())
.build()
.addStep(assertSamlSuccess)
.getSteps());
}

private void testReauthnWithForceAuthn(Boolean reloginRequired) throws Exception {
// Ensure that the first authentication passes
SamlClient samlClient = new SamlClientBuilder()
Expand Down
Loading
0