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