diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 542204570b..7cf3649c02 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -118,6 +118,8 @@ public class SamlProtocol implements LoginProtocol { public static final String SAML_PERSISTENT_NAME_ID_FOR = "saml.persistent.name.id.for"; public static final String SAML_IDP_INITIATED_SSO_RELAY_STATE = "saml_idp_initiated_sso_relay_state"; public static final String SAML_IDP_INITIATED_SSO_URL_NAME = "saml_idp_initiated_sso_url_name"; + public static final String SAML_LOGIN_REQUEST_FORCEAUTHN = "SAML_LOGIN_REQUEST_FORCEAUTHN"; + public static final String SAML_FORCEAUTHN_REQUIREMENT = "true"; protected KeycloakSession session; @@ -726,8 +728,8 @@ public class SamlProtocol implements LoginProtocol { @Override public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) { - // Not yet supported - return false; + String requireReauthentication = authSession.getAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN); + return Objects.equals(SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT, requireReauthentication); } private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient) { diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java index 28ae013c7c..430c37065f 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -352,6 +352,11 @@ public class SamlService extends AuthorizationEndpointBase { } } + + if (null != requestAbstractType.isForceAuthn() + && requestAbstractType.isForceAuthn()) { + authSession.setAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN, SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT); + } //If unset we fall back to default "false" final boolean isPassive = (null == requestAbstractType.isIsPassive() ? false : requestAbstractType.isIsPassive().booleanValue()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java index 63ec8828c9..8b79daa3b4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java @@ -199,7 +199,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration { config.put(SINGLE_SIGN_ON_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml"); config.put(SINGLE_LOGOUT_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml"); config.put(NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); - config.put(FORCE_AUTHN, "true"); + config.put(FORCE_AUTHN, "false"); config.put(POST_BINDING_RESPONSE, "true"); config.put(POST_BINDING_AUTHN_REQUEST, "true"); config.put(VALIDATE_SIGNATURE, "false"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java index 774c0cb934..98abaa44c6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/BasicSamlTest.java @@ -5,6 +5,7 @@ import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.saml.SignatureAlgorithm; import org.keycloak.saml.common.constants.GeneralConstants; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ProcessingException; @@ -13,12 +14,19 @@ import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.saml.processing.web.util.RedirectBindingUtil; import org.keycloak.services.resources.RealmsResource; import org.keycloak.testsuite.util.KeyUtils; +import org.keycloak.testsuite.util.Matchers; import org.keycloak.testsuite.util.SamlClient; import org.keycloak.testsuite.util.SamlClient.Binding; import org.keycloak.testsuite.util.SamlClient.RedirectStrategyWithSwitchableFollowRedirect; +import org.keycloak.testsuite.util.SamlClient.Step; import org.keycloak.testsuite.util.SamlClientBuilder; +import java.io.IOException; import java.net.URI; import java.security.Signature; +import java.util.List; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriBuilder; @@ -177,4 +185,57 @@ public class BasicSamlTest extends AbstractSamlTest { assertThat(EntityUtils.toString(response.getEntity(), "UTF-8"), pageTextMatcher); } } + + @Test + public void testReauthnWithForceAuthnNotSet() throws Exception { + testReauthnWithForceAuthn(null); + } + + @Test + public void testReauthnWithForceAuthnFalse() throws Exception { + testReauthnWithForceAuthn(false); + } + + @Test + public void testReauthnWithForceAuthnTrue() throws Exception { + testReauthnWithForceAuthn(true); + } + + private void testReauthnWithForceAuthn(Boolean reloginRequired) throws Exception { + // Ensure that the first authentication passes + SamlClient samlClient = new SamlClientBuilder() + // First authn + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, Binding.POST) + .build() + + .login().user(bburkeUser).build() + + .execute(hr -> { + try { + SAMLDocumentHolder doc = Binding.POST.extractResponse(hr); + assertThat(doc.getSamlObject(), Matchers.isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + } catch (IOException ex) { + Logger.getLogger(BasicSamlTest.class.getName()).log(Level.SEVERE, null, ex); + } + }); + + List secondAuthn = new SamlClientBuilder() + // Second authn with forceAuth not set (SSO) + .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, Binding.POST) + .transformObject(so -> { + so.setForceAuthn(reloginRequired); + return so; + }) + .build() + + .assertResponse(Matchers.bodyHC(containsString( + Objects.equals(reloginRequired, Boolean.TRUE) + ? "Log in" + : GeneralConstants.SAML_RESPONSE_KEY + ))) + + .getSteps(); + + samlClient.execute(secondAuthn); + } }