KEYCLOAK-5657 Support for transient NameIDPolicy and AllowCreate in SAML IdP
This commit is contained in:
parent
0a0caa07d6
commit
ec5c256562
6 changed files with 141 additions and 8 deletions
|
@ -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);
|
||||
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
@ -334,6 +336,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
|||
public void setSignSpMetadata(boolean signSpMetadata) {
|
||||
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) {
|
||||
|
@ -341,5 +351,9 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
|||
|
||||
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");
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -396,6 +401,109 @@ public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
|
|||
assertThat(fed.getUserId(), is(PROVIDER_REALM_USER_NAME));
|
||||
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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
@ -1008,7 +1006,8 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
|
|||
$scope.identityProvider.config.nameIDPolicyFormat = $scope.nameIdFormats[0].format;
|
||||
$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.xmlSigKeyInfoKeyNameTransformer = $scope.xmlKeyNameTranformers[1];
|
||||
$scope.identityProvider.config.allowCreate = 'true';
|
||||
}
|
||||
$scope.identityProvider.config.entityId = $scope.identityProvider.config.entityId || (authUrl + '/realms/' + realm.realm);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue