| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.geronimo.microprofile.impl.jwtauth.jwt; |
| |
| import static java.util.stream.Collectors.toSet; |
| |
| import java.net.HttpURLConnection; |
| import java.nio.charset.StandardCharsets; |
| import java.security.KeyFactory; |
| import java.security.PublicKey; |
| import java.security.Signature; |
| import java.security.spec.X509EncodedKeySpec; |
| import java.util.Arrays; |
| import java.util.Base64; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.stream.Stream; |
| |
| import javax.annotation.PostConstruct; |
| import javax.crypto.Mac; |
| import javax.crypto.SecretKey; |
| import javax.crypto.spec.SecretKeySpec; |
| import javax.enterprise.context.ApplicationScoped; |
| import javax.inject.Inject; |
| |
| import org.apache.geronimo.microprofile.impl.jwtauth.JwtException; |
| import org.apache.geronimo.microprofile.impl.jwtauth.config.GeronimoJwtAuthConfig; |
| |
| @ApplicationScoped |
| public class SignatureValidator { |
| @Inject |
| private GeronimoJwtAuthConfig config; |
| |
| private Set<String> supportedAlgorithms; |
| private String jcaProvider; |
| private boolean useCache; |
| private final ConcurrentMap<String, PublicKey> publicKeyCache = new ConcurrentHashMap<>(); |
| |
| @PostConstruct |
| private void init() { |
| useCache = Boolean.parseBoolean(config.read("public-key.cache.active", "true")); |
| supportedAlgorithms = Stream.of(config.read("header.alg.supported", "RS256").split(",")) |
| .map(String::trim) |
| .map(s -> s.toLowerCase(Locale.ROOT)) |
| .filter(s -> !s.isEmpty()) |
| .collect(toSet()); |
| jcaProvider = config.read("jca.provider", null); |
| } |
| |
| void verifySignature(final String alg, final String key, final String signingString, final String expected) { |
| final String normalizedAlg = alg.toLowerCase(Locale.ROOT); |
| if (!supportedAlgorithms.contains(normalizedAlg)) { |
| throw new JwtException("Unsupported algorithm", HttpURLConnection.HTTP_UNAUTHORIZED); |
| } |
| switch (normalizedAlg) { |
| case "rs256": |
| verifySignature(toPublicKey(key, "RSA"), signingString, expected, "SHA256withRSA"); |
| break; |
| case "rs384": |
| verifySignature(toPublicKey(key, "RSA"), signingString, expected, "SHA384withRSA"); |
| break; |
| case "rs512": |
| verifySignature(toPublicKey(key, "RSA"), signingString, expected, "SHA512withRSA"); |
| break; |
| case "hs256": |
| verifyMac(toSecretKey(key, "HmacSHA256"), signingString, expected); |
| break; |
| case "hs384": |
| verifyMac(toSecretKey(key, "HmacSHA384"), signingString, expected); |
| break; |
| case "hs512": |
| verifyMac(toSecretKey(key, "HmacSHA512"), signingString, expected); |
| break; |
| case "es256": |
| verifySignature(toPublicKey(key, "EC"), signingString, expected, "SHA256withECDSA"); |
| break; |
| case "es384": |
| verifySignature(toPublicKey(key, "EC"), signingString, expected, "SHA384withECDSA"); |
| break; |
| case "es512": |
| verifySignature(toPublicKey(key, "EC"), signingString, expected, "SHA512withECDSA"); |
| break; |
| default: |
| throw new IllegalArgumentException("Unsupported algorithm: " + normalizedAlg); |
| } |
| } |
| |
| private SecretKey toSecretKey(final String key, final String algo) { |
| return new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algo); |
| } |
| |
| private PublicKey toPublicKey(final String key, final String algo) { |
| PublicKey publicKey = useCache ? publicKeyCache.get(key) : null; |
| if (publicKey == null) { |
| final byte[] decoded = Base64.getDecoder().decode(key |
| .replace("-----BEGIN RSA KEY-----", "") |
| .replace("-----END RSA KEY-----", "") |
| .replace("-----BEGIN PUBLIC KEY-----", "") |
| .replace("-----END PUBLIC KEY-----", "") |
| .replace("-----BEGIN RSA PUBLIC KEY-----", "") |
| .replace("-----END RSA PUBLIC KEY-----", "") |
| .replace("\n", "") |
| .trim()); |
| try { |
| switch (algo) { |
| case "RSA": { |
| final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded); |
| final KeyFactory keyFactory = KeyFactory.getInstance(algo); |
| publicKey = keyFactory.generatePublic(keySpec); |
| if (useCache) { |
| publicKeyCache.putIfAbsent(key, publicKey); |
| } |
| break; |
| } |
| case "EC": // TODO |
| default: |
| throw new JwtException("Invalid signing", HttpURLConnection.HTTP_UNAUTHORIZED); |
| } |
| } catch (final Exception e) { |
| throw new JwtException("Invalid signing", HttpURLConnection.HTTP_UNAUTHORIZED); |
| } |
| } |
| return publicKey; |
| } |
| |
| private void verifyMac(final SecretKey key, final String signingString, final String expected) { |
| try { |
| final Mac signature = jcaProvider == null ? |
| Mac.getInstance(key.getAlgorithm()) : |
| Mac.getInstance(key.getAlgorithm(), jcaProvider); |
| signature.init(key); |
| signature.update(signingString.getBytes(StandardCharsets.UTF_8)); |
| if (!Arrays.equals(signature.doFinal(), Base64.getUrlDecoder().decode(expected))) { |
| invalidSignature(); |
| } |
| } catch (final Exception e) { |
| invalidSignature(); |
| } |
| } |
| |
| private void verifySignature(final PublicKey publicKey, final String signingString, final String expected, |
| final String algo) { |
| try { |
| final Signature signature = jcaProvider == null ? |
| Signature.getInstance(algo) : |
| Signature.getInstance(algo, jcaProvider); |
| signature.initVerify(publicKey); |
| signature.update(signingString.getBytes(StandardCharsets.UTF_8)); |
| if (!signature.verify(Base64.getUrlDecoder().decode(expected))) { |
| invalidSignature(); |
| } |
| } catch (final Exception e) { |
| invalidSignature(); |
| } |
| } |
| |
| private void invalidSignature() { |
| throw new JwtException("Invalid signature", HttpURLConnection.HTTP_UNAUTHORIZED); |
| } |
| } |