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.JSONObjectUtils; 014import com.nimbusds.jose.util.DefaultResourceRetriever; 015import com.nimbusds.jose.util.Resource; 016import com.nimbusds.jose.util.RestrictedResourceRetriever; 017import net.minidev.json.JSONArray; 018import net.minidev.json.JSONObject; 019import org.apache.commons.io.FileUtils; 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 2016-04-10 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 * @param kid They key identifier. 154 * 155 * @return The key identified by {@code kid} or {@code null} if no key 156 * exists. 157 */ 158 public JWK getKeyByKeyId(String kid) { 159 160 for (JWK key : getKeys()) { 161 162 if (key.getKeyID() != null && key.getKeyID().equals(kid)) { 163 return key; 164 } 165 } 166 167 // no key found 168 return null; 169 } 170 171 172 /** 173 * Gets the additional custom members of this JSON Web Key (JWK) set. 174 * 175 * @return The additional custom members, empty map if none. 176 */ 177 public Map<String,Object> getAdditionalMembers() { 178 179 return customMembers; 180 } 181 182 183 /** 184 * Returns a copy of this JSON Web Key (JWK) set with all private keys 185 * and parameters removed. 186 * 187 * @return A copy of this JWK set with all private keys and parameters 188 * removed. 189 */ 190 public JWKSet toPublicJWKSet() { 191 192 List<JWK> publicKeyList = new LinkedList<>(); 193 194 for (JWK key: keys) { 195 196 JWK publicKey = key.toPublicJWK(); 197 198 if (publicKey != null) { 199 publicKeyList.add(publicKey); 200 } 201 } 202 203 return new JWKSet(publicKeyList, customMembers); 204 } 205 206 207 /** 208 * Returns the JSON object representation of this JSON Web Key (JWK) 209 * set. Private keys and parameters will be omitted from the output. 210 * Use the alternative {@link #toJSONObject(boolean)} method if you 211 * wish to include them. 212 * 213 * @return The JSON object representation. 214 */ 215 public JSONObject toJSONObject() { 216 217 return toJSONObject(true); 218 } 219 220 221 /** 222 * Returns the JSON object representation of this JSON Web Key (JWK) 223 * set. 224 * 225 * @param publicKeysOnly Controls the inclusion of private keys and 226 * parameters into the output JWK members. If 227 * {@code true} private keys and parameters will 228 * be omitted. If {@code false} all available key 229 * parameters will be included. 230 * 231 * @return The JSON object representation. 232 */ 233 public JSONObject toJSONObject(final boolean publicKeysOnly) { 234 235 JSONObject o = new JSONObject(customMembers); 236 237 JSONArray a = new JSONArray(); 238 239 for (JWK key: keys) { 240 241 if (publicKeysOnly) { 242 243 // Try to get public key, then serialise 244 JWK publicKey = key.toPublicJWK(); 245 246 if (publicKey != null) { 247 a.add(publicKey.toJSONObject()); 248 } 249 } else { 250 251 a.add(key.toJSONObject()); 252 } 253 } 254 255 o.put("keys", a); 256 257 return o; 258 } 259 260 261 /** 262 * Returns the JSON object string representation of this JSON Web Key 263 * (JWK) set. 264 * 265 * @return The JSON object string representation. 266 */ 267 @Override 268 public String toString() { 269 270 return toJSONObject().toString(); 271 } 272 273 274 /** 275 * Parses the specified string representing a JSON Web Key (JWK) set. 276 * 277 * @param s The string to parse. Must not be {@code null}. 278 * 279 * @return The JSON Web Key (JWK) set. 280 * 281 * @throws ParseException If the string couldn't be parsed to a valid 282 * JSON Web Key (JWK) set. 283 */ 284 public static JWKSet parse(final String s) 285 throws ParseException { 286 287 return parse(JSONObjectUtils.parse(s)); 288 } 289 290 291 /** 292 * Parses the specified JSON object representing a JSON Web Key (JWK) 293 * set. 294 * 295 * @param json The JSON object to parse. Must not be {@code null}. 296 * 297 * @return The JSON Web Key (JWK) set. 298 * 299 * @throws ParseException If the string couldn't be parsed to a valid 300 * JSON Web Key (JWK) set. 301 */ 302 public static JWKSet parse(final JSONObject json) 303 throws ParseException { 304 305 JSONArray keyArray = JSONObjectUtils.getJSONArray(json, "keys"); 306 307 List<JWK> keys = new LinkedList<>(); 308 309 for (int i=0; i < keyArray.size(); i++) { 310 311 if (! (keyArray.get(i) instanceof JSONObject)) { 312 throw new ParseException("The \"keys\" JSON array must contain JSON objects only", 0); 313 } 314 315 JSONObject keyJSON = (JSONObject)keyArray.get(i); 316 317 try { 318 keys.add(JWK.parse(keyJSON)); 319 320 } catch (ParseException e) { 321 322 throw new ParseException("Invalid JWK at position " + i + ": " + e.getMessage(), 0); 323 } 324 } 325 326 // Parse additional custom members 327 JWKSet jwkSet = new JWKSet(keys); 328 329 for (Map.Entry<String,Object> entry: json.entrySet()) { 330 331 if (entry.getKey() == null || entry.getKey().equals("keys")) { 332 continue; 333 } 334 335 jwkSet.getAdditionalMembers().put(entry.getKey(), entry.getValue()); 336 } 337 338 return jwkSet; 339 } 340 341 342 /** 343 * Loads a JSON Web Key (JWK) set from the specified file. 344 * 345 * @param file The JWK set file. Must not be {@code null}. 346 * 347 * @return The JSON Web Key (JWK) set. 348 * 349 * @throws IOException If the file couldn't be read. 350 * @throws ParseException If the file couldn't be parsed to a valid 351 * JSON Web Key (JWK) set. 352 */ 353 public static JWKSet load(final File file) 354 throws IOException, ParseException { 355 356 return parse(FileUtils.readFileToString(file)); 357 } 358 359 360 /** 361 * Loads a JSON Web Key (JWK) set from the specified URL. 362 * 363 * @param url The JWK set URL. Must not be {@code null}. 364 * @param connectTimeout The URL connection timeout, in milliseconds. 365 * If zero no (infinite) timeout. 366 * @param readTimeout The URL read timeout, in milliseconds. If zero 367 * no (infinite) timeout. 368 * @param sizeLimit The read size limit, in bytes. If zero no 369 * limit. 370 * 371 * @return The JSON Web Key (JWK) set. 372 * 373 * @throws IOException If the file couldn't be read. 374 * @throws ParseException If the file couldn't be parsed to a valid 375 * JSON Web Key (JWK) set. 376 */ 377 public static JWKSet load(final URL url, 378 final int connectTimeout, 379 final int readTimeout, 380 final int sizeLimit) 381 throws IOException, ParseException { 382 383 RestrictedResourceRetriever resourceRetriever = new DefaultResourceRetriever( 384 connectTimeout, 385 readTimeout, 386 sizeLimit); 387 Resource resource = resourceRetriever.retrieveResource(url); 388 return parse(resource.getContent()); 389 } 390 391 392 /** 393 * Loads a JSON Web Key (JWK) set from the specified URL. 394 * 395 * @param url The JWK set URL. Must not be {@code null}. 396 * 397 * @return The JSON Web Key (JWK) set. 398 * 399 * @throws IOException If the file couldn't be read. 400 * @throws ParseException If the file couldn't be parsed to a valid 401 * JSON Web Key (JWK) set. 402 */ 403 public static JWKSet load(final URL url) 404 throws IOException, ParseException { 405 406 return load(url, 0, 0, 0); 407 } 408}