Skip to content

Commit

Permalink
organization claim
Browse files Browse the repository at this point in the history
Signed-off-by: Pedro Igor <[email protected]>
  • Loading branch information
pedroigor committed Mar 22, 2024
1 parent 31293d3 commit a510330
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 0 deletions.
1 change: 1 addition & 0 deletions core/src/main/java/org/keycloak/OAuth2Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public interface OAuth2Constants {
String SCOPE_ADDRESS = "address";
String SCOPE_PHONE = "phone";

String ORGANIZATION = "organization";
String UI_LOCALES_PARAM = "ui_locales";

String PROMPT = "prompt";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.UriUtils;
import org.keycloak.events.EventBuilder;
Expand All @@ -39,6 +40,7 @@
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.OrganizationProtocolMapper;
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
import org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper;
import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
Expand Down Expand Up @@ -81,6 +83,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
public static final String LOCALE = "locale";
public static final String ADDRESS = "address";
public static final String PHONE_NUMBER = "phone number";
public static final String ORGANIZATION = "organization";
public static final String PHONE_NUMBER_VERIFIED = "phone number verified";
public static final String REALM_ROLES = "realm roles";
public static final String CLIENT_ROLES = "client roles";
Expand All @@ -100,6 +103,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
public static final String EMAIL_SCOPE_CONSENT_TEXT = "${emailScopeConsentText}";
public static final String ADDRESS_SCOPE_CONSENT_TEXT = "${addressScopeConsentText}";
public static final String PHONE_SCOPE_CONSENT_TEXT = "${phoneScopeConsentText}";
public static final String ORGANIZATION_SCOPE_CONSENT_TEXT = "${organizationScopeConsentText}";
public static final String OFFLINE_ACCESS_SCOPE_CONSENT_TEXT = Constants.OFFLINE_ACCESS_SCOPE_CONSENT_TEXT;
public static final String ROLES_SCOPE_CONSENT_TEXT = "${rolesScopeConsentText}";

Expand Down Expand Up @@ -217,6 +221,11 @@ void initBuiltIns() {
model = AcrProtocolMapper.create(ACR, true, true, true);
builtins.put(ACR, model);
}

if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
model = OrganizationProtocolMapper.create();
builtins.put(ORGANIZATION, model);
}
}

private void createUserAttributeMapper(String name, String attrName, String claimName, String type) {
Expand Down Expand Up @@ -277,6 +286,17 @@ protected void createDefaultClientScopesImpl(RealmModel newRealm) {
phoneScope.addProtocolMapper(builtins.get(PHONE_NUMBER));
phoneScope.addProtocolMapper(builtins.get(PHONE_NUMBER_VERIFIED));

if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
ClientScopeModel organizationScope = newRealm.addClientScope(OAuth2Constants.ORGANIZATION);
organizationScope.setDescription("Additional claims about the organization a subject belongs to");
organizationScope.setDisplayOnConsentScreen(true);
organizationScope.setConsentScreenText(ORGANIZATION_SCOPE_CONSENT_TEXT);
organizationScope.setIncludeInTokenScope(true);
organizationScope.setProtocol(getId());
organizationScope.addProtocolMapper(builtins.get(ORGANIZATION));
newRealm.addDefaultClientScope(organizationScope, false);
}

// 'profile' and 'email' will be default scopes for now. 'address' and 'phone' will be optional scopes
newRealm.addDefaultClientScope(profileScope, true);
newRealm.addDefaultClientScope(emailScope, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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.protocol.oidc.mappers;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.keycloak.Config.Scope;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.ClientSessionContext;
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.oidc.OIDCLoginProtocol;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;

/**
* @author <a href="mailto:[email protected]">Pedro Igor</a>
*/
public class OrganizationProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, TokenIntrospectionTokenMapper, EnvironmentDependentProviderFactory {

private static final String PROVIDER_ID = "organization";

public static ProtocolMapperModel create() {
ProtocolMapperModel mapper = new ProtocolMapperModel();
mapper.setName("organization");
mapper.setProtocolMapper(OrganizationProtocolMapper.PROVIDER_ID);
mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
Map<String, String> config = new HashMap<>();
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
config.put(OIDCAttributeMapperHelper.INCLUDE_IN_INTROSPECTION, "true");
mapper.setConfig(config);
return mapper;
}

@Override
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);
UserModel user = userSession.getUser();
OrganizationModel organization = provider.getOrganizationByMember(session.getContext().getRealm(), user);

if (organization == null) {
return;
}

Map<String, Map<String, Object>> claim = new HashMap<>();
claim.put(organization.getName(), Map.of());
token.getOtherClaims().put("organization", claim);
}

@Override
public String getDisplayCategory() {
return TOKEN_MAPPER_CATEGORY;
}

@Override
public String getDisplayType() {
return "Organization";
}

@Override
public String getHelpText() {
return "Sets metadata into tokens about the access context of a subject within a organization";
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {
ArrayList<ProviderConfigProperty> properties = new ArrayList<>();
OIDCAttributeMapperHelper.addIncludeInTokensConfig(properties, OrganizationProtocolMapper.class);
return properties;
}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public boolean isSupported(Scope config) {
return Profile.isFeatureEnabled(Feature.ORGANIZATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper
org.keycloak.protocol.saml.mappers.UserAttributeNameIdMapper
org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper
org.keycloak.protocol.oidc.mappers.NonceBackwardsCompatibleMapper
org.keycloak.protocol.oidc.mappers.OrganizationProtocolMapper
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD;

import java.util.ArrayList;
import java.util.List;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.admin.AbstractAdminTest;
Expand Down Expand Up @@ -64,6 +69,14 @@ protected UserRepresentation addMember(OrganizationResource organization, String

expected.setEmail(email);
expected.setUsername(expected.getEmail());
expected.setEnabled(true);
List<CredentialRepresentation> credentials = new ArrayList<>();
CredentialRepresentation pass = new CredentialRepresentation();
pass.setType(PASSWORD);
pass.setValue("password");
pass.setTemporary(false);
credentials.add(pass);
expected.setCredentials(credentials);

try (Response response = organization.members().addMember(expected)) {
assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.Map;

import org.junit.Test;
import org.keycloak.TokenVerifier;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.representations.AccessToken;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;

@EnableFeature(Feature.ORGANIZATION)
public class OrganizationOIDCProtocolMapperTest extends AbstractOrganizationTest {

@Test
public void testUpdate() throws Exception {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
addMember(organization);

oauth.clientId("direct-grant");
oauth.scope("openid organization");
AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "[email protected]", "password");
String scope = response.getScope();
assertTrue(scope.contains("organization"));

AccessToken accessToken = TokenVerifier.create(response.getAccessToken(), AccessToken.class).getToken();
Map<String, Object> claim = (Map<String, Object>) accessToken.getOtherClaims().get("organization");
assertNotNull(claim);
assertNotNull(claim.get("neworg"));
}
}

0 comments on commit a510330

Please sign in to comment.