001/* 002 * nimbus-jose-jwt 003 * 004 * Copyright 2012-2016, Connect2id Ltd. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.jose.jwk.source; 019 020 021import java.io.IOException; 022import java.net.URL; 023import java.util.Collections; 024import java.util.List; 025import java.util.Set; 026 027import net.jcip.annotations.ThreadSafe; 028 029import com.nimbusds.jose.RemoteKeySourceException; 030import com.nimbusds.jose.jwk.JWK; 031import com.nimbusds.jose.jwk.JWKMatcher; 032import com.nimbusds.jose.jwk.JWKSelector; 033import com.nimbusds.jose.jwk.JWKSet; 034import com.nimbusds.jose.proc.SecurityContext; 035import com.nimbusds.jose.util.DefaultResourceRetriever; 036import com.nimbusds.jose.util.Resource; 037import com.nimbusds.jose.util.ResourceRetriever; 038 039 040/** 041 * Remote JSON Web Key (JWK) source specified by a JWK set URL. The retrieved 042 * JWK set is cached to minimise network calls. The cache is updated whenever 043 * the key selector tries to get a key with an unknown ID. 044 * 045 * <p>If no {@link ResourceRetriever} is specified when creating a remote JWK 046 * set source the {@link DefaultResourceRetriever default one} will be used, 047 * with the following HTTP timeouts and limits: 048 * 049 * <ul> 050 * <li>HTTP connect timeout, in milliseconds: Determined by the 051 * {@link #DEFAULT_HTTP_CONNECT_TIMEOUT} constant which can be 052 * overridden by setting the 053 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout} 054 * Java system property. 055 * <li>HTTP read timeout, in milliseconds: Determined by the 056 * {@link #DEFAULT_HTTP_READ_TIMEOUT} constant which can be 057 * overridden by setting the 058 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout} 059 * Java system property. 060 * <li>HTTP entity size limit: Determined by the 061 * {@link #DEFAULT_HTTP_SIZE_LIMIT} constant which can be 062 * overridden by setting the 063 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit} 064 * Java system property. 065 * </ul> 066 * 067 * @author Vladimir Dzhuvinov 068 * @version 2022-01-24 069 */ 070@ThreadSafe 071public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> { 072 073 074 /** 075 * The default HTTP connect timeout for JWK set retrieval, in 076 * milliseconds. Set to 500 milliseconds. 077 */ 078 public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 500; 079 080 081 /** 082 * The default HTTP read timeout for JWK set retrieval, in 083 * milliseconds. Set to 500 milliseconds. 084 */ 085 public static final int DEFAULT_HTTP_READ_TIMEOUT = 500; 086 087 088 /** 089 * The default HTTP entity size limit for JWK set retrieval, in bytes. 090 * Set to 50 KBytes. 091 */ 092 public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024; 093 094 095 /** 096 * Resolves the default HTTP connect timeout for JWK set retrieval, in 097 * milliseconds. 098 * 099 * @return The {@link #DEFAULT_HTTP_CONNECT_TIMEOUT static constant}, 100 * overridden by setting the 101 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpConnectTimeout} 102 * Java system property. 103 */ 104 public static int resolveDefaultHTTPConnectTimeout() { 105 return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpConnectTimeout", DEFAULT_HTTP_CONNECT_TIMEOUT); 106 } 107 108 109 /** 110 * Resolves the default HTTP read timeout for JWK set retrieval, in 111 * milliseconds. 112 * 113 * @return The {@link #DEFAULT_HTTP_READ_TIMEOUT static constant}, 114 * overridden by setting the 115 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpReadTimeout} 116 * Java system property. 117 */ 118 public static int resolveDefaultHTTPReadTimeout() { 119 return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpReadTimeout", DEFAULT_HTTP_READ_TIMEOUT); 120 } 121 122 123 /** 124 * Resolves default HTTP entity size limit for JWK set retrieval, in 125 * bytes. 126 * 127 * @return The {@link #DEFAULT_HTTP_SIZE_LIMIT static constant}, 128 * overridden by setting the 129 * {@code com.nimbusds.jose.jwk.source.RemoteJWKSet.defaultHttpSizeLimit} 130 * Java system property. 131 */ 132 public static int resolveDefaultHTTPSizeLimit() { 133 return resolveDefault(RemoteJWKSet.class.getName() + ".defaultHttpSizeLimit", DEFAULT_HTTP_SIZE_LIMIT); 134 } 135 136 137 private static int resolveDefault(final String sysPropertyName, final int defaultValue) { 138 139 String value = System.getProperty(sysPropertyName); 140 141 if (value == null) { 142 return defaultValue; 143 } 144 145 try { 146 return Integer.parseInt(value); 147 } catch (NumberFormatException e) { 148 // Illegal value 149 return defaultValue; 150 } 151 } 152 153 154 /** 155 * The JWK set URL. 156 */ 157 private final URL jwkSetURL; 158 159 160 /** 161 * The JWK set cache. 162 */ 163 private final JWKSetCache jwkSetCache; 164 165 166 /** 167 * The JWK set retriever. 168 */ 169 private final ResourceRetriever jwkSetRetriever; 170 171 172 /** 173 * Creates a new remote JWK set using the 174 * {@link DefaultResourceRetriever default HTTP resource retriever} 175 * with the default HTTP timeouts and entity size limit. 176 * 177 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 178 */ 179 public RemoteJWKSet(final URL jwkSetURL) { 180 this(jwkSetURL, null); 181 } 182 183 184 /** 185 * Creates a new remote JWK set. 186 * 187 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 188 * @param resourceRetriever The HTTP resource retriever to use, 189 * {@code null} to use the 190 * {@link DefaultResourceRetriever default 191 * one} with the default HTTP timeouts and 192 * entity size limit. 193 */ 194 public RemoteJWKSet(final URL jwkSetURL, 195 final ResourceRetriever resourceRetriever) { 196 197 this(jwkSetURL, resourceRetriever, null); 198 } 199 200 201 /** 202 * Creates a new remote JWK set. 203 * 204 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 205 * @param resourceRetriever The HTTP resource retriever to use, 206 * {@code null} to use the 207 * {@link DefaultResourceRetriever default 208 * one} with the default HTTP timeouts and 209 * entity size limit. 210 * @param jwkSetCache The JWK set cache to use, {@code null} to 211 * use the {@link DefaultJWKSetCache default 212 * one}. 213 */ 214 public RemoteJWKSet(final URL jwkSetURL, 215 final ResourceRetriever resourceRetriever, 216 final JWKSetCache jwkSetCache) { 217 218 if (jwkSetURL == null) { 219 throw new IllegalArgumentException("The JWK set URL must not be null"); 220 } 221 this.jwkSetURL = jwkSetURL; 222 223 if (resourceRetriever != null) { 224 jwkSetRetriever = resourceRetriever; 225 } else { 226 jwkSetRetriever = new DefaultResourceRetriever( 227 resolveDefaultHTTPConnectTimeout(), 228 resolveDefaultHTTPReadTimeout(), 229 resolveDefaultHTTPSizeLimit()); 230 } 231 232 if (jwkSetCache != null) { 233 this.jwkSetCache = jwkSetCache; 234 } else { 235 this.jwkSetCache = new DefaultJWKSetCache(); 236 } 237 } 238 239 240 /** 241 * Updates the cached JWK set from the configured URL. 242 * 243 * @return The updated JWK set. 244 * 245 * @throws RemoteKeySourceException If JWK retrieval failed. 246 */ 247 private JWKSet updateJWKSetFromURL() 248 throws RemoteKeySourceException { 249 Resource res; 250 try { 251 res = jwkSetRetriever.retrieveResource(jwkSetURL); 252 } catch (IOException e) { 253 throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e); 254 } 255 JWKSet jwkSet; 256 try { 257 jwkSet = JWKSet.parse(res.getContent()); 258 } catch (java.text.ParseException e) { 259 throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e); 260 } 261 jwkSetCache.put(jwkSet); 262 return jwkSet; 263 } 264 265 266 /** 267 * Returns the JWK set URL. 268 * 269 * @return The JWK set URL. 270 */ 271 public URL getJWKSetURL() { 272 273 return jwkSetURL; 274 } 275 276 277 /** 278 * Returns the HTTP resource retriever. 279 * 280 * @return The HTTP resource retriever. 281 */ 282 public ResourceRetriever getResourceRetriever() { 283 284 return jwkSetRetriever; 285 } 286 287 288 /** 289 * Returns the configured JWK set cache. 290 * 291 * @return The JWK set cache. 292 */ 293 public JWKSetCache getJWKSetCache() { 294 295 return jwkSetCache; 296 } 297 298 299 /** 300 * Returns the cached JWK set. 301 * 302 * @return The cached JWK set, {@code null} if none or expired. 303 */ 304 public JWKSet getCachedJWKSet() { 305 306 return jwkSetCache.get(); 307 } 308 309 310 /** 311 * Returns the first specified key ID (kid) for a JWK matcher. 312 * 313 * @param jwkMatcher The JWK matcher. Must not be {@code null}. 314 * 315 * @return The first key ID, {@code null} if none. 316 */ 317 protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) { 318 319 Set<String> keyIDs = jwkMatcher.getKeyIDs(); 320 321 if (keyIDs == null || keyIDs.isEmpty()) { 322 return null; 323 } 324 325 for (String id: keyIDs) { 326 if (id != null) { 327 return id; 328 } 329 } 330 return null; // No kid in matcher 331 } 332 333 334 /** 335 * {@inheritDoc} The security context is ignored. 336 */ 337 @Override 338 public List<JWK> get(final JWKSelector jwkSelector, final C context) 339 throws RemoteKeySourceException { 340 341 // Get the JWK set, may necessitate a cache update 342 JWKSet jwkSet = jwkSetCache.get(); 343 if (jwkSetCache.requiresRefresh() || jwkSet == null) { 344 try { 345 // retrieve jwkSet by calling JWK set URL 346 jwkSet = updateJWKSetFromURL(); 347 } catch (Exception ex) { 348 if (jwkSet == null) { 349 // throw the received exception if expired. 350 throw ex; 351 } 352 } 353 } 354 355 // Run the selector on the JWK set 356 List<JWK> matches = jwkSelector.select(jwkSet); 357 358 if (! matches.isEmpty()) { 359 // Success 360 return matches; 361 } 362 363 // Refresh the JWK set if the sought key ID is not in the cached JWK set 364 365 // Looking for JWK with specific ID? 366 String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher()); 367 if (soughtKeyID == null) { 368 // No key ID specified, return no matches 369 return Collections.emptyList(); 370 } 371 372 if (jwkSet.getKeyByKeyId(soughtKeyID) != null) { 373 // The key ID exists in the cached JWK set, matching 374 // failed for some other reason, return no matches 375 return Collections.emptyList(); 376 } 377 378 // Make new HTTP GET to the JWK set URL 379 jwkSet = updateJWKSetFromURL(); 380 if (jwkSet == null) { 381 // Retrieval has failed 382 return Collections.emptyList(); 383 } 384 385 // Repeat select, return final result (success or no matches) 386 return jwkSelector.select(jwkSet); 387 } 388}