/* * Copyright 2015 JBoss Inc * * 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 io.apiman.plugins.keycloak_oauth_policy; import java.lang.reflect.Field; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; import org.keycloak.representations.JsonWebToken; import com.fasterxml.jackson.annotation.JsonProperty; /** * @author Marc Savy {@literal <msavy@redhat.com>} */ @SuppressWarnings("nls") public class ClaimLookup { private static final Map<String, List<Field>> STANDARD_CLAIMS_FIELD_MAP = new LinkedHashMap<>(); static { Class<?> clazz = AccessToken.class; do { getProperties(clazz, "", new ArrayDeque<Field>()); } while ((clazz = clazz.getSuperclass()) != null); // Legacy mappings, to ensure old configs keep working STANDARD_CLAIMS_FIELD_MAP.put("username", STANDARD_CLAIMS_FIELD_MAP.get(IDToken.PREFERRED_USERNAME)); STANDARD_CLAIMS_FIELD_MAP.put("subject", STANDARD_CLAIMS_FIELD_MAP.get("sub")); } private static void getProperties(Class<?> klazz, String path, Deque<Field> fieldChain) { for (Field f: klazz.getDeclaredFields()) { f.setAccessible(true); JsonProperty jsonProperty = f.getAnnotation(JsonProperty.class); if (jsonProperty != null) { fieldChain.push(f); // If the inspected type has nested @JsonProperty annotations, we need to inspect it if (hasJsonPropertyAnnotation(f)) { getProperties(f.getType(), f.getName() + ".", fieldChain); // Add "." when traversing into new object. } else { // Otherwise, just assume it's simple as the best we can do is #toString List<Field> fieldList = new ArrayList<>(fieldChain); Collections.reverse(fieldList); STANDARD_CLAIMS_FIELD_MAP.put(path + jsonProperty.value(), fieldList); fieldChain.pop(); // Pop, as we have now reached end of this chain. } } } } private static boolean hasJsonPropertyAnnotation(Field f) { for (Field g : f.getType().getDeclaredFields()) { g.setAccessible(true); if (g.getAnnotation(JsonProperty.class) != null) return true; } return false; } /** * * @param token token to retrieve claim from * @param claim the claim (field key) * @return string representaion of claim */ public static String getClaim(IDToken token, String claim) { if (claim == null || token == null) return null; // Get the standard claim field, if available if (STANDARD_CLAIMS_FIELD_MAP.containsKey(claim)) { return callClaimChain(token, STANDARD_CLAIMS_FIELD_MAP.get(claim)); } else { // Otherwise look up 'other claims' Object otherClaim = getOtherClaimValue(token, claim); return otherClaim == null ? null : otherClaim.toString(); } } private static String callClaimChain(Object rootObject, List<Field> list) { try { Object candidate = rootObject; for (Field f : list) { if ((candidate = f.get(candidate)) == null) break; } return (candidate == null) ? null : candidate.toString(); } catch (IllegalArgumentException | IllegalAccessException e) { // TODO Use logger. These exceptions shouldn't occur, but if it somehow does happen we need to know. System.err.println("Unexpected error looking up token field: " + e); //$NON-NLS-1$ e.printStackTrace(); } return null; } @SuppressWarnings("unchecked") // KC code - thanks. private static Object getOtherClaimValue(JsonWebToken token, String claim) { String[] split = claim.split("\\."); Map<String, Object> jsonObject = token.getOtherClaims(); for (int i = 0; i < split.length; i++) { if (i == split.length - 1) { return jsonObject.get(split[i]); } else { Object val = jsonObject.get(split[i]); if (!(val instanceof Map)) return null; jsonObject = (Map<String, Object>) val; } } return null; } }