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