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