KEYCLOAK-5657 Support for transient NameIDPolicy and AllowCreate in SAML IdP

This commit is contained in:
Konstantinos Georgilakis 2021-02-12 16:32:24 +02:00 committed by Hynek Mlnařík
parent 0a0caa07d6
commit ec5c256562
6 changed files with 141 additions and 8 deletions

View file

@ -130,6 +130,9 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
requestedAuthnContext.addAuthnContextDeclRef(authnContextDeclRef);
String loginHint = getConfig().isLoginHint() ? request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM) : null;
Boolean allowCreate = null;
if (getConfig().getConfig().get(SAMLIdentityProviderConfig.ALLOW_CREATE) == null || getConfig().isAllowCreate())
allowCreate = Boolean.TRUE;
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
.assertionConsumerUrl(assertionConsumerServiceUrl)
.destination(destinationUrl)
@ -138,7 +141,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
.protocolBinding(protocolBinding)
.nameIdPolicy(SAML2NameIDPolicyBuilder
.format(nameIDPolicyFormat)
.setAllowCreate(Boolean.TRUE))
.setAllowCreate(allowCreate))
.requestedAuthnContext(requestedAuthnContext)
.subject(loginHint);

View file

@ -24,6 +24,7 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.saml.SamlPrincipalType;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
/**
@ -58,6 +59,7 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
public static final String AUTHN_CONTEXT_CLASS_REFS = "authnContextClassRefs";
public static final String AUTHN_CONTEXT_DECL_REFS = "authnContextDeclRefs";
public static final String SIGN_SP_METADATA = "signSpMetadata";
public static final String ALLOW_CREATE = "allowCreate";
public SAMLIdentityProviderConfig() {
}
@ -335,11 +337,23 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
getConfig().put(SIGN_SP_METADATA, String.valueOf(signSpMetadata));
}
public boolean isAllowCreate() {
return Boolean.valueOf(getConfig().get(ALLOW_CREATE));
}
public void setAllowCreated(boolean allowCreate) {
getConfig().put(ALLOW_CREATE, String.valueOf(allowCreate));
}
@Override
public void validate(RealmModel realm) {
SslRequired sslRequired = realm.getSslRequired();
checkUrl(sslRequired, getSingleLogoutServiceUrl(), SINGLE_LOGOUT_SERVICE_URL);
checkUrl(sslRequired, getSingleSignOnServiceUrl(), SINGLE_SIGN_ON_SERVICE_URL);
//transient name id format is not accepted together with principaltype SubjectnameId
if (JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get().equals(getNameIDPolicyFormat()) && SamlPrincipalType.SUBJECT == getPrincipalType())
throw new IllegalArgumentException("Can not have Transient NameID Policy Format together with SUBJECT Principal Type");
}
}

View file

@ -10,6 +10,7 @@ import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.dom.saml.v2.assertion.AudienceRestrictionType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.saml.SamlPrincipalType;
@ -61,7 +62,10 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_CONS_NAME;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
@ -122,6 +126,7 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
public void resetPrincipalType() {
IdentityProviderResource idp = adminClient.realm(REALM_CONS_NAME).identityProviders().get("saml-leaf");
IdentityProviderRepresentation rep = idp.toRepresentation();
rep.getConfig().put(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get());
rep.getConfig().put(SAMLIdentityProviderConfig.PRINCIPAL_TYPE, SamlPrincipalType.SUBJECT.name());
idp.update(rep);
}
@ -397,6 +402,109 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
assertThat(fed.getUserName(), is(PROVIDER_REALM_USER_NAME));
}
@Test
public void testProviderTransientIdpInitiatedLogin() throws Exception {
IdentityProviderResource idp = adminClient.realm(REALM_CONS_NAME).identityProviders().get("saml-leaf");
IdentityProviderRepresentation rep = idp.toRepresentation();
rep.getConfig().put(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get());
rep.getConfig().put(SAMLIdentityProviderConfig.PRINCIPAL_TYPE, SamlPrincipalType.ATTRIBUTE.name());
rep.getConfig().put(SAMLIdentityProviderConfig.PRINCIPAL_ATTRIBUTE, X500SAMLProfileConstants.UID.get());
idp.update(rep);
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
.navigateTo(getSamlIdpInitiatedUrl(REALM_PROV_NAME, "samlbroker"))
// Login in provider realm
.login().user(PROVIDER_REALM_USER_NAME, PROVIDER_REALM_USER_PASSWORD).build()
// Send the response to the consumer realm
.processSamlResponse(Binding.POST)
.transformObject(ob -> {
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) ob;
assertThat(resp.getDestination(), is(getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales")));
assertAudience(resp, getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales"));
NameIDType nameId = new NameIDType();
nameId.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()));
nameId.setValue("subjectId1" );
resp.getAssertions().get(0).getAssertion().getSubject().getSubType().addBaseID(nameId);
Set<StatementAbstractType> statements = resp.getAssertions().get(0).getAssertion().getStatements();
AttributeStatementType attributeType = (AttributeStatementType) statements.stream()
.filter(statement -> statement instanceof AttributeStatementType).findFirst()
.orElse(new AttributeStatementType());
AttributeType attr = new AttributeType(X500SAMLProfileConstants.UID.get());
attr.addAttributeValue(PROVIDER_REALM_USER_NAME);
attributeType.addAttribute(new AttributeStatementType.ASTChoiceType(attr));
resp.getAssertions().get(0).getAssertion().addStatement(attributeType);
return ob;
})
.build()
// Now login to the second app
.navigateTo(getSamlIdpInitiatedUrl(REALM_PROV_NAME, "samlbroker-2"))
// Login in provider realm
.login().sso(true).build()
.processSamlResponse(Binding.POST)
.transformObject(ob -> {
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) ob;
assertThat(resp.getDestination(), is(getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales2")));
assertAudience(resp, getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales2"));
NameIDType nameId = new NameIDType();
nameId.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()));
nameId.setValue("subjectId2" );
resp.getAssertions().get(0).getAssertion().getSubject().getSubType().addBaseID(nameId);
Set<StatementAbstractType> statements = resp.getAssertions().get(0).getAssertion().getStatements();
AttributeStatementType attributeType = (AttributeStatementType) statements.stream()
.filter(statement -> statement instanceof AttributeStatementType).findFirst()
.orElse(new AttributeStatementType());
AttributeType attr = new AttributeType(X500SAMLProfileConstants.UID.get());
attr.addAttributeValue(PROVIDER_REALM_USER_NAME);
attributeType.addAttribute(new AttributeStatementType.ASTChoiceType(attr));
resp.getAssertions().get(0).getAssertion().addStatement(attributeType);
return ob;
})
.build()
.updateProfile().username(CONSUMER_CHOSEN_USERNAME).email("test@localhost").firstName("Firstname").lastName("Lastname").build()
.followOneRedirect()
// Obtain the response sent to the app
.getSamlResponse(Binding.POST);
assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
ResponseType resp = (ResponseType) samlResponse.getSamlObject();
assertThat(resp.getDestination(), is(urlRealmConsumer + "/app/auth2/saml"));
assertAudience(resp, urlRealmConsumer + "/app/auth2");
UsersResource users = adminClient.realm(REALM_CONS_NAME).users();
List<UserRepresentation> userList= users.search(CONSUMER_CHOSEN_USERNAME);
assertEquals(1, userList.size());
String id = userList.get(0).getId();
FederatedIdentityRepresentation fed = users.get(id).getFederatedIdentity().get(0);
assertThat(fed.getUserId(), is(PROVIDER_REALM_USER_NAME));
assertThat(fed.getUserName(), is(PROVIDER_REALM_USER_NAME));
//check that no user with sent subject-id was sent
userList = users.search("subjectId1");
assertTrue(userList.isEmpty());
userList = users.search("subjectId2");
assertTrue(userList.isEmpty());
}
private void assertSingleUserSession(String realmName, String userName, String... expectedClientIds) {
final UsersResource users = adminClient.realm(realmName).users();
final ClientsResource clients = adminClient.realm(realmName).clients();

View file

@ -712,6 +712,8 @@ saml.principal-type=Principal Type
saml.principal-type.tooltip=Way to identify and track external users from the assertion. Default is using Subject NameID, alternatively you can set up identifying attribute.
saml.principal-attribute=Principal Attribute
saml.principal-attribute.tooltip=Name or Friendly Name of the attribute used to identify external users.
saml.allow-create=Allow create
saml.allow-create.tooltip=Allow the external identity provider to create a new identifier to represent the principal
http-post-binding-response=HTTP-POST Binding Response
http-post-binding-response.tooltip=Indicates whether to respond to requests using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used.
http-post-binding-for-authn-request=HTTP-POST Binding for AuthnRequest

View file

@ -935,17 +935,15 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
$scope.initSamlProvider = function() {
$scope.nameIdFormats = [
/*
{
format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
name: "Transient"
},
*/
{
format: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
name: "Persistent"
},
{
format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
name: "Transient"
},
{
format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
name: "Email"
@ -1009,6 +1007,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
$scope.identityProvider.config.principalType = $scope.principalTypes[0].type;
$scope.identityProvider.config.signatureAlgorithm = $scope.signatureAlgorithms[1];
$scope.identityProvider.config.xmlSigKeyInfoKeyNameTransformer = $scope.xmlKeyNameTranformers[1];
$scope.identityProvider.config.allowCreate = 'true';
}
$scope.identityProvider.config.entityId = $scope.identityProvider.config.entityId || (authUrl + '/realms/' + realm.realm);
}

View file

@ -186,6 +186,13 @@
<input class="form-control" id="principalAttribute" type="text" ng-model="identityProvider.config.principalAttribute" ng-required="identityProvider.config.principalType.endsWith('ATTRIBUTE')">
</div>
<kc-tooltip>{{:: 'saml.principal-attribute.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="allowCreate">{{:: 'saml.allow-create' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.config.allowCreate" id="allowCreate" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'saml.allow-create.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="postBindingResponse">{{:: 'http-post-binding-response' | translate}}</label>