/* * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.security.oauth2.provider.token.store.jwk; import org.springframework.security.jwt.codec.Codecs; import org.springframework.security.jwt.crypto.sign.RsaVerifier; import org.springframework.security.jwt.crypto.sign.SignatureVerifier; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyFactory; import java.security.interfaces.RSAPublicKey; import java.security.spec.RSAPublicKeySpec; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * A source for JSON Web Key(s) (JWK) that is solely responsible for fetching (and caching) * the JWK Set (a set of JWKs) from the URL supplied to the constructor. * * @see JwkSetConverter * @see JwkDefinition * @see SignatureVerifier * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517#page-10">JWK Set Format</a> * * @author Joe Grandja */ class JwkDefinitionSource { private final URL jwkSetUrl; private final Map<String, JwkDefinitionHolder> jwkDefinitions = new ConcurrentHashMap<String, JwkDefinitionHolder>(); private static final JwkSetConverter jwkSetConverter = new JwkSetConverter(); /** * Creates a new instance using the provided URL as the location for the JWK Set. * * @param jwkSetUrl the JWK Set URL */ JwkDefinitionSource(String jwkSetUrl) { try { this.jwkSetUrl = new URL(jwkSetUrl); } catch (MalformedURLException ex) { throw new IllegalArgumentException("Invalid JWK Set URL: " + ex.getMessage(), ex); } } /** * Returns the JWK definition matching the provided keyId ("kid"). * * @param keyId the Key ID ("kid") * @return the matching {@link JwkDefinition} or null if not found */ JwkDefinition getDefinition(String keyId) { JwkDefinition result = null; JwkDefinitionHolder jwkDefinitionHolder = this.jwkDefinitions.get(keyId); if (jwkDefinitionHolder != null) { result = jwkDefinitionHolder.getJwkDefinition(); } return result; } /** * Returns the JWK definition matching the provided keyId ("kid"). * If the JWK definition is not available in the internal cache then {@link #loadJwkDefinitions(URL)} * will be called (to re-load the cache) and then followed-up with a second attempt to locate the JWK definition. * * @param keyId the Key ID ("kid") * @return the matching {@link JwkDefinition} or null if not found */ JwkDefinition getDefinitionLoadIfNecessary(String keyId) { JwkDefinition result = this.getDefinition(keyId); if (result != null) { return result; } this.jwkDefinitions.clear(); this.jwkDefinitions.putAll(loadJwkDefinitions(this.jwkSetUrl)); return this.getDefinition(keyId); } /** * Returns the {@link SignatureVerifier} matching the provided keyId ("kid"). * * @param keyId the Key ID ("kid") * @return the matching {@link SignatureVerifier} or null if not found */ SignatureVerifier getVerifier(String keyId) { SignatureVerifier result = null; JwkDefinition jwkDefinition = this.getDefinitionLoadIfNecessary(keyId); if (jwkDefinition != null) { result = this.jwkDefinitions.get(keyId).getSignatureVerifier(); } return result; } /** * Fetches the JWK Set from the provided <code>URL</code> and * returns a <code>Map</code> keyed by the JWK keyId ("kid") * and mapped to an association of the {@link JwkDefinition} and {@link SignatureVerifier}. * Uses a {@link JwkSetConverter} to convert the JWK Set URL source to a set of {@link JwkDefinition}(s) * followed by the instantiation of a {@link SignatureVerifier} which is associated to it's {@link JwkDefinition}. * * @param jwkSetUrl the JWK Set URL * @return a <code>Map</code> keyed by the JWK keyId and mapped to an association of {@link JwkDefinition} and {@link SignatureVerifier} * @see JwkSetConverter */ static Map<String, JwkDefinitionHolder> loadJwkDefinitions(URL jwkSetUrl) { InputStream jwkSetSource; try { jwkSetSource = jwkSetUrl.openStream(); } catch (IOException ex) { throw new JwkException("An I/O error occurred while reading from the JWK Set source: " + ex.getMessage(), ex); } Set<JwkDefinition> jwkDefinitionSet = jwkSetConverter.convert(jwkSetSource); Map<String, JwkDefinitionHolder> jwkDefinitions = new LinkedHashMap<String, JwkDefinitionHolder>(); for (JwkDefinition jwkDefinition : jwkDefinitionSet) { if (JwkDefinition.KeyType.RSA.equals(jwkDefinition.getKeyType())) { jwkDefinitions.put(jwkDefinition.getKeyId(), new JwkDefinitionHolder(jwkDefinition, createRsaVerifier((RsaJwkDefinition) jwkDefinition))); } } return jwkDefinitions; } private static RsaVerifier createRsaVerifier(RsaJwkDefinition rsaDefinition) { RsaVerifier result; try { BigInteger modulus = new BigInteger(1, Codecs.b64UrlDecode(rsaDefinition.getModulus())); BigInteger exponent = new BigInteger(1, Codecs.b64UrlDecode(rsaDefinition.getExponent())); RSAPublicKey rsaPublicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") .generatePublic(new RSAPublicKeySpec(modulus, exponent)); result = new RsaVerifier(rsaPublicKey, rsaDefinition.getAlgorithm().standardName()); } catch (Exception ex) { throw new JwkException("An error occurred while creating a RSA Public Key Verifier for " + rsaDefinition.getKeyId() + " : " + ex.getMessage(), ex); } return result; } static class JwkDefinitionHolder { private final JwkDefinition jwkDefinition; private final SignatureVerifier signatureVerifier; private JwkDefinitionHolder(JwkDefinition jwkDefinition, SignatureVerifier signatureVerifier) { this.jwkDefinition = jwkDefinition; this.signatureVerifier = signatureVerifier; } private JwkDefinition getJwkDefinition() { return jwkDefinition; } private SignatureVerifier getSignatureVerifier() { return signatureVerifier; } } }