All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.uid2.shared.attest.JwtService Maven / Gradle / Ivy

package com.uid2.shared.attest;

import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.Optional;

import com.uid2.shared.Const;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.api.client.json.webtoken.JsonWebToken;
import com.google.auth.oauth2.TokenVerifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JwtService {

    private static final Logger LOGGER = LoggerFactory.getLogger(JwtService.class);
    private final JsonObject config;

    private final HashSet publicKeys = new HashSet<>();


    public JwtService(JsonObject config) {
        this.config = config;
        String keysData = config.getString(Const.Config.AwsKmsJwtSigningPublicKeysProp, "");
        String[] keys = keysData.split(",");
        if (keysData.isBlank() || keys == null || keys.length == 0) {
            LOGGER.info("Unable to read public keys from the configuration. JWTs can not be verified.");
            return;
        }
        this.parsePublicKeysFromConfig(keys);
    }

    /*
     * Validates the jwt token signature is valid, and then has the expected
     * iss and aud values.
     * Checks the config value for the public keys. Will loop though all the keys in the config
     * so that when a key is rotated, or a new one give, we can still validate older tokens.
     */
    public JwtValidationResponse validateJwt(String jwt, String audience, String issuer) throws ValidationException {
        if (audience == null || audience.isBlank()) {
            throw new IllegalArgumentException("Audience can not be empty");
        }

        if (issuer == null || issuer.isBlank()) {
            throw new IllegalArgumentException("Issuer can not be empty");
        }

        JwtValidationResponse response = new JwtValidationResponse(false);

        if (this.publicKeys.isEmpty()) {
            LOGGER.error("Unable to get public keys. Validation can not continue. Check the configuration for the service and ensure all valid public keys are specified in the property: {}", Const.Config.AwsKmsJwtSigningPublicKeysProp);
            throw new ValidationException(Optional.of("Unable to get public keys. Validation can not continue"));
        }

        Exception lastException = null;

        for (PublicKey key : this.publicKeys) {
            var tokenVerifier = TokenVerifier.newBuilder()
                    .setPublicKey(key)
                    .setAudience(audience)
                    .setIssuer(issuer)
                    .build();

            try {
                // verify checks that the token has not expired
                JsonWebSignature signature = tokenVerifier.verify(jwt);
                JsonWebToken.Payload webToken = signature.getPayload();
                response = new JwtValidationResponse(true)
                        .withSubject(webToken.get("sub").toString())
                        .withRoles(webToken.get("roles").toString())
                        .withEnclaveId(webToken.get("enclaveId").toString())
                        .withEnclaveType(webToken.get("enclaveType").toString())
                        .withSiteId(Integer.valueOf(webToken.get("siteId").toString()))
                        .withOperatorVersion(webToken.get("operatorVersion").toString())
                        .withAudience(webToken.get("aud").toString());

                // return the first verified response
                return response;
            } catch (TokenVerifier.VerificationException e) {
                LOGGER.info("Error validating JWT. Error message: {}", e.getMessage());
                lastException = e;
            } catch (Exception e) {
                LOGGER.warn("Error thrown verifying token", e);
                lastException = e;
            }
        }

        response.setValidationException(lastException);
        return response;
    }

    private void parsePublicKeysFromConfig(String[] publicKeys) {
        Arrays.stream(publicKeys).forEach(key -> {
            try {
                if (key != null && !key.isBlank()) {
                    PublicKey publicKey = this.getPublicKeyFromString(key);
                    if (publicKey != null) {
                        this.publicKeys.add(publicKey);
                    }
                }
            } catch (ValidationException e) {
                LOGGER.error("Unable to parse Public Key string that starts with: {}", key.substring(0, key.length() > 15 ? 15 : key.length()));
            }
        });
    }

    private PublicKey getPublicKeyFromString(String keyString) throws ValidationException {
        try {
            String publicKeyPEM = keyString
                    .replace("-----BEGIN PUBLIC KEY-----", "")
                    .replaceAll(System.lineSeparator(), "")
                    .replaceAll("\n", "")
                    .replace("-----END PUBLIC KEY-----", "");
            byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);

            PublicKey key = keyFactory.generatePublic(keySpec);
            return key;

        } catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException ex) {
            LOGGER.error("Error creating Public key from configuration:", ex);
            throw new ValidationException(Optional.of("Error creating Public key from configuration"), ex);
        }
    }

    public class ValidationException extends Exception {
        public ValidationException(Optional message) {
            super(message == null ? "Validation Error" : message.orElse("Validation Error"));
        }
        public ValidationException(Optional message, Exception ex) {
            super(message == null ? "Validation Error" : message.orElse("Validation Error"), ex);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy