/* * 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 com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import org.springframework.core.convert.converter.Converter; import org.springframework.util.StringUtils; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import static org.springframework.security.oauth2.provider.token.store.jwk.JwkAttributes.*; /** * A {@link Converter} that converts the supplied <code>InputStream</code> to a <code>Set</code> of {@link JwkDefinition}(s). * The source of the <code>InputStream</code> <b>must be</b> a JWK Set representation which is a JSON object * that has a "keys" member and its value is an array of JWKs. * <br> * <br> * * <b>NOTE:</b> The Key Type ("kty") currently supported by this {@link Converter} is {@link JwkDefinition.KeyType#RSA}. * <br> * <br> * * @see JwkDefinition * @see <a target="_blank" href="https://tools.ietf.org/html/rfc7517#page-10">JWK Set Format</a> * * @author Joe Grandja */ class JwkSetConverter implements Converter<InputStream, Set<JwkDefinition>> { private final JsonFactory factory = new JsonFactory(); /** * Converts the supplied <code>InputStream</code> to a <code>Set</code> of {@link JwkDefinition}(s). * * @param jwkSetSource the source for the JWK Set * @return a <code>Set</code> of {@link JwkDefinition}(s) * @throws JwkException if the JWK Set JSON object is invalid */ @Override public Set<JwkDefinition> convert(InputStream jwkSetSource) { Set<JwkDefinition> jwkDefinitions; JsonParser parser = null; try { parser = this.factory.createParser(jwkSetSource); if (parser.nextToken() != JsonToken.START_OBJECT) { throw new JwkException("Invalid JWK Set Object."); } if (parser.nextToken() != JsonToken.FIELD_NAME) { throw new JwkException("Invalid JWK Set Object."); } if (!parser.getCurrentName().equals(KEYS)) { throw new JwkException("Invalid JWK Set Object. The JWK Set MUST have a " + KEYS + " attribute."); } if (parser.nextToken() != JsonToken.START_ARRAY) { throw new JwkException("Invalid JWK Set Object. The JWK Set MUST have an array of JWK(s)."); } jwkDefinitions = new LinkedHashSet<JwkDefinition>(); Map<String, String> attributes = new HashMap<String, String>(); while (parser.nextToken() == JsonToken.START_OBJECT) { while (parser.nextToken() == JsonToken.FIELD_NAME) { String attributeName = parser.getCurrentName(); parser.nextToken(); String attributeValue = parser.getValueAsString(); attributes.put(attributeName, attributeValue); } JwkDefinition jwkDefinition = this.createJwkDefinition(attributes); if (!jwkDefinitions.add(jwkDefinition)) { throw new JwkException("Duplicate JWK found in Set: " + jwkDefinition.getKeyId() + " (" + KEY_ID + ")"); } attributes.clear(); } } catch (IOException ex) { throw new JwkException("An I/O error occurred while reading the JWK Set: " + ex.getMessage(), ex); } finally { try { if (parser != null) parser.close(); } catch (IOException ex) { } } return jwkDefinitions; } /** * Creates a {@link JwkDefinition} based on the supplied attributes. * * @param attributes the attributes used to create the {@link JwkDefinition} * @return a {@link JwkDefinition} * @throws JwkException if the Key Type ("kty") attribute value is not {@link JwkDefinition.KeyType#RSA} */ private JwkDefinition createJwkDefinition(Map<String, String> attributes) { JwkDefinition.KeyType keyType = JwkDefinition.KeyType.fromValue(attributes.get(KEY_TYPE)); if (!JwkDefinition.KeyType.RSA.equals(keyType)) { throw new JwkException((keyType != null ? keyType.value() : "unknown") + " (" + KEY_TYPE + ") is currently not supported." + " Valid values for '" + KEY_TYPE + "' are: " + JwkDefinition.KeyType.RSA.value()); } return this.createRsaJwkDefinition(attributes); } /** * Creates a {@link RsaJwkDefinition} based on the supplied attributes. * * @param attributes the attributes used to create the {@link RsaJwkDefinition} * @return a {@link JwkDefinition} representation of a RSA Key * @throws JwkException if at least one attribute value is missing or invalid for a RSA Key */ private JwkDefinition createRsaJwkDefinition(Map<String, String> attributes) { // kid String keyId = attributes.get(KEY_ID); if (!StringUtils.hasText(keyId)) { throw new JwkException(KEY_ID + " is a required attribute for a JWK."); } // use JwkDefinition.PublicKeyUse publicKeyUse = JwkDefinition.PublicKeyUse.fromValue(attributes.get(PUBLIC_KEY_USE)); if (!JwkDefinition.PublicKeyUse.SIG.equals(publicKeyUse)) { throw new JwkException((publicKeyUse != null ? publicKeyUse.value() : "unknown") + " (" + PUBLIC_KEY_USE + ") is currently not supported."); } // alg JwkDefinition.CryptoAlgorithm algorithm = JwkDefinition.CryptoAlgorithm.fromHeaderParamValue(attributes.get(ALGORITHM)); if (!JwkDefinition.CryptoAlgorithm.RS256.equals(algorithm) && !JwkDefinition.CryptoAlgorithm.RS384.equals(algorithm) && !JwkDefinition.CryptoAlgorithm.RS512.equals(algorithm)) { throw new JwkException((algorithm != null ? algorithm.standardName() : "unknown") + " (" + ALGORITHM + ") is currently not supported."); } // n String modulus = attributes.get(RSA_PUBLIC_KEY_MODULUS); if (!StringUtils.hasText(modulus)) { throw new JwkException(RSA_PUBLIC_KEY_MODULUS + " is a required attribute for a RSA JWK."); } // e String exponent = attributes.get(RSA_PUBLIC_KEY_EXPONENT); if (!StringUtils.hasText(exponent)) { throw new JwkException(RSA_PUBLIC_KEY_EXPONENT + " is a required attribute for a RSA JWK."); } RsaJwkDefinition jwkDefinition = new RsaJwkDefinition( keyId, publicKeyUse, algorithm, modulus, exponent); return jwkDefinition; } }