001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyrighted [year] [name of copyright owner]".
013 *
014 * Copyright 2013-2017 ForgeRock AS.
015 */
016
017package org.forgerock.json.jose.jwk;
018
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.Iterator;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026
027import org.forgerock.json.JsonException;
028import org.forgerock.json.JsonValue;
029import org.forgerock.json.jose.jwt.Algorithm;
030import org.forgerock.json.jose.jwt.JWObject;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import com.fasterxml.jackson.databind.ObjectMapper;
035
036/**
037 * Holds a Set of JWKs.
038 */
039public class JWKSet extends JWObject {
040
041    private static final Logger logger = LoggerFactory.getLogger(JWKSet.class);
042
043    /**
044     * Constructs an empty JWKSet.
045     */
046    public JWKSet() {
047        put("keys", Collections.EMPTY_LIST);
048    }
049
050    /**
051     * Construct a JWKSet from a single JWK.
052     * @param jwk the jwk to construct the set from
053     */
054    public JWKSet(JWK jwk) {
055        if (jwk == null) {
056            throw new JsonException("JWK must not be null");
057        }
058        put("keys", Collections.singletonList(jwk.toJsonValue().asMap()));
059    }
060
061    /**
062     * Construct a JWKSet from a single JWK.
063     * @param jwks contains a list of json web keys
064     */
065    public JWKSet(JsonValue jwks) {
066        if (jwks == null) {
067            throw new JsonException("JWK set must not be null");
068        }
069        put("keys", jwks.expect(List.class));
070    }
071
072    /**
073     * Construct a JWKSet from a List of JWKs.
074     * @param jwkList a list of jwks
075     */
076    public JWKSet(List<JWK> jwkList) {
077        if (jwkList == null) {
078            throw new JsonException("The list cannot be null");
079        }
080        //transform to json, as it's our current way of storing jwks
081        List<Map<String, Object>> jwkListAsJson = new ArrayList<>();
082        for (JWK jwk : jwkList) {
083            jwkListAsJson.add(jwk.toJsonValue().asMap());
084        }
085        put("keys", jwkListAsJson);
086    }
087
088    /**
089     * Get the JWKs in the set.
090     * @return a list of JWKs
091     */
092    public List<JWK> getJWKsAsList() {
093        List<JWK> listOfJWKs = new LinkedList<>();
094        JsonValue jwks = get("keys");
095        Iterator<JsonValue> i = jwks.iterator();
096        while (i.hasNext()) {
097            listOfJWKs.add(JWK.parse(i.next()));
098        }
099        return listOfJWKs;
100    }
101
102    /**
103     * Get the JWKs in the set.
104     * @return a list of JWKs as JsonValues
105     */
106    public JsonValue getJWKsAsJsonValue() {
107        return get("keys");
108    }
109
110    /**
111     * Converts a json string to a jsonValue.
112     * @param json a json jwk set object string
113     * @return a json value of the son string
114     * @throws JsonException if unable to parse
115     */
116    protected static JsonValue toJsonValue(String json) {
117        ObjectMapper mapper = new ObjectMapper();
118        try {
119            return new JsonValue(mapper.readValue(json, Map.class));
120        } catch (IOException e) {
121            throw new JsonException("Failed to parse json", e);
122        }
123    }
124
125    /**
126     * Parses a JWKSet object from a string json object.
127     * @param json string json object
128     * @return a JWKSet
129     */
130    public static JWKSet parse(String json) {
131        JsonValue jwkSet = new JsonValue(toJsonValue(json));
132        return parse(jwkSet);
133    }
134
135    /**
136     * Parses a JWKSet object from a jsonValue object.
137     * @param json an JsonValue object
138     * @return a JWKSet
139     */
140    public static JWKSet parse(JsonValue json) {
141        if (json == null) {
142            throw new JsonException("Cant parse JWKSet. No json data.");
143        }
144        return new JWKSet(json.get("keys"));
145    }
146
147    /**
148     * Prints the JWK Set as a json string.
149     * @return A String representing JWK
150     */
151    public String toJsonString() {
152        return super.toString();
153    }
154
155    /**
156     * Search for a JWK that matches the algorithm and the key usage.
157     *
158     * @param algorithm the algorithm needed
159     * @param keyUse the key usage. If null, only the algorithm will be used as a search criteria.
160     * @return A jwk that matches the search criteria. If no JWK found for the key usage, then it searches for a JWK
161     * without key usage defined. If still no JWK found, then returns null.
162     */
163    public JWK findJwk(Algorithm algorithm, KeyUse keyUse) {
164        //First, we try to find a JWK that matches the keyUse
165        for (JWK jwk : getJWKsAsList()) {
166            try {
167                if (algorithm.getJwaAlgorithmName().equalsIgnoreCase(jwk.getAlgorithm()) && (keyUse == jwk.getUse())) {
168                    return jwk;
169                }
170            } catch (IllegalArgumentException e) {
171                // We raise a warning as the JWKs could be the client one, with some non-compliant JWK.
172                logger.warn("Can't load JWK with kid'" + jwk.getKeyId() + "'", e);
173            }
174        }
175
176        //At this point, no jwk was found. We can try to find a JWK without a keyUse now
177        return keyUse != null ? findJwk(algorithm, null) :  null;
178    }
179
180    /**
181     * Search for a JWK that matches the kid.
182     *
183     * @param kid Key ID
184     * @return A jwk that matches the kid. If no JWK found, returns null
185     */
186    public JWK findJwk(String kid) {
187        for (JWK jwk : getJWKsAsList()) {
188            if (kid.equals(jwk.getKeyId())) {
189                return jwk;
190            }
191        }
192        return null;
193    }
194}