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 com.nimbusds.jose.RemoteKeySourceException; 028import com.nimbusds.jose.jwk.JWK; 029import com.nimbusds.jose.jwk.JWKMatcher; 030import com.nimbusds.jose.jwk.JWKSelector; 031import com.nimbusds.jose.jwk.JWKSet; 032import com.nimbusds.jose.proc.SecurityContext; 033import com.nimbusds.jose.util.DefaultResourceRetriever; 034import com.nimbusds.jose.util.Resource; 035import com.nimbusds.jose.util.ResourceRetriever; 036import net.jcip.annotations.ThreadSafe; 037 038 039/** 040 * Remote JSON Web Key (JWK) source specified by a JWK set URL. The retrieved 041 * JWK set is cached to minimise network calls. The cache is updated whenever 042 * the key selector tries to get a key with an unknown ID. 043 * 044 * @author Vladimir Dzhuvinov 045 * @version 2018-10-28 046 */ 047@ThreadSafe 048public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> { 049 050 051 /** 052 * The default HTTP connect timeout for JWK set retrieval, in 053 * milliseconds. Set to 500 milliseconds. 054 */ 055 public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 500; 056 057 058 /** 059 * The default HTTP read timeout for JWK set retrieval, in 060 * milliseconds. Set to 500 milliseconds. 061 */ 062 public static final int DEFAULT_HTTP_READ_TIMEOUT = 500; 063 064 065 /** 066 * The default HTTP entity size limit for JWK set retrieval, in bytes. 067 * Set to 50 KBytes. 068 */ 069 public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024; 070 071 072 /** 073 * The JWK set URL. 074 */ 075 private final URL jwkSetURL; 076 077 078 /** 079 * The JWK set cache. 080 */ 081 private final JWKSetCache jwkSetCache; 082 083 084 /** 085 * The JWK set retriever. 086 */ 087 private final ResourceRetriever jwkSetRetriever; 088 089 090 /** 091 * Creates a new remote JWK set using the 092 * {@link DefaultResourceRetriever default HTTP resource retriever}, 093 * with a HTTP connect timeout set to 250 ms, HTTP read timeout set to 094 * 250 ms and a 50 KByte size limit. 095 * 096 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 097 */ 098 public RemoteJWKSet(final URL jwkSetURL) { 099 this(jwkSetURL, null); 100 } 101 102 103 /** 104 * Creates a new remote JWK set. 105 * 106 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 107 * @param resourceRetriever The HTTP resource retriever to use, 108 * {@code null} to use the 109 * {@link DefaultResourceRetriever default 110 * one}. 111 */ 112 public RemoteJWKSet(final URL jwkSetURL, 113 final ResourceRetriever resourceRetriever) { 114 115 this(jwkSetURL, resourceRetriever, null); 116 } 117 118 119 /** 120 * Creates a new remote JWK set. 121 * 122 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 123 * @param resourceRetriever The HTTP resource retriever to use, 124 * {@code null} to use the 125 * {@link DefaultResourceRetriever default 126 * one}. 127 * @param jwkSetCache The JWK set cache to use, {@code null} to 128 * use the {@link DefaultJWKSetCache default 129 * one}. 130 */ 131 public RemoteJWKSet(final URL jwkSetURL, 132 final ResourceRetriever resourceRetriever, 133 final JWKSetCache jwkSetCache) { 134 135 if (jwkSetURL == null) { 136 throw new IllegalArgumentException("The JWK set URL must not be null"); 137 } 138 this.jwkSetURL = jwkSetURL; 139 140 if (resourceRetriever != null) { 141 jwkSetRetriever = resourceRetriever; 142 } else { 143 jwkSetRetriever = new DefaultResourceRetriever(DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT, DEFAULT_HTTP_SIZE_LIMIT); 144 } 145 146 if (jwkSetCache != null) { 147 this.jwkSetCache = jwkSetCache; 148 } else { 149 this.jwkSetCache = new DefaultJWKSetCache(); 150 } 151 } 152 153 154 /** 155 * Updates the cached JWK set from the configured URL. 156 * 157 * @return The updated JWK set. 158 * 159 * @throws RemoteKeySourceException If JWK retrieval failed. 160 */ 161 private JWKSet updateJWKSetFromURL() 162 throws RemoteKeySourceException { 163 Resource res; 164 try { 165 res = jwkSetRetriever.retrieveResource(jwkSetURL); 166 } catch (IOException e) { 167 throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e); 168 } 169 JWKSet jwkSet; 170 try { 171 jwkSet = JWKSet.parse(res.getContent()); 172 } catch (java.text.ParseException e) { 173 throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e); 174 } 175 jwkSetCache.put(jwkSet); 176 return jwkSet; 177 } 178 179 180 /** 181 * Returns the JWK set URL. 182 * 183 * @return The JWK set URL. 184 */ 185 public URL getJWKSetURL() { 186 187 return jwkSetURL; 188 } 189 190 191 /** 192 * Returns the HTTP resource retriever. 193 * 194 * @return The HTTP resource retriever. 195 */ 196 public ResourceRetriever getResourceRetriever() { 197 198 return jwkSetRetriever; 199 } 200 201 202 /** 203 * Returns the configured JWK set cache. 204 * 205 * @return The JWK set cache. 206 */ 207 public JWKSetCache getJWKSetCache() { 208 209 return jwkSetCache; 210 } 211 212 213 /** 214 * Returns the cached JWK set. 215 * 216 * @return The cached JWK set, {@code null} if none or expired. 217 */ 218 public JWKSet getCachedJWKSet() { 219 220 return jwkSetCache.get(); 221 } 222 223 224 /** 225 * Returns the first specified key ID (kid) for a JWK matcher. 226 * 227 * @param jwkMatcher The JWK matcher. Must not be {@code null}. 228 * 229 * @return The first key ID, {@code null} if none. 230 */ 231 protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) { 232 233 Set<String> keyIDs = jwkMatcher.getKeyIDs(); 234 235 if (keyIDs == null || keyIDs.isEmpty()) { 236 return null; 237 } 238 239 for (String id: keyIDs) { 240 if (id != null) { 241 return id; 242 } 243 } 244 return null; // No kid in matcher 245 } 246 247 248 /** 249 * {@inheritDoc} The security context is ignored. 250 */ 251 @Override 252 public List<JWK> get(final JWKSelector jwkSelector, final C context) 253 throws RemoteKeySourceException { 254 255 // Get the JWK set, may necessitate a cache update 256 JWKSet jwkSet = jwkSetCache.get(); 257 if (jwkSetCache.requiresRefresh() || jwkSet == null) { 258 try { 259 // retrieve jwkSet by calling JWK set URL 260 jwkSet = updateJWKSetFromURL(); 261 } catch (Exception ex) { 262 if (jwkSet == null) { 263 // throw the received exception if expired. 264 throw ex; 265 } 266 } 267 } 268 269 // Run the selector on the JWK set 270 List<JWK> matches = jwkSelector.select(jwkSet); 271 272 if (! matches.isEmpty()) { 273 // Success 274 return matches; 275 } 276 277 // Refresh the JWK set if the sought key ID is not in the cached JWK set 278 279 // Looking for JWK with specific ID? 280 String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher()); 281 if (soughtKeyID == null) { 282 // No key ID specified, return no matches 283 return Collections.emptyList(); 284 } 285 286 if (jwkSet.getKeyByKeyId(soughtKeyID) != null) { 287 // The key ID exists in the cached JWK set, matching 288 // failed for some other reason, return no matches 289 return Collections.emptyList(); 290 } 291 292 // Make new HTTP GET to the JWK set URL 293 jwkSet = updateJWKSetFromURL(); 294 if (jwkSet == null) { 295 // Retrieval has failed 296 return Collections.emptyList(); 297 } 298 299 // Repeat select, return final result (success or no matches) 300 return jwkSelector.select(jwkSet); 301 } 302}