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