KEYCLOAK-10802 add support of SAMLv2 ForceAuthn

This commit is contained in:
Mathieu CLAUDEL 2019-09-04 08:29:35 +02:00 committed by Hynek Mlnařík
parent 6283c7add3
commit 2fb507e170
4 changed files with 71 additions and 3 deletions

View file

@ -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_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_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_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; protected KeycloakSession session;
@ -726,8 +728,8 @@ public class SamlProtocol implements LoginProtocol {
@Override @Override
public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) { public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) {
// Not yet supported String requireReauthentication = authSession.getAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN);
return false; return Objects.equals(SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT, requireReauthentication);
} }
private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient) { private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient) {

View file

@ -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" //If unset we fall back to default "false"
final boolean isPassive = (null == requestAbstractType.isIsPassive() ? final boolean isPassive = (null == requestAbstractType.isIsPassive() ?
false : requestAbstractType.isIsPassive().booleanValue()); false : requestAbstractType.isIsPassive().booleanValue());

View file

@ -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_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(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(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_RESPONSE, "true");
config.put(POST_BINDING_AUTHN_REQUEST, "true"); config.put(POST_BINDING_AUTHN_REQUEST, "true");
config.put(VALIDATE_SIGNATURE, "false"); config.put(VALIDATE_SIGNATURE, "false");

View file

@ -5,6 +5,7 @@ import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.saml.SignatureAlgorithm; import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants; 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.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException; 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.saml.processing.web.util.RedirectBindingUtil;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.util.KeyUtils; import org.keycloak.testsuite.util.KeyUtils;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.SamlClient; import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClient.Binding; import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClient.RedirectStrategyWithSwitchableFollowRedirect; import org.keycloak.testsuite.util.SamlClient.RedirectStrategyWithSwitchableFollowRedirect;
import org.keycloak.testsuite.util.SamlClient.Step;
import org.keycloak.testsuite.util.SamlClientBuilder; import org.keycloak.testsuite.util.SamlClientBuilder;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.security.Signature; 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;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
@ -177,4 +185,57 @@ public class BasicSamlTest extends AbstractSamlTest {
assertThat(EntityUtils.toString(response.getEntity(), "UTF-8"), pageTextMatcher); 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<Step> 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);
}
} }