001package com.nimbusds.jose.jwk; 002 003 004import java.io.File; 005import java.io.IOException; 006import java.net.URL; 007import java.text.ParseException; 008import java.util.HashMap; 009import java.util.LinkedList; 010import java.util.List; 011import java.util.Map; 012 013import com.nimbusds.jose.util.URLUtils; 014import net.minidev.json.JSONArray; 015import net.minidev.json.JSONObject; 016 017import org.apache.commons.io.FileUtils; 018 019import com.nimbusds.jose.util.JSONObjectUtils; 020 021 022/** 023 * JSON Web Key (JWK) set. Represented by a JSON object that contains an array 024 * of {@link JWK JSON Web Keys} (JWKs) as the value of its "keys" member. 025 * Additional (custom) members of the JWK Set JSON object are also supported. 026 * 027 * <p>Example JSON Web Key (JWK) set: 028 * 029 * <pre> 030 * { 031 * "keys" : [ { "kty" : "EC", 032 * "crv" : "P-256", 033 * "x" : "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 034 * "y" : "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 035 * "use" : "enc", 036 * "kid" : "1" }, 037 * 038 * { "kty" : "RSA", 039 * "n" : "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx 040 * 4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs 041 * tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2 042 * QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI 043 * SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb 044 * w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 045 * "e" : "AQAB", 046 * "alg" : "RS256", 047 * "kid" : "2011-04-29" } ] 048 * } 049 * </pre> 050 * 051 * @author Vladimir Dzhuvinov 052 * @version $version$ (2014-12-14) 053 */ 054public class JWKSet { 055 056 057 /** 058 * The MIME type of JWK set objects: 059 * {@code application/jwk-set+json; charset=UTF-8} 060 */ 061 public static final String MIME_TYPE = "application/jwk-set+json; charset=UTF-8"; 062 063 064 /** 065 * The JWK list. 066 */ 067 private final List<JWK> keys = new LinkedList<>(); 068 069 070 /** 071 * Additional custom members. 072 */ 073 private final Map<String,Object> customMembers = new HashMap<>(); 074 075 076 /** 077 * Creates a new empty JSON Web Key (JWK) set. 078 */ 079 public JWKSet() { 080 081 // Nothing to do 082 } 083 084 085 /** 086 * Creates a new JSON Web Key (JWK) set with a single key. 087 * 088 * @param key The JWK. Must not be {@code null}. 089 */ 090 public JWKSet(final JWK key) { 091 092 if (key == null) { 093 throw new IllegalArgumentException("The JWK must not be null"); 094 } 095 096 keys.add(key); 097 } 098 099 100 /** 101 * Creates a new JSON Web Key (JWK) set with the specified keys. 102 * 103 * @param keys The JWK list. Must not be {@code null}. 104 */ 105 public JWKSet(final List<JWK> keys) { 106 107 if (keys == null) { 108 throw new IllegalArgumentException("The JWK list must not be null"); 109 } 110 111 this.keys.addAll(keys); 112 } 113 114 115 /** 116 * Creates a new JSON Web Key (JWK) set with the specified keys and 117 * additional custom members. 118 * 119 * @param keys The JWK list. Must not be {@code null}. 120 * @param customMembers The additional custom members. Must not be 121 * {@code null}. 122 */ 123 public JWKSet(final List<JWK> keys, final Map<String,Object> customMembers) { 124 125 if (keys == null) { 126 throw new IllegalArgumentException("The JWK list must not be null"); 127 } 128 129 this.keys.addAll(keys); 130 131 this.customMembers.putAll(customMembers); 132 } 133 134 135 /** 136 * Gets the keys (ordered) of this JSON Web Key (JWK) set. 137 * 138 * @return The keys, empty list if none. 139 */ 140 public List<JWK> getKeys() { 141 142 return keys; 143 } 144 145 146 /** 147 * Gets the key from this JSON Web Key (JWK) set as identified by its 148 * Key ID (kid) member. 149 * 150 * <p>If more than one key exists in the JWK Set with the same 151 * identifier, this function returns only the first one in the set. 152 * 153 * @return The key identified by {@code kid} or {@code null} if no key 154 * exists. 155 */ 156 public JWK getKeyByKeyId(String kid) { 157 158 for (JWK key : getKeys()) { 159 160 if (key.getKeyID() != null && key.getKeyID().equals(kid)) { 161 return key; 162 } 163 } 164 165 // no key found 166 return null; 167 } 168 169 170 /** 171 * Gets the additional custom members of this JSON Web Key (JWK) set. 172 * 173 * @return The additional custom members, empty map if none. 174 */ 175 public Map<String,Object> getAdditionalMembers() { 176 177 return customMembers; 178 } 179 180 181 /** 182 * Returns a copy of this JSON Web Key (JWK) set with all private keys 183 * and parameters removed. 184 * 185 * @return A copy of this JWK set with all private keys and parameters 186 * removed. 187 */ 188 public JWKSet toPublicJWKSet() { 189 190 List<JWK> publicKeyList = new LinkedList<>(); 191 192 for (JWK key: keys) { 193 194 JWK publicKey = key.toPublicJWK(); 195 196 if (publicKey != null) { 197 publicKeyList.add(publicKey); 198 } 199 } 200 201 return new JWKSet(publicKeyList, customMembers); 202 } 203 204 205 /** 206 * Returns the JSON object representation of this JSON Web Key (JWK) 207 * set. Private keys and parameters will be omitted from the output. 208 * Use the alternative {@link #toJSONObject(boolean)} method if you 209 * wish to include them. 210 * 211 * @return The JSON object representation. 212 */ 213 public JSONObject toJSONObject() { 214 215 return toJSONObject(true); 216 } 217 218 219 /** 220 * Returns the JSON object representation of this JSON Web Key (JWK) 221 * set. 222 * 223 * @param publicKeysOnly Controls the inclusion of private keys and 224 * parameters into the output JWK members. If 225 * {@code true} private keys and parameters will 226 * be omitted. If {@code false} all available key 227 * parameters will be included. 228 * 229 * @return The JSON object representation. 230 */ 231 public JSONObject toJSONObject(final boolean publicKeysOnly) { 232 233 JSONObject o = new JSONObject(customMembers); 234 235 JSONArray a = new JSONArray(); 236 237 for (JWK key: keys) { 238 239 if (publicKeysOnly) { 240 241 // Try to get public key, then serialise 242 JWK publicKey = key.toPublicJWK(); 243 244 if (publicKey != null) { 245 a.add(publicKey.toJSONObject()); 246 } 247 } else { 248 249 a.add(key.toJSONObject()); 250 } 251 } 252 253 o.put("keys", a); 254 255 return o; 256 } 257 258 259 /** 260 * Returns the JSON object string representation of this JSON Web Key 261 * (JWK) set. 262 * 263 * @return The JSON object string representation. 264 */ 265 @Override 266 public String toString() { 267 268 return toJSONObject().toString(); 269 } 270 271 272 /** 273 * Parses the specified string representing a JSON Web Key (JWK) set. 274 * 275 * @param s The string to parse. Must not be {@code null}. 276 * 277 * @return The JSON Web Key (JWK) set. 278 * 279 * @throws ParseException If the string couldn't be parsed to a valid 280 * JSON Web Key (JWK) set. 281 */ 282 public static JWKSet parse(final String s) 283 throws ParseException { 284 285 return parse(JSONObjectUtils.parseJSONObject(s)); 286 } 287 288 289 /** 290 * Parses the specified JSON object representing a JSON Web Key (JWK) 291 * set. 292 * 293 * @param json The JSON object to parse. Must not be {@code null}. 294 * 295 * @return The JSON Web Key (JWK) set. 296 * 297 * @throws ParseException If the string couldn't be parsed to a valid 298 * JSON Web Key (JWK) set. 299 */ 300 public static JWKSet parse(final JSONObject json) 301 throws ParseException { 302 303 JSONArray keyArray = JSONObjectUtils.getJSONArray(json, "keys"); 304 305 List<JWK> keys = new LinkedList<>(); 306 307 for (int i=0; i < keyArray.size(); i++) { 308 309 if (! (keyArray.get(i) instanceof JSONObject)) { 310 throw new ParseException("The \"keys\" JSON array must contain JSON objects only", 0); 311 } 312 313 JSONObject keyJSON = (JSONObject)keyArray.get(i); 314 315 try { 316 keys.add(JWK.parse(keyJSON)); 317 318 } catch (ParseException e) { 319 320 throw new ParseException("Invalid JWK at position " + i + ": " + e.getMessage(), 0); 321 } 322 } 323 324 // Parse additional custom members 325 JWKSet jwkSet = new JWKSet(keys); 326 327 for (Map.Entry<String,Object> entry: json.entrySet()) { 328 329 if (entry.getKey() == null || entry.getKey().equals("keys")) { 330 continue; 331 } 332 333 jwkSet.getAdditionalMembers().put(entry.getKey(), entry.getValue()); 334 } 335 336 return jwkSet; 337 } 338 339 340 /** 341 * Loads a JSON Web Key (JWK) set from the specified file. 342 * 343 * @param file The JWK set file. Must not be {@code null}. 344 * 345 * @return The JSON Web Key (JWK) set. 346 * 347 * @throws IOException If the file couldn't be read. 348 * @throws ParseException If the file couldn't be parsed to a valid 349 * JSON Web Key (JWK) set. 350 */ 351 public static JWKSet load(final File file) 352 throws IOException, ParseException { 353 354 return parse(FileUtils.readFileToString(file)); 355 } 356 357 358 /** 359 * Loads a JSON Web Key (JWK) set from the specified URL. 360 * 361 * @param url The JWK set URL. Must not be {@code null}. 362 * @param connectTimeout The URL connection timeout, in milliseconds. 363 * If zero no (infinite) timeout. 364 * @param readTimeout The URL read timeout, in milliseconds. If zero 365 * no (infinite) timeout. 366 * @param sizeLimit The read size limit, in bytes. If negative no 367 * limit. 368 * 369 * @return The JSON Web Key (JWK) set. 370 * 371 * @throws IOException If the file couldn't be read. 372 * @throws ParseException If the file couldn't be parsed to a valid 373 * JSON Web Key (JWK) set. 374 */ 375 public static JWKSet load(final URL url, 376 final int connectTimeout, 377 final int readTimeout, 378 final int sizeLimit) 379 throws IOException, ParseException { 380 381 return parse(URLUtils.read(url, connectTimeout, readTimeout, sizeLimit)); 382 } 383 384 385 /** 386 * Loads a JSON Web Key (JWK) set from the specified URL. 387 * 388 * @param url The JWK set URL. Must not be {@code null}. 389 * 390 * @return The JSON Web Key (JWK) set. 391 * 392 * @throws IOException If the file couldn't be read. 393 * @throws ParseException If the file couldn't be parsed to a valid 394 * JSON Web Key (JWK) set. 395 */ 396 public static JWKSet load(final URL url) 397 throws IOException, ParseException { 398 399 return parse(URLUtils.read(url, 0, 0, -1)); 400 } 401}