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