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

fish.payara.microprofile.jwtauth.eesecurity.SignedJWTIdentityStore Maven / Gradle / Ivy

There is a newer version: 7.2024.1.Alpha1
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) [2017-2023] Payara Foundation and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://github.com/payara/Payara/blob/master/LICENSE.txt
 * See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * The Payara Foundation designates this particular file as subject to the "Classpath"
 * exception as provided by the Payara Foundation in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package fish.payara.microprofile.jwtauth.eesecurity;

import fish.payara.microprofile.jwtauth.jwt.JsonWebTokenImpl;
import fish.payara.microprofile.jwtauth.jwt.JwtTokenParser;
import jakarta.security.enterprise.identitystore.CredentialValidationResult;
import jakarta.security.enterprise.identitystore.IdentityStore;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.jwt.config.Names;

import java.io.IOException;
import java.net.URL;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Logger;

import static jakarta.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT;
import static java.lang.Thread.currentThread;
import static java.util.logging.Level.INFO;
import static org.eclipse.microprofile.jwt.config.Names.ISSUER;

/**
 * Identity store capable of asserting that a signed JWT token is valid
 * according to the MP-JWT 1.1 spec.
 *
 * @author Arjan Tijms
 */
public class SignedJWTIdentityStore implements IdentityStore {

    private static final Logger LOGGER = Logger.getLogger(SignedJWTIdentityStore.class.getName());

    private final String acceptedIssuer;
    private final Optional> allowedAudience;
    private final Optional enabledNamespace;
    private final Optional customNamespace;
    private final Optional disableTypeVerification;

    private final Config config;
    private final JwtPublicKeyStore publicKeyStore;
    private final JwtPrivateKeyStore privateKeyStore;

    private final boolean isEncryptionRequired;

    private Map> optionalConfigProperty;

    public SignedJWTIdentityStore() {
        config = ConfigProvider.getConfig();

        Optional properties = readVendorProperties();
        acceptedIssuer = readVendorIssuer(properties)
                .orElseGet(() -> config.getOptionalValue(ISSUER, String.class)
                .orElseThrow(() -> new IllegalStateException("No issuer found")));
        Optional allowedAudienceOpt = readAudience(properties);
        if (!allowedAudienceOpt.isPresent()) {
            allowedAudienceOpt = config.getOptionalValue(Names.AUDIENCES, String.class);
        }
        allowedAudience = allowedAudienceOpt.map(str -> Arrays.asList(str.split(",")));

        enabledNamespace = readEnabledNamespace(properties);
        customNamespace = readCustomNamespace(properties);
        disableTypeVerification = readDisableTypeVerification(properties);
        Optional publicKeyLocation = readConfigOptional(Names.VERIFIER_PUBLIC_KEY_LOCATION, properties, config); //mp.jwt.verifyAndParseEncryptedJWT.publickey.location
        Optional publicKey = readConfigOptional(Names.VERIFIER_PUBLIC_KEY, properties, config); //mp.jwt.verifyAndParseEncryptedJWT.publickey
        Optional decryptKeyLocation = readConfigOptional(Names.DECRYPTOR_KEY_LOCATION, properties, config); //mp.jwt.decrypt.key.location
        publicKeyStore = new JwtPublicKeyStore(readPublicKeyCacheTTL(properties), publicKeyLocation);
        privateKeyStore = new JwtPrivateKeyStore(readPublicKeyCacheTTL(properties), decryptKeyLocation);

        // Signing is required by default, it doesn't parse if not signed
        isEncryptionRequired = decryptKeyLocation.isPresent();
        setOptionalConfigProperty(properties);
    }

    public CredentialValidationResult validate(SignedJWTCredential signedJWTCredential) {
        final JwtTokenParser jwtTokenParser = new JwtTokenParser(enabledNamespace, customNamespace, disableTypeVerification);
        try {
            JsonWebTokenImpl jsonWebToken = jwtTokenParser.parse(signedJWTCredential.getSignedJWT(),
                    isEncryptionRequired, publicKeyStore, acceptedIssuer, privateKeyStore, optionalConfigProperty);

            // verifyAndParseEncryptedJWT audience
            final Set recipientsOfThisJWT = jsonWebToken.getAudience();
            // find if any recipient is in the allowed audience
            Boolean recipientInAudience = allowedAudience
                    .map(recipient -> recipient.stream().anyMatch(a -> recipientsOfThisJWT != null && recipientsOfThisJWT.contains(a)))
                    .orElse(true);
            if (!recipientInAudience) {
                throw new Exception("The intended audience " + recipientsOfThisJWT + " is not a part of allowed audience.");
            }

            Set groups = new HashSet<>();
            Collection groupClaims = jsonWebToken.getClaim("groups");
            if (groupClaims != null) {
                groups.addAll(groupClaims);
            }

            return new CredentialValidationResult(jsonWebToken, groups);

        } catch (Exception e) {
            LOGGER.log(INFO, "Exception trying to parse JWT token.", e);
        }

        return INVALID_RESULT;
    }

    public static Optional readVendorProperties() {
        URL mpJwtResource = currentThread().getContextClassLoader().getResource("/payara-mp-jwt.properties");
        Properties properties = null;
        if (mpJwtResource != null) {
            try {
                properties = new Properties();
                properties.load(mpJwtResource.openStream());
            } catch (IOException e) {
                throw new IllegalStateException("Failed to load Vendor properties from resource file", e);
            }
        }
        return Optional.ofNullable(properties);
    }

    private Optional readVendorIssuer(Optional properties) {
        return properties.isPresent() ? Optional.ofNullable(properties.get().getProperty("accepted.issuer")) : Optional.empty();
    }

    private Optional readEnabledNamespace(Optional properties) {
        return properties.isPresent() ? Optional.ofNullable(Boolean.valueOf(properties.get().getProperty("enable.namespace", "false"))) : Optional.empty();
    }

    private Optional readCustomNamespace(Optional properties) {
        return properties.isPresent() ? Optional.ofNullable(properties.get().getProperty("custom.namespace", null)) : Optional.empty();
    }

    private Optional readDisableTypeVerification(Optional properties) {
        return properties.isPresent() ? Optional.ofNullable(Boolean.valueOf(properties.get().getProperty("disable.type.verification", "false"))) : Optional.empty();
    }
    
    private Duration readPublicKeyCacheTTL(Optional properties) {
        return properties
        		.map(props -> props.getProperty("publicKey.cache.ttl"))
        		.map(Long::valueOf)
        		.map(Duration::ofMillis)
        		.orElseGet( () -> Duration.ofMinutes(5));
    }
    
    private Optional readAudience(Optional properties) {
        return properties.isPresent() ? Optional.ofNullable(properties.get().getProperty(Names.AUDIENCES)) : Optional.empty();
    }

    /**
     * Read configuration from Vendor or server or return default value.
     */
    public static String readConfig(String key, Optional properties, Config config, String defaultValue) {
        Optional valueOpt = readConfigOptional(key, properties, config);
        return valueOpt.orElse(defaultValue);
    }

    public static Optional readConfigOptional(String key, Optional properties, Config config) {
        Optional valueOpt = properties.map(props -> props.getProperty(key));
        if (!valueOpt.isPresent()) {
            valueOpt = config.getOptionalValue(key, String.class);
        }
        return valueOpt;
    }

    private void setOptionalConfigProperty(Optional properties) {
        optionalConfigProperty = new HashMap<>();
        optionalConfigProperty.put(Names.TOKEN_AGE, readConfigOptional(Names.TOKEN_AGE, properties, config)); // mp.jwt.verify.token.age
        optionalConfigProperty.put(Names.CLOCK_SKEW, readConfigOptional(Names.CLOCK_SKEW, properties, config)); // mp.jwt.verify.clock.skew
        optionalConfigProperty.put(Names.DECRYPTOR_KEY_ALGORITHM, readConfigOptional(Names.DECRYPTOR_KEY_ALGORITHM, properties, config)); //mp.jwt.decrypt.key.algorithm
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy