001package com.nimbusds.jose.jwk.source; 002 003 004import java.io.IOException; 005import java.net.URL; 006import java.util.Collections; 007import java.util.List; 008import java.util.Set; 009import java.util.concurrent.atomic.AtomicReference; 010 011import com.nimbusds.jose.RemoteKeySourceException; 012import com.nimbusds.jose.jwk.JWK; 013import com.nimbusds.jose.jwk.JWKMatcher; 014import com.nimbusds.jose.jwk.JWKSelector; 015import com.nimbusds.jose.jwk.JWKSet; 016import com.nimbusds.jose.proc.SecurityContext; 017import com.nimbusds.jose.util.DefaultResourceRetriever; 018import com.nimbusds.jose.util.Resource; 019import com.nimbusds.jose.util.ResourceRetriever; 020import net.jcip.annotations.ThreadSafe; 021 022 023/** 024 * Remote JSON Web Key (JWK) source specified by a JWK set URL. The retrieved 025 * JWK set is cached to minimise network calls. The cache is updated whenever 026 * the key selector tries to get a key with an unknown ID. 027 * 028 * @author Vladimir Dzhuvinov 029 * @version 2016-05-28 030 */ 031@ThreadSafe 032public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> { 033 034 035 /** 036 * The default HTTP connect timeout for JWK set retrieval, in 037 * milliseconds. Set to 250 milliseconds. 038 */ 039 public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 250; 040 041 042 /** 043 * The default HTTP read timeout for JWK set retrieval, in 044 * milliseconds. Set to 250 milliseconds. 045 */ 046 public static final int DEFAULT_HTTP_READ_TIMEOUT = 250; 047 048 049 /** 050 * The default HTTP entity size limit for JWK set retrieval, in bytes. 051 * Set to 50 KBytes. 052 */ 053 public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024; 054 055 056 /** 057 * The JWK set URL. 058 */ 059 private final URL jwkSetURL; 060 061 062 /** 063 * The cached JWK set. 064 */ 065 private final AtomicReference<JWKSet> cachedJWKSet = new AtomicReference<>(); 066 067 068 /** 069 * The JWK set retriever. 070 */ 071 private final ResourceRetriever jwkSetRetriever; 072 073 074 /** 075 * Creates a new remote JWK set using the 076 * {@link DefaultResourceRetriever default HTTP resource retriever}, 077 * with a HTTP connect timeout set to 250 ms, HTTP read timeout set to 078 * 250 ms and a 50 KByte size limit. 079 * 080 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 081 */ 082 public RemoteJWKSet(final URL jwkSetURL) { 083 this(jwkSetURL, null); 084 } 085 086 087 /** 088 * Creates a new remote JWK set. 089 * 090 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 091 * @param resourceRetriever The HTTP resource retriever to use, 092 * {@code null} to use the 093 * {@link DefaultResourceRetriever default 094 * one}. 095 */ 096 public RemoteJWKSet(final URL jwkSetURL, 097 final ResourceRetriever resourceRetriever) { 098 if (jwkSetURL == null) { 099 throw new IllegalArgumentException("The JWK set URL must not be null"); 100 } 101 this.jwkSetURL = jwkSetURL; 102 103 if (resourceRetriever != null) { 104 jwkSetRetriever = resourceRetriever; 105 } else { 106 jwkSetRetriever = new DefaultResourceRetriever(DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT, DEFAULT_HTTP_SIZE_LIMIT); 107 } 108 } 109 110 111 /** 112 * Updates the cached JWK set from the configured URL. 113 * 114 * @return The updated JWK set. 115 * 116 * @throws RemoteKeySourceException If JWK retrieval failed. 117 */ 118 private JWKSet updateJWKSetFromURL() 119 throws RemoteKeySourceException { 120 Resource res; 121 try { 122 res = jwkSetRetriever.retrieveResource(jwkSetURL); 123 } catch (IOException e) { 124 throw new RemoteKeySourceException("Couldn't retrieve remote JWK set: " + e.getMessage(), e); 125 } 126 JWKSet jwkSet; 127 try { 128 jwkSet = JWKSet.parse(res.getContent()); 129 } catch (java.text.ParseException e) { 130 throw new RemoteKeySourceException("Couldn't parse remote JWK set: " + e.getMessage(), e); 131 } 132 cachedJWKSet.set(jwkSet); 133 return jwkSet; 134 } 135 136 137 /** 138 * Returns the JWK set URL. 139 * 140 * @return The JWK set URL. 141 */ 142 public URL getJWKSetURL() { 143 return jwkSetURL; 144 } 145 146 147 /** 148 * Returns the HTTP resource retriever. 149 * 150 * @return The HTTP resource retriever. 151 */ 152 public ResourceRetriever getResourceRetriever() { 153 154 return jwkSetRetriever; 155 } 156 157 158 /** 159 * Returns the cached JWK set. 160 * 161 * @return The cached JWK set, {@code null} if none. 162 */ 163 public JWKSet getCachedJWKSet() { 164 return cachedJWKSet.get(); 165 } 166 167 168 /** 169 * Returns the first specified key ID (kid) for a JWK matcher. 170 * 171 * @param jwkMatcher The JWK matcher. Must not be {@code null}. 172 * 173 * @return The first key ID, {@code null} if none. 174 */ 175 protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) { 176 177 Set<String> keyIDs = jwkMatcher.getKeyIDs(); 178 179 if (keyIDs == null || keyIDs.isEmpty()) { 180 return null; 181 } 182 183 for (String id: keyIDs) { 184 if (id != null) { 185 return id; 186 } 187 } 188 return null; // No kid in matcher 189 } 190 191 192 /** 193 * {@inheritDoc} The security context is ignored. 194 */ 195 @Override 196 public List<JWK> get(final JWKSelector jwkSelector, final C context) 197 throws RemoteKeySourceException { 198 199 // Get the JWK set, may necessitate a cache update 200 JWKSet jwkSet = cachedJWKSet.get(); 201 if (jwkSet == null) { 202 jwkSet = updateJWKSetFromURL(); 203 } 204 205 // Run the selector on the JWK set 206 List<JWK> matches = jwkSelector.select(jwkSet); 207 208 if (! matches.isEmpty()) { 209 // Success 210 return matches; 211 } 212 213 // Refresh the JWK set if the sought key ID is not in the cached JWK set 214 215 // Looking for JWK with specific ID? 216 String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher()); 217 if (soughtKeyID == null) { 218 // No key ID specified, return no matches 219 return Collections.emptyList(); 220 } 221 222 if (jwkSet.getKeyByKeyId(soughtKeyID) != null) { 223 // The key ID exists in the cached JWK set, matching 224 // failed for some other reason, return no matches 225 return Collections.emptyList(); 226 } 227 228 // Make new HTTP GET to the JWK set URL 229 jwkSet = updateJWKSetFromURL(); 230 if (jwkSet == null) { 231 // Retrieval has failed 232 return Collections.emptyList(); 233 } 234 235 // Repeat select, return final result (success or no matches) 236 return jwkSelector.select(jwkSet); 237 } 238}