Skip to content

Commit a4c4c00

Browse files
rmartincmposolda
authored andcommitted
[KEYCLOAK-14309] Duplicate sub claim at JSON level
1 parent cec6a8a commit a4c4c00

File tree

3 files changed

+249
-16
lines changed

3 files changed

+249
-16
lines changed

services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,73 @@ public AccessToken transformUserInfoAccessToken(KeycloakSession session, AccessT
717717

718718
public Map<String, Object> generateUserInfoClaims(AccessToken userInfo, UserModel userModel) {
719719
Map<String, Object> claims = new HashMap<>();
720-
claims.put("sub", userModel.getId());
720+
claims.put("sub", userInfo.getSubject() == null? userModel.getId() : userInfo.getSubject());
721+
if (userInfo.getIssuer() != null) {
722+
claims.put("iss", userInfo.getIssuer());
723+
}
724+
if (userInfo.getAudience()!= null) {
725+
claims.put("aud", userInfo.getAudience());
726+
}
727+
if (userInfo.getName() != null) {
728+
claims.put("name", userInfo.getName());
729+
}
730+
if (userInfo.getGivenName() != null) {
731+
claims.put("given_name", userInfo.getGivenName());
732+
}
733+
if (userInfo.getFamilyName() != null) {
734+
claims.put("family_name", userInfo.getFamilyName());
735+
}
736+
if (userInfo.getMiddleName() != null) {
737+
claims.put("middle_name", userInfo.getMiddleName());
738+
}
739+
if (userInfo.getNickName() != null) {
740+
claims.put("nickname", userInfo.getNickName());
741+
}
742+
if (userInfo.getPreferredUsername() != null) {
743+
claims.put("preferred_username", userInfo.getPreferredUsername());
744+
}
745+
if (userInfo.getProfile() != null) {
746+
claims.put("profile", userInfo.getProfile());
747+
}
748+
if (userInfo.getPicture() != null) {
749+
claims.put("picture", userInfo.getPicture());
750+
}
751+
if (userInfo.getWebsite() != null) {
752+
claims.put("website", userInfo.getWebsite());
753+
}
754+
if (userInfo.getEmail() != null) {
755+
claims.put("email", userInfo.getEmail());
756+
}
757+
if (userInfo.getEmailVerified() != null) {
758+
claims.put("email_verified", userInfo.getEmailVerified());
759+
}
760+
if (userInfo.getGender() != null) {
761+
claims.put("gender", userInfo.getGender());
762+
}
763+
if (userInfo.getBirthdate() != null) {
764+
claims.put("birthdate", userInfo.getBirthdate());
765+
}
766+
if (userInfo.getZoneinfo() != null) {
767+
claims.put("zoneinfo", userInfo.getZoneinfo());
768+
}
769+
if (userInfo.getLocale() != null) {
770+
claims.put("locale", userInfo.getLocale());
771+
}
772+
if (userInfo.getPhoneNumber() != null) {
773+
claims.put("phone_number", userInfo.getPhoneNumber());
774+
}
775+
if (userInfo.getPhoneNumberVerified() != null) {
776+
claims.put("phone_number_verified", userInfo.getPhoneNumberVerified());
777+
}
778+
if (userInfo.getAddress() != null) {
779+
claims.put("address", userInfo.getAddress());
780+
}
781+
if (userInfo.getUpdatedAt() != null) {
782+
claims.put("updated_at", userInfo.getUpdatedAt());
783+
}
784+
if (userInfo.getClaimsLocales() != null) {
785+
claims.put("claims_locales", userInfo.getClaimsLocales());
786+
}
721787
claims.putAll(userInfo.getOtherClaims());
722788

723789
if (userInfo.getRealmAccess() != null) {

services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAttributeMapperHelper.java

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.keycloak.protocol.oidc.mappers;
1919

2020
import com.fasterxml.jackson.databind.JsonNode;
21+
import org.jboss.logging.Logger;
2122
import org.keycloak.models.ProtocolMapperModel;
2223
import org.keycloak.protocol.ProtocolMapper;
2324
import org.keycloak.protocol.ProtocolMapperUtils;
@@ -59,11 +60,81 @@ public class OIDCAttributeMapperHelper {
5960
public static final String INCLUDE_IN_USERINFO_LABEL = "includeInUserInfo.label";
6061
public static final String INCLUDE_IN_USERINFO_HELP_TEXT = "includeInUserInfo.tooltip";
6162

63+
private static final Logger logger = Logger.getLogger(OIDCAttributeMapperHelper.class);
64+
65+
/**
66+
* Interface for a token property setter in a class T that accept claims.
67+
* @param <T> The token class for the property
68+
*/
69+
private static interface PropertySetter<T> {
70+
void set(String claim, String mapperName, T token, Object value);
71+
}
72+
73+
/**
74+
* Setters for claims in IDToken/AccessToken that will not use the other claims map.
75+
*/
76+
private static final Map<String, PropertySetter<IDToken>> tokenPropertySetters;
77+
78+
/**
79+
* Setters for claims in AccessTokenResponse that will not use the other claims map.
80+
*/
81+
private static final Map<String, PropertySetter<AccessTokenResponse>> responsePropertySetters;
82+
83+
static {
84+
// allowed claims that can be set in the IDToken/AccessToken object
85+
Map<String, PropertySetter<IDToken>> tmpToken = new HashMap<>();
86+
tmpToken.put("sub", (claim, mapperName, token, value) -> {
87+
token.setSubject(value.toString());
88+
});
89+
tmpToken.put("azp", (claim, mapperName, token, value) -> {
90+
token.issuedFor(value.toString());
91+
});
92+
tmpToken.put("aud", (claim, mapperName, token, value) -> {
93+
if (value instanceof Collection) {
94+
String[] audiences = ((Collection<?>) value).stream().map(Object::toString).toArray(String[]::new);
95+
token.audience(audiences);
96+
} else {
97+
token.audience(value.toString());
98+
}
99+
});
100+
// not allowed claims that are set by the server and can generate duplicates
101+
PropertySetter<IDToken> notAllowedInToken = (claim, mapperName, token, value) -> {
102+
logger.warnf("Claim '%s' is non-modifiable in IDToken. Ignoring the assignment for mapper '%s'.", claim, mapperName);
103+
};
104+
tmpToken.put("jti", notAllowedInToken);
105+
tmpToken.put("typ", notAllowedInToken);
106+
tmpToken.put("iat", notAllowedInToken);
107+
tmpToken.put("exp", notAllowedInToken);
108+
tmpToken.put("iss", notAllowedInToken);
109+
tmpToken.put("scope", notAllowedInToken);
110+
tmpToken.put(IDToken.NONCE, notAllowedInToken);
111+
tmpToken.put(IDToken.ACR, notAllowedInToken);
112+
tmpToken.put(IDToken.AUTH_TIME, notAllowedInToken);
113+
tmpToken.put(IDToken.SESSION_STATE, notAllowedInToken);
114+
tokenPropertySetters = Collections.unmodifiableMap(tmpToken);
115+
116+
// in the AccessTokenResponse do not allow modifications for server assigned properties
117+
Map<String, PropertySetter<AccessTokenResponse>> tmpResponse = new HashMap<>();
118+
PropertySetter<AccessTokenResponse> notAllowedInResponse = (claim, mapperName, token, value) -> {
119+
logger.warnf("Claim '%s' is non-modifiable in AccessTokenResponse. Ignoring the assignment for mapper '%s'.", claim, mapperName);
120+
};
121+
tmpResponse.put("access_token", notAllowedInResponse);
122+
tmpResponse.put("token_type", notAllowedInResponse);
123+
tmpResponse.put("session_state", notAllowedInResponse);
124+
tmpResponse.put("expires_in", notAllowedInResponse);
125+
tmpResponse.put("id_token", notAllowedInResponse);
126+
tmpResponse.put("refresh_token", notAllowedInResponse);
127+
tmpResponse.put("refresh_expires_in", notAllowedInResponse);
128+
tmpResponse.put("not-before-policy", notAllowedInResponse);
129+
tmpResponse.put("scope", notAllowedInResponse);
130+
responsePropertySetters = Collections.unmodifiableMap(tmpResponse);
131+
}
132+
62133
public static Object mapAttributeValue(ProtocolMapperModel mappingModel, Object attributeValue) {
63134
if (attributeValue == null) return null;
64135

65136
if (attributeValue instanceof Collection) {
66-
Collection<Object> valueAsList = (Collection<Object>) attributeValue;
137+
Collection<?> valueAsList = (Collection<?>) attributeValue;
67138
if (valueAsList.isEmpty()) return null;
68139

69140
if (isMultivalued(mappingModel)) {
@@ -100,34 +171,34 @@ private static Object convertToType(String type, Object attributeValue) {
100171
Boolean booleanObject = getBoolean(attributeValue);
101172
if (booleanObject != null) return booleanObject;
102173
if (attributeValue instanceof List) {
103-
return transform((List<Boolean>) attributeValue, OIDCAttributeMapperHelper::getBoolean);
174+
return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getBoolean);
104175
}
105176
throw new RuntimeException("cannot map type for token claim");
106177
case "String":
107178
if (attributeValue instanceof String) return attributeValue;
108179
if (attributeValue instanceof List) {
109-
return transform((List<String>) attributeValue, OIDCAttributeMapperHelper::getString);
180+
return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getString);
110181
}
111182
return attributeValue.toString();
112183
case "long":
113184
Long longObject = getLong(attributeValue);
114185
if (longObject != null) return longObject;
115186
if (attributeValue instanceof List) {
116-
return transform((List<Long>) attributeValue, OIDCAttributeMapperHelper::getLong);
187+
return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getLong);
117188
}
118189
throw new RuntimeException("cannot map type for token claim");
119190
case "int":
120191
Integer intObject = getInteger(attributeValue);
121192
if (intObject != null) return intObject;
122193
if (attributeValue instanceof List) {
123-
return transform((List<Integer>) attributeValue, OIDCAttributeMapperHelper::getInteger);
194+
return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getInteger);
124195
}
125196
throw new RuntimeException("cannot map type for token claim");
126197
case "JSON":
127198
JsonNode jsonNodeObject = getJsonNode(attributeValue);
128199
if (jsonNodeObject != null) return jsonNodeObject;
129200
if (attributeValue instanceof List) {
130-
return transform((List<JsonNode>) attributeValue, OIDCAttributeMapperHelper::getJsonNode);
201+
return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getJsonNode);
131202
}
132203
throw new RuntimeException("cannot map type for token claim");
133204
default:
@@ -200,22 +271,49 @@ public static List<String> splitClaimPath(String claimPath) {
200271
}
201272

202273
public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) {
203-
mapClaim(mappingModel, attributeValue, token.getOtherClaims());
274+
mapClaim(token, mappingModel, attributeValue, tokenPropertySetters, token.getOtherClaims());
204275
}
205276

206277
public static void mapClaim(AccessTokenResponse token, ProtocolMapperModel mappingModel, Object attributeValue) {
207-
mapClaim(mappingModel, attributeValue, token.getOtherClaims());
278+
mapClaim(token, mappingModel, attributeValue, responsePropertySetters, token.getOtherClaims());
208279
}
209280

210-
private static void mapClaim(ProtocolMapperModel mappingModel, Object attributeValue, Map<String, Object> jsonObject) {
281+
private static <T> void mapClaim(T token, ProtocolMapperModel mappingModel, Object attributeValue,
282+
Map<String, PropertySetter<T>> setters, Map<String, Object> jsonObject) {
211283
attributeValue = mapAttributeValue(mappingModel, attributeValue);
212-
if (attributeValue == null) return;
284+
if (attributeValue == null) {
285+
return;
286+
}
213287

214288
String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
215289
if (protocolClaim == null) {
216290
return;
217291
}
292+
218293
List<String> split = splitClaimPath(protocolClaim);
294+
if (split.isEmpty()) {
295+
return;
296+
}
297+
298+
String firstClaim = split.iterator().next();
299+
PropertySetter<T> setter = setters.get(firstClaim);
300+
if (setter != null) {
301+
// assign using the property setters over the token
302+
if (split.size() > 1) {
303+
logger.warnf("Claim '%s' contains more than one level in a setter. Ignoring the assignment for mapper '%s'.",
304+
protocolClaim, mappingModel.getName());
305+
return;
306+
}
307+
308+
setter.set(protocolClaim, mappingModel.getName(), token, attributeValue);
309+
return;
310+
}
311+
312+
// map value to the other claims map
313+
mapClaim(split, attributeValue, jsonObject);
314+
}
315+
316+
private static void mapClaim(List<String> split, Object attributeValue, Map<String, Object> jsonObject) {
219317
final int length = split.size();
220318
int i = 0;
221319
for (String component : split) {
@@ -253,7 +351,7 @@ public static ProtocolMapperModel createClaimMapper(String name,
253351
mapper.setName(name);
254352
mapper.setProtocolMapper(mapperId);
255353
mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
256-
Map<String, String> config = new HashMap<String, String>();
354+
Map<String, String> config = new HashMap<>();
257355
config.put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute);
258356
config.put(TOKEN_CLAIM_NAME, tokenClaimName);
259357
config.put(JSON_TYPE, claimType);
@@ -311,7 +409,7 @@ public static void addJsonTypeConfig(List<ProviderConfigProperty> configProperti
311409
ProviderConfigProperty property = new ProviderConfigProperty();
312410
property.setName(JSON_TYPE);
313411
property.setLabel(JSON_TYPE);
314-
List<String> types = new ArrayList(5);
412+
List<String> types = new ArrayList<>(5);
315413
types.add("String");
316414
types.add("long");
317415
types.add("int");

0 commit comments

Comments
 (0)