Adding SAML protocol mapper to map organization membership
Closes #28732 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
aa945d5636
commit
f8bc74d64f
6 changed files with 234 additions and 10 deletions
|
@ -15,7 +15,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.protocol.oidc.mappers;
|
||||
package org.keycloak.organization.protocol.mappers.oidc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
@ -32,6 +32,12 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
||||
import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.TokenIntrospectionTokenMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.representations.IDToken;
|
||||
|
@ -69,15 +75,22 @@ public class OrganizationMembershipMapper extends AbstractOIDCProtocolMapper imp
|
|||
|
||||
@Override
|
||||
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) {
|
||||
UserModel user = userSession.getUser();
|
||||
OrganizationProvider organizationProvider = keycloakSession.getProvider(OrganizationProvider.class);
|
||||
OrganizationModel organization = organizationProvider.getByMember(user);
|
||||
OrganizationProvider provider = keycloakSession.getProvider(OrganizationProvider.class);
|
||||
|
||||
if (organization != null) {
|
||||
Map<String, Map<String, Object>> claim = new HashMap<>();
|
||||
claim.put(organization.getName(), Map.of());
|
||||
token.getOtherClaims().put(OAuth2Constants.ORGANIZATION, claim);
|
||||
if (!provider.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserModel user = userSession.getUser();
|
||||
OrganizationModel organization = provider.getByMember(user);
|
||||
|
||||
if (organization == null || !organization.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Map<String, Object>> claim = new HashMap<>();
|
||||
claim.put(organization.getName(), Map.of());
|
||||
token.getOtherClaims().put(OAuth2Constants.ORGANIZATION, claim);
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel create(String name, boolean accessToken, boolean idToken, boolean introspectionEndpoint) {
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.organization.protocol.mappers.saml;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config.Scope;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.OrganizationModel;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.organization.OrganizationProvider;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.protocol.saml.mappers.AbstractSAMLProtocolMapper;
|
||||
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
|
||||
import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
|
||||
public class OrganizationMembershipMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper, EnvironmentDependentProviderFactory {
|
||||
|
||||
public static final String ID = "saml-organization-membership-mapper";
|
||||
public static final String ORGANIZATION_ATTRIBUTE_NAME = "organization";
|
||||
|
||||
public static ProtocolMapperModel create() {
|
||||
ProtocolMapperModel mapper = new ProtocolMapperModel();
|
||||
|
||||
mapper.setName("organization");
|
||||
mapper.setProtocolMapper(ID);
|
||||
mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
||||
|
||||
return mapper;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
|
||||
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
|
||||
|
||||
if (!provider.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserModel user = userSession.getUser();
|
||||
OrganizationModel organization = provider.getByMember(user);
|
||||
|
||||
if (organization == null || !organization.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
AttributeType attribute = new AttributeType(ORGANIZATION_ATTRIBUTE_NAME);
|
||||
attribute.setFriendlyName(ORGANIZATION_ATTRIBUTE_NAME);
|
||||
attribute.setNameFormat(JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get());
|
||||
attribute.addAttributeValue(organization.getName());
|
||||
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Organization Membership";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return AttributeStatementHelper.ATTRIBUTE_STATEMENT_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Add an attribute to the assertion with information about the organization membership.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported(Scope config) {
|
||||
return Profile.isFeatureEnabled(Feature.ORGANIZATION);
|
||||
}
|
||||
}
|
|
@ -39,7 +39,7 @@ import org.keycloak.protocol.oidc.mappers.AddressMapper;
|
|||
import org.keycloak.protocol.oidc.mappers.AllowedWebOriginsProtocolMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.FullNameMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.OrganizationMembershipMapper;
|
||||
import org.keycloak.organization.protocol.mappers.oidc.OrganizationMembershipMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper;
|
||||
import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
|
@ -28,6 +30,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
|||
import org.keycloak.protocol.AbstractLoginProtocolFactory;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
|
||||
import org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper;
|
||||
import org.keycloak.protocol.saml.mappers.RoleListMapper;
|
||||
import org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper;
|
||||
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||
|
@ -93,6 +96,11 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
|
|||
model = RoleListMapper.create("role list", "Role", AttributeStatementHelper.BASIC, null, false);
|
||||
builtins.put("role list", model);
|
||||
defaultBuiltins.add(model);
|
||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
model = OrganizationMembershipMapper.create();
|
||||
builtins.put("organization", model);
|
||||
defaultBuiltins.add(model);
|
||||
}
|
||||
this.destinationValidator = DestinationValidator.forProtocolMap(config.getArray("knownProtocols"));
|
||||
}
|
||||
|
||||
|
@ -118,6 +126,14 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
|
|||
roleListScope.setProtocol(getId());
|
||||
roleListScope.addProtocolMapper(builtins.get("role list"));
|
||||
newRealm.addDefaultClientScope(roleListScope, true);
|
||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
ClientScopeModel organizationScope = newRealm.addClientScope("saml_organization");
|
||||
organizationScope.setDescription("Organization Membership");
|
||||
organizationScope.setDisplayOnConsentScreen(false);
|
||||
organizationScope.setProtocol(getId());
|
||||
organizationScope.addProtocolMapper(builtins.get("organization"));
|
||||
newRealm.addDefaultClientScope(organizationScope, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -25,7 +25,7 @@ org.keycloak.protocol.oidc.mappers.RoleNameMapper
|
|||
org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper
|
||||
org.keycloak.protocol.oidc.mappers.GroupMembershipMapper
|
||||
org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper
|
||||
org.keycloak.protocol.oidc.mappers.OrganizationMembershipMapper
|
||||
org.keycloak.organization.protocol.mappers.oidc.OrganizationMembershipMapper
|
||||
org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper
|
||||
org.keycloak.protocol.oidc.mappers.AllowedWebOriginsProtocolMapper
|
||||
org.keycloak.protocol.oidc.mappers.AcrProtocolMapper
|
||||
|
@ -56,3 +56,4 @@ org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTypeMapper
|
|||
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCContextMapper
|
||||
org.keycloak.protocol.oidc.mappers.SessionStateMapper
|
||||
org.keycloak.protocol.oidc.mappers.SubMapper
|
||||
org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.organization.admin;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.keycloak.testsuite.util.SamlStreams.assertionsUnencrypted;
|
||||
import static org.keycloak.testsuite.util.SamlStreams.attributeStatements;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.ws.rs.core.UriBuilder;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.OrganizationResource;
|
||||
import org.keycloak.common.Profile.Feature;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
|
||||
import org.keycloak.dom.saml.v2.assertion.AttributeType;
|
||||
import org.keycloak.protocol.saml.SamlConfigAttributes;
|
||||
import org.keycloak.protocol.saml.SamlProtocol;
|
||||
import org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper;
|
||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.saml.RoleMapperTest;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
import org.keycloak.testsuite.util.Matchers;
|
||||
import org.keycloak.testsuite.util.SamlClient;
|
||||
import org.keycloak.testsuite.util.SamlClientBuilder;
|
||||
|
||||
@EnableFeature(Feature.ORGANIZATION)
|
||||
public class OrganizationSAMLProtocolMapperTest extends AbstractOrganizationTest {
|
||||
|
||||
@Test
|
||||
public void testAttribute() {
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
addMember(organization);
|
||||
String clientId = "saml-client";
|
||||
testRealm().clients().create(ClientBuilder.create()
|
||||
.protocol(SamlProtocol.LOGIN_PROTOCOL)
|
||||
.clientId(clientId)
|
||||
.redirectUris("*")
|
||||
.attribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, Boolean.FALSE.toString())
|
||||
.build()).close();
|
||||
|
||||
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
|
||||
.authnRequest(RealmsResource
|
||||
.protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
|
||||
.build(TEST_REALM_NAME, SamlProtocol.LOGIN_PROTOCOL), clientId, RoleMapperTest.SAML_ASSERTION_CONSUMER_URL_EMPLOYEE_2, SamlClient.Binding.POST)
|
||||
.build()
|
||||
.login().user(memberEmail, memberPassword).build()
|
||||
.login().user(memberEmail, memberPassword).build()
|
||||
.getSamlResponse(SamlClient.Binding.POST);
|
||||
|
||||
assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
|
||||
AttributeType orgAttribute = attributeStatements(assertionsUnencrypted(samlResponse.getSamlObject()))
|
||||
.flatMap((Function<AttributeStatementType, Stream<ASTChoiceType>>) attributeStatementType -> attributeStatementType.getAttributes().stream())
|
||||
.map(ASTChoiceType::getAttribute)
|
||||
.filter(attribute -> OrganizationMembershipMapper.ORGANIZATION_ATTRIBUTE_NAME.equals(attribute.getName()))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
Assert.assertNotNull(orgAttribute);
|
||||
List<Object> values = orgAttribute.getAttributeValue();
|
||||
Assert.assertEquals(1, values.size());
|
||||
Assert.assertEquals(organizationName, values.get(0));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue