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.RestrictedResourceRetriever; 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. 025 * 026 * @author Vladimir Dzhuvinov 027 * @version 2016-04-10 028 */ 029@ThreadSafe 030public class RemoteJWKSet<C extends SecurityContext> implements JWKSource<C> { 031 032 033 /** 034 * The default HTTP connect timeout for JWK set retrieval, in 035 * milliseconds. Set to 250 milliseconds. 036 */ 037 public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 250; 038 039 040 /** 041 * The default HTTP read timeout for JWK set retrieval, in 042 * milliseconds. Set to 250 milliseconds. 043 */ 044 public static final int DEFAULT_HTTP_READ_TIMEOUT = 250; 045 046 047 /** 048 * The default HTTP entity size limit for JWK set retrieval, in bytes. 049 * Set to 50 KBytes. 050 */ 051 public static final int DEFAULT_HTTP_SIZE_LIMIT = 50 * 1024; 052 053 054 /** 055 * The JWK set URL. 056 */ 057 private final URL jwkSetURL; 058 059 060 /** 061 * The cached JWK set. 062 */ 063 private final AtomicReference<JWKSet> cachedJWKSet = new AtomicReference<>(); 064 065 066 /** 067 * The JWK set retriever. 068 */ 069 private final RestrictedResourceRetriever jwkSetRetriever; 070 071 072 /** 073 * Creates a new remote JWK set using the 074 * {@link DefaultResourceRetriever default HTTP resource retriever}. 075 * Starts an asynchronous thread to fetch the JWK set from the 076 * specified URL. The JWK set is cached if successfully retrieved. 077 * 078 * @param jwkSetURL The JWK set URL. Must not be {@code null}. 079 */ 080 public RemoteJWKSet(final URL jwkSetURL) { 081 this(jwkSetURL, null); 082 } 083 084 085 /** 086 * Creates a new remote JWK set. Starts an asynchronous thread to 087 * fetch the JWK set from the specified URL. The JWK set is cached if 088 * successfully retrieved. 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 RestrictedResourceRetriever 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 Thread t = new Thread() { 110 public void run() { 111 updateJWKSetFromURL(); 112 } 113 }; 114 t.setName("initial-jwk-set-retriever["+ jwkSetURL +"]"); 115 t.start(); 116 } 117 118 119 /** 120 * Updates the cached JWK set from the configured URL. 121 * 122 * @return The updated JWK set, {@code null} if retrieval failed. 123 */ 124 private JWKSet updateJWKSetFromURL() { 125 JWKSet jwkSet; 126 try { 127 Resource res = jwkSetRetriever.retrieveResource(jwkSetURL); 128 jwkSet = JWKSet.parse(res.getContent()); 129 } catch (IOException | java.text.ParseException e) { 130 return null; 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 RestrictedResourceRetriever 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 getJWKSet() { 164 JWKSet jwkSet = cachedJWKSet.get(); 165 if (jwkSet != null) { 166 return jwkSet; 167 } 168 return updateJWKSetFromURL(); 169 } 170 171 172 /** 173 * Returns the first specified key ID (kid) for a JWK matcher. 174 * 175 * @param jwkMatcher The JWK matcher. Must not be {@code null}. 176 * 177 * @return The first key ID, {@code null} if none. 178 */ 179 protected static String getFirstSpecifiedKeyID(final JWKMatcher jwkMatcher) { 180 181 Set<String> keyIDs = jwkMatcher.getKeyIDs(); 182 183 if (keyIDs == null || keyIDs.isEmpty()) { 184 return null; 185 } 186 187 for (String id: keyIDs) { 188 if (id != null) { 189 return id; 190 } 191 } 192 return null; // No kid in matcher 193 } 194 195 196 /** 197 * {@inheritDoc} The security context is ignored. 198 */ 199 @Override 200 public List<JWK> get(final JWKSelector jwkSelector, final C context) { 201 202 // Get the JWK set, may necessitate a cache update 203 JWKSet jwkSet = getJWKSet(); 204 if (jwkSet == null) { 205 // Retrieval has failed 206 return Collections.emptyList(); 207 } 208 List<JWK> matches = jwkSelector.select(jwkSet); 209 210 if (! matches.isEmpty()) { 211 // Success 212 return matches; 213 } 214 215 // Refresh the JWK set if the sought key ID is not in the cached JWK set 216 String soughtKeyID = getFirstSpecifiedKeyID(jwkSelector.getMatcher()); 217 if (soughtKeyID == null) { 218 // No key ID specified, return no matches 219 return matches; 220 } 221 if (jwkSet.getKeyByKeyId(soughtKeyID) != null) { 222 // The key ID exists in the cached JWK set, matching 223 // failed for some other reason, return no matches 224 return matches; 225 } 226 // Make new HTTP GET to the JWK set URL 227 jwkSet = updateJWKSetFromURL(); 228 if (jwkSet == null) { 229 // Retrieval has failed 230 return null; 231 } 232 // Repeat select, return final result (success or no matches) 233 return jwkSelector.select(jwkSet); 234 } 235}