|
18 | 18 | package org.keycloak.protocol.oidc.mappers; |
19 | 19 |
|
20 | 20 | import com.fasterxml.jackson.databind.JsonNode; |
| 21 | +import org.jboss.logging.Logger; |
21 | 22 | import org.keycloak.models.ProtocolMapperModel; |
22 | 23 | import org.keycloak.protocol.ProtocolMapper; |
23 | 24 | import org.keycloak.protocol.ProtocolMapperUtils; |
@@ -59,11 +60,81 @@ public class OIDCAttributeMapperHelper { |
59 | 60 | public static final String INCLUDE_IN_USERINFO_LABEL = "includeInUserInfo.label"; |
60 | 61 | public static final String INCLUDE_IN_USERINFO_HELP_TEXT = "includeInUserInfo.tooltip"; |
61 | 62 |
|
| 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 | + |
62 | 133 | public static Object mapAttributeValue(ProtocolMapperModel mappingModel, Object attributeValue) { |
63 | 134 | if (attributeValue == null) return null; |
64 | 135 |
|
65 | 136 | if (attributeValue instanceof Collection) { |
66 | | - Collection<Object> valueAsList = (Collection<Object>) attributeValue; |
| 137 | + Collection<?> valueAsList = (Collection<?>) attributeValue; |
67 | 138 | if (valueAsList.isEmpty()) return null; |
68 | 139 |
|
69 | 140 | if (isMultivalued(mappingModel)) { |
@@ -100,34 +171,34 @@ private static Object convertToType(String type, Object attributeValue) { |
100 | 171 | Boolean booleanObject = getBoolean(attributeValue); |
101 | 172 | if (booleanObject != null) return booleanObject; |
102 | 173 | if (attributeValue instanceof List) { |
103 | | - return transform((List<Boolean>) attributeValue, OIDCAttributeMapperHelper::getBoolean); |
| 174 | + return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getBoolean); |
104 | 175 | } |
105 | 176 | throw new RuntimeException("cannot map type for token claim"); |
106 | 177 | case "String": |
107 | 178 | if (attributeValue instanceof String) return attributeValue; |
108 | 179 | if (attributeValue instanceof List) { |
109 | | - return transform((List<String>) attributeValue, OIDCAttributeMapperHelper::getString); |
| 180 | + return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getString); |
110 | 181 | } |
111 | 182 | return attributeValue.toString(); |
112 | 183 | case "long": |
113 | 184 | Long longObject = getLong(attributeValue); |
114 | 185 | if (longObject != null) return longObject; |
115 | 186 | if (attributeValue instanceof List) { |
116 | | - return transform((List<Long>) attributeValue, OIDCAttributeMapperHelper::getLong); |
| 187 | + return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getLong); |
117 | 188 | } |
118 | 189 | throw new RuntimeException("cannot map type for token claim"); |
119 | 190 | case "int": |
120 | 191 | Integer intObject = getInteger(attributeValue); |
121 | 192 | if (intObject != null) return intObject; |
122 | 193 | if (attributeValue instanceof List) { |
123 | | - return transform((List<Integer>) attributeValue, OIDCAttributeMapperHelper::getInteger); |
| 194 | + return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getInteger); |
124 | 195 | } |
125 | 196 | throw new RuntimeException("cannot map type for token claim"); |
126 | 197 | case "JSON": |
127 | 198 | JsonNode jsonNodeObject = getJsonNode(attributeValue); |
128 | 199 | if (jsonNodeObject != null) return jsonNodeObject; |
129 | 200 | if (attributeValue instanceof List) { |
130 | | - return transform((List<JsonNode>) attributeValue, OIDCAttributeMapperHelper::getJsonNode); |
| 201 | + return transform((List<?>) attributeValue, OIDCAttributeMapperHelper::getJsonNode); |
131 | 202 | } |
132 | 203 | throw new RuntimeException("cannot map type for token claim"); |
133 | 204 | default: |
@@ -200,22 +271,49 @@ public static List<String> splitClaimPath(String claimPath) { |
200 | 271 | } |
201 | 272 |
|
202 | 273 | public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) { |
203 | | - mapClaim(mappingModel, attributeValue, token.getOtherClaims()); |
| 274 | + mapClaim(token, mappingModel, attributeValue, tokenPropertySetters, token.getOtherClaims()); |
204 | 275 | } |
205 | 276 |
|
206 | 277 | public static void mapClaim(AccessTokenResponse token, ProtocolMapperModel mappingModel, Object attributeValue) { |
207 | | - mapClaim(mappingModel, attributeValue, token.getOtherClaims()); |
| 278 | + mapClaim(token, mappingModel, attributeValue, responsePropertySetters, token.getOtherClaims()); |
208 | 279 | } |
209 | 280 |
|
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) { |
211 | 283 | attributeValue = mapAttributeValue(mappingModel, attributeValue); |
212 | | - if (attributeValue == null) return; |
| 284 | + if (attributeValue == null) { |
| 285 | + return; |
| 286 | + } |
213 | 287 |
|
214 | 288 | String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME); |
215 | 289 | if (protocolClaim == null) { |
216 | 290 | return; |
217 | 291 | } |
| 292 | + |
218 | 293 | 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) { |
219 | 317 | final int length = split.size(); |
220 | 318 | int i = 0; |
221 | 319 | for (String component : split) { |
@@ -253,7 +351,7 @@ public static ProtocolMapperModel createClaimMapper(String name, |
253 | 351 | mapper.setName(name); |
254 | 352 | mapper.setProtocolMapper(mapperId); |
255 | 353 | mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); |
256 | | - Map<String, String> config = new HashMap<String, String>(); |
| 354 | + Map<String, String> config = new HashMap<>(); |
257 | 355 | config.put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute); |
258 | 356 | config.put(TOKEN_CLAIM_NAME, tokenClaimName); |
259 | 357 | config.put(JSON_TYPE, claimType); |
@@ -311,7 +409,7 @@ public static void addJsonTypeConfig(List<ProviderConfigProperty> configProperti |
311 | 409 | ProviderConfigProperty property = new ProviderConfigProperty(); |
312 | 410 | property.setName(JSON_TYPE); |
313 | 411 | property.setLabel(JSON_TYPE); |
314 | | - List<String> types = new ArrayList(5); |
| 412 | + List<String> types = new ArrayList<>(5); |
315 | 413 | types.add("String"); |
316 | 414 | types.add("long"); |
317 | 415 | types.add("int"); |
|
0 commit comments