Skip to content

Commit ec5c256

Browse files
cgeorgilakishmlnarik
authored andcommitted
KEYCLOAK-5657 Support for transient NameIDPolicy and AllowCreate in SAML IdP
1 parent 0a0caa0 commit ec5c256

File tree

6 files changed

+141
-8
lines changed

6 files changed

+141
-8
lines changed

services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ public Response performLogin(AuthenticationRequest request) {
130130
requestedAuthnContext.addAuthnContextDeclRef(authnContextDeclRef);
131131

132132
String loginHint = getConfig().isLoginHint() ? request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM) : null;
133+
Boolean allowCreate = null;
134+
if (getConfig().getConfig().get(SAMLIdentityProviderConfig.ALLOW_CREATE) == null || getConfig().isAllowCreate())
135+
allowCreate = Boolean.TRUE;
133136
SAML2AuthnRequestBuilder authnRequestBuilder = new SAML2AuthnRequestBuilder()
134137
.assertionConsumerUrl(assertionConsumerServiceUrl)
135138
.destination(destinationUrl)
@@ -138,7 +141,7 @@ public Response performLogin(AuthenticationRequest request) {
138141
.protocolBinding(protocolBinding)
139142
.nameIdPolicy(SAML2NameIDPolicyBuilder
140143
.format(nameIDPolicyFormat)
141-
.setAllowCreate(Boolean.TRUE))
144+
.setAllowCreate(allowCreate))
142145
.requestedAuthnContext(requestedAuthnContext)
143146
.subject(loginHint);
144147

services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.keycloak.models.KeycloakSession;
2525
import org.keycloak.models.RealmModel;
2626
import org.keycloak.protocol.saml.SamlPrincipalType;
27+
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
2728
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
2829

2930
/**
@@ -58,6 +59,7 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
5859
public static final String AUTHN_CONTEXT_CLASS_REFS = "authnContextClassRefs";
5960
public static final String AUTHN_CONTEXT_DECL_REFS = "authnContextDeclRefs";
6061
public static final String SIGN_SP_METADATA = "signSpMetadata";
62+
public static final String ALLOW_CREATE = "allowCreate";
6163

6264
public SAMLIdentityProviderConfig() {
6365
}
@@ -334,12 +336,24 @@ public boolean isSignSpMetadata() {
334336
public void setSignSpMetadata(boolean signSpMetadata) {
335337
getConfig().put(SIGN_SP_METADATA, String.valueOf(signSpMetadata));
336338
}
339+
340+
public boolean isAllowCreate() {
341+
return Boolean.valueOf(getConfig().get(ALLOW_CREATE));
342+
}
343+
344+
public void setAllowCreated(boolean allowCreate) {
345+
getConfig().put(ALLOW_CREATE, String.valueOf(allowCreate));
346+
}
337347

338348
@Override
339349
public void validate(RealmModel realm) {
340350
SslRequired sslRequired = realm.getSslRequired();
341351

342352
checkUrl(sslRequired, getSingleLogoutServiceUrl(), SINGLE_LOGOUT_SERVICE_URL);
343353
checkUrl(sslRequired, getSingleSignOnServiceUrl(), SINGLE_SIGN_ON_SERVICE_URL);
354+
//transient name id format is not accepted together with principaltype SubjectnameId
355+
if (JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get().equals(getNameIDPolicyFormat()) && SamlPrincipalType.SUBJECT == getPrincipalType())
356+
throw new IllegalArgumentException("Can not have Transient NameID Policy Format together with SUBJECT Principal Type");
357+
344358
}
345359
}

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
1111
import org.keycloak.dom.saml.v2.assertion.AttributeType;
1212
import org.keycloak.dom.saml.v2.assertion.AudienceRestrictionType;
13+
import org.keycloak.dom.saml.v2.assertion.NameIDType;
1314
import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
1415
import org.keycloak.dom.saml.v2.protocol.ResponseType;
1516
import org.keycloak.protocol.saml.SamlPrincipalType;
@@ -61,7 +62,10 @@
6162
import static org.hamcrest.Matchers.notNullValue;
6263
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_CONS_NAME;
6364
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
65+
import static org.junit.Assert.assertEquals;
6466
import static org.junit.Assert.assertThat;
67+
import static org.junit.Assert.assertTrue;
68+
6569
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
6670
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
6771

@@ -122,6 +126,7 @@ public void initRealmUrls() {
122126
public void resetPrincipalType() {
123127
IdentityProviderResource idp = adminClient.realm(REALM_CONS_NAME).identityProviders().get("saml-leaf");
124128
IdentityProviderRepresentation rep = idp.toRepresentation();
129+
rep.getConfig().put(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get());
125130
rep.getConfig().put(SAMLIdentityProviderConfig.PRINCIPAL_TYPE, SamlPrincipalType.SUBJECT.name());
126131
idp.update(rep);
127132
}
@@ -396,6 +401,109 @@ public void testProviderIdpInitiatedLoginWithPrincipalAttribute() throws Excepti
396401
assertThat(fed.getUserId(), is(PROVIDER_REALM_USER_NAME));
397402
assertThat(fed.getUserName(), is(PROVIDER_REALM_USER_NAME));
398403
}
404+
405+
@Test
406+
public void testProviderTransientIdpInitiatedLogin() throws Exception {
407+
IdentityProviderResource idp = adminClient.realm(REALM_CONS_NAME).identityProviders().get("saml-leaf");
408+
IdentityProviderRepresentation rep = idp.toRepresentation();
409+
rep.getConfig().put(SAMLIdentityProviderConfig.NAME_ID_POLICY_FORMAT, JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get());
410+
rep.getConfig().put(SAMLIdentityProviderConfig.PRINCIPAL_TYPE, SamlPrincipalType.ATTRIBUTE.name());
411+
rep.getConfig().put(SAMLIdentityProviderConfig.PRINCIPAL_ATTRIBUTE, X500SAMLProfileConstants.UID.get());
412+
idp.update(rep);
413+
414+
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
415+
.navigateTo(getSamlIdpInitiatedUrl(REALM_PROV_NAME, "samlbroker"))
416+
// Login in provider realm
417+
.login().user(PROVIDER_REALM_USER_NAME, PROVIDER_REALM_USER_PASSWORD).build()
418+
419+
// Send the response to the consumer realm
420+
.processSamlResponse(Binding.POST)
421+
.transformObject(ob -> {
422+
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
423+
ResponseType resp = (ResponseType) ob;
424+
assertThat(resp.getDestination(), is(getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales")));
425+
assertAudience(resp, getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales"));
426+
427+
NameIDType nameId = new NameIDType();
428+
nameId.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()));
429+
nameId.setValue("subjectId1" );
430+
resp.getAssertions().get(0).getAssertion().getSubject().getSubType().addBaseID(nameId);
431+
432+
Set<StatementAbstractType> statements = resp.getAssertions().get(0).getAssertion().getStatements();
433+
434+
AttributeStatementType attributeType = (AttributeStatementType) statements.stream()
435+
.filter(statement -> statement instanceof AttributeStatementType).findFirst()
436+
.orElse(new AttributeStatementType());
437+
438+
AttributeType attr = new AttributeType(X500SAMLProfileConstants.UID.get());
439+
attr.addAttributeValue(PROVIDER_REALM_USER_NAME);
440+
441+
attributeType.addAttribute(new AttributeStatementType.ASTChoiceType(attr));
442+
resp.getAssertions().get(0).getAssertion().addStatement(attributeType);
443+
444+
return ob;
445+
})
446+
.build()
447+
448+
// Now login to the second app
449+
.navigateTo(getSamlIdpInitiatedUrl(REALM_PROV_NAME, "samlbroker-2"))
450+
451+
// Login in provider realm
452+
.login().sso(true).build()
453+
454+
.processSamlResponse(Binding.POST)
455+
.transformObject(ob -> {
456+
assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
457+
ResponseType resp = (ResponseType) ob;
458+
assertThat(resp.getDestination(), is(getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales2")));
459+
assertAudience(resp, getSamlBrokerIdpInitiatedUrl(REALM_CONS_NAME, "sales2"));
460+
461+
NameIDType nameId = new NameIDType();
462+
nameId.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()));
463+
nameId.setValue("subjectId2" );
464+
resp.getAssertions().get(0).getAssertion().getSubject().getSubType().addBaseID(nameId);
465+
466+
Set<StatementAbstractType> statements = resp.getAssertions().get(0).getAssertion().getStatements();
467+
468+
AttributeStatementType attributeType = (AttributeStatementType) statements.stream()
469+
.filter(statement -> statement instanceof AttributeStatementType).findFirst()
470+
.orElse(new AttributeStatementType());
471+
472+
AttributeType attr = new AttributeType(X500SAMLProfileConstants.UID.get());
473+
attr.addAttributeValue(PROVIDER_REALM_USER_NAME);
474+
475+
attributeType.addAttribute(new AttributeStatementType.ASTChoiceType(attr));
476+
resp.getAssertions().get(0).getAssertion().addStatement(attributeType);
477+
478+
return ob;
479+
})
480+
.build()
481+
482+
.updateProfile().username(CONSUMER_CHOSEN_USERNAME).email("test@localhost").firstName("Firstname").lastName("Lastname").build()
483+
.followOneRedirect()
484+
485+
// Obtain the response sent to the app
486+
.getSamlResponse(Binding.POST);
487+
488+
assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
489+
ResponseType resp = (ResponseType) samlResponse.getSamlObject();
490+
assertThat(resp.getDestination(), is(urlRealmConsumer + "/app/auth2/saml"));
491+
assertAudience(resp, urlRealmConsumer + "/app/auth2");
492+
493+
UsersResource users = adminClient.realm(REALM_CONS_NAME).users();
494+
List<UserRepresentation> userList= users.search(CONSUMER_CHOSEN_USERNAME);
495+
assertEquals(1, userList.size());
496+
String id = userList.get(0).getId();
497+
FederatedIdentityRepresentation fed = users.get(id).getFederatedIdentity().get(0);
498+
assertThat(fed.getUserId(), is(PROVIDER_REALM_USER_NAME));
499+
assertThat(fed.getUserName(), is(PROVIDER_REALM_USER_NAME));
500+
501+
//check that no user with sent subject-id was sent
502+
userList = users.search("subjectId1");
503+
assertTrue(userList.isEmpty());
504+
userList = users.search("subjectId2");
505+
assertTrue(userList.isEmpty());
506+
}
399507

400508
private void assertSingleUserSession(String realmName, String userName, String... expectedClientIds) {
401509
final UsersResource users = adminClient.realm(realmName).users();

themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,8 @@ saml.principal-type=Principal Type
712712
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.
713713
saml.principal-attribute=Principal Attribute
714714
saml.principal-attribute.tooltip=Name or Friendly Name of the attribute used to identify external users.
715+
saml.allow-create=Allow create
716+
saml.allow-create.tooltip=Allow the external identity provider to create a new identifier to represent the principal
715717
http-post-binding-response=HTTP-POST Binding Response
716718
http-post-binding-response.tooltip=Indicates whether to respond to requests using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used.
717719
http-post-binding-for-authn-request=HTTP-POST Binding for AuthnRequest

themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -935,17 +935,15 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
935935

936936
$scope.initSamlProvider = function() {
937937
$scope.nameIdFormats = [
938-
/*
939-
{
940-
format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
941-
name: "Transient"
942-
},
943-
*/
944938
{
945939
format: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
946940
name: "Persistent"
947941

948942
},
943+
{
944+
format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
945+
name: "Transient"
946+
},
949947
{
950948
format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
951949
name: "Email"
@@ -1008,7 +1006,8 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
10081006
$scope.identityProvider.config.nameIDPolicyFormat = $scope.nameIdFormats[0].format;
10091007
$scope.identityProvider.config.principalType = $scope.principalTypes[0].type;
10101008
$scope.identityProvider.config.signatureAlgorithm = $scope.signatureAlgorithms[1];
1011-
$scope.identityProvider.config.xmlSigKeyInfoKeyNameTransformer = $scope.xmlKeyNameTranformers[1];
1009+
$scope.identityProvider.config.xmlSigKeyInfoKeyNameTransformer = $scope.xmlKeyNameTranformers[1];
1010+
$scope.identityProvider.config.allowCreate = 'true';
10121011
}
10131012
$scope.identityProvider.config.entityId = $scope.identityProvider.config.entityId || (authUrl + '/realms/' + realm.realm);
10141013
}

themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,13 @@
186186
<input class="form-control" id="principalAttribute" type="text" ng-model="identityProvider.config.principalAttribute" ng-required="identityProvider.config.principalType.endsWith('ATTRIBUTE')">
187187
</div>
188188
<kc-tooltip>{{:: 'saml.principal-attribute.tooltip' | translate}}</kc-tooltip>
189+
</div>
190+
<div class="form-group">
191+
<label class="col-md-2 control-label" for="allowCreate">{{:: 'saml.allow-create' | translate}}</label>
192+
<div class="col-md-6">
193+
<input ng-model="identityProvider.config.allowCreate" id="allowCreate" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
194+
</div>
195+
<kc-tooltip>{{:: 'saml.allow-create.tooltip' | translate}}</kc-tooltip>
189196
</div>
190197
<div class="form-group">
191198
<label class="col-md-2 control-label" for="postBindingResponse">{{:: 'http-post-binding-response' | translate}}</label>

0 commit comments

Comments
 (0)