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

cn.home1.cloud.config.server.security.ConfigSecurity Maven / Gradle / Ivy

The newest version!
package cn.home1.cloud.config.server.security;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.Boolean.FALSE;
import static org.apache.commons.lang3.RandomStringUtils.random;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.joda.time.DateTime.now;

import com.google.common.collect.ImmutableMap;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;

import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.config.server.encryption.TextEncryptorLocator;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.security.SecureRandom;
import java.util.Iterator;
import java.util.regex.Pattern;

import javax.annotation.PostConstruct;

@Slf4j
public class ConfigSecurity {

    static final String TOKEN_PREFIX = "{token}";
    private static final int BCRYPT_STRENGTH = -1;
    private static final Pattern CONCAT_PATTERN = Pattern.compile(":");
    private static final String TOKEN_CLAIM = "encrypted";
    private static final int TOKEN_EXPIRE_DAYS = 365 * 5;
    private static final String TOKEN_ISSUER = "config-server";
    private final PasswordEncoder passwordEncoder;

    @Setter
    private TextEncryptor encryptor;

    private Algorithm hmacAlgorithm;

    @Setter
    @Value("${spring.cloud.config.encrypt.hmac-secret:secret}")
    private String hmacSecret;

    private JWTVerifier hmacVerifier;

    @Setter
    @Value("${security.basic.enabled:true}")
    private Boolean securityEnabled;

    public ConfigSecurity() {
        this.passwordEncoder = new BCryptPasswordEncoder(BCRYPT_STRENGTH);
    }

    static String decryptProperty(final String value, final TextEncryptor encryptor) {
        final String result;
        if (isNotBlank(value) && value.startsWith("{cipher}")) {
            final String base64 = value.replaceAll("\\{[^}]+\\}", "");
            result = encryptor.decrypt(base64);
        } else {
            result = value;
        }
        return result;
    }

    /**
     * By default, this is a {@link org.springframework.cloud.config.server.encryption.KeyStoreTextEncryptorLocator} instance.
     */
    @Autowired
    public void setEncryptorLocator(final TextEncryptorLocator encryptorLocator) {
        this.encryptor = encryptorLocator.locate(ImmutableMap.of());
    }

    @PostConstruct
    @SneakyThrows
    public void init() {
        this.hmacAlgorithm = Algorithm.HMAC256(this.hmacSecret);
        this.hmacVerifier = JWT.require(this.hmacAlgorithm)
            .withIssuer(TOKEN_ISSUER)
            .build(); //Reusable verifier instance
    }

    /**
     * Generate a password (token) valid only for given application to access a given parent
     *
     * @param application       from child config
     * @param parentApplication from child config
     * @param parentPassword    from child config
     * @return token valid for application only
     */
    public String encryptParentPassword(final String application, final String parentApplication, final String parentPassword) {
        //if (!this.securityEnabled) {
        //  return "";
        //}

        checkArgument(isNotBlank(application), "blank application");
        checkArgument(isNotBlank(application), "blank parentApplication");
        checkArgument(isNotBlank(application), "blank parentPassword");

        // digest/hash
        // random string (length 16)
        final String randomString = random(16, 0, 0, true, true, null, new SecureRandom());
        final String encodedApplication = this.passwordEncoder.encode(application);
        final String encodedParentApplication = this.passwordEncoder.encode(parentApplication);
        final String encodedParentPassword = this.passwordEncoder.encode(parentPassword);

        // concat
        final String plainText = randomString + ":" + encodedApplication + ":" + encodedParentApplication + ":" + encodedParentPassword;

        // encrypt
        final String encrypted = this.encryptor.encrypt(plainText);

        // sign
        final String token = JWT.create()
            .withIssuer(TOKEN_ISSUER)
            .withClaim(TOKEN_CLAIM, encrypted)
            .withExpiresAt(now().plusDays(TOKEN_EXPIRE_DAYS).toDate())
            .sign(this.hmacAlgorithm);

        log.info("Granted parent ({}) config access for application '{}', token: '{}'.", parentApplication, application, token);

        return TOKEN_PREFIX + token;
    }

    /**
     * Verify privilege
     *
     * @param application            from context (URL path)
     * @param parentApplication      from child config
     * @param token                  from child config
     * @param expectedParentPassword from parent config (may need to decrypt before verify)
     * @return whether token, application in token and password in token valid
     */
    public Boolean verifyParentPassword(final String application, final String parentApplication, final String token, final String expectedParentPassword) {
        final Boolean result;
        if (!this.securityEnabled) {
            result = isNotEmpty(application) && isNotEmpty(parentApplication);
        } else {
            final String rawParentPassword = this.decryptProperty(expectedParentPassword);

            if (isNotEmpty(application) && isNotEmpty(token)) {
                if (token.startsWith(TOKEN_PREFIX)) {
                    final String rawToken = token.replace(TOKEN_PREFIX, "");
                    // verify signature
                    final String encrypted = this.hmacVerifier.verify(rawToken).getClaim(TOKEN_CLAIM).asString();

                    // decrypt
                    final String plainText = this.encryptor.decrypt(encrypted);

                    // split
                    final Iterator parts = CONCAT_PATTERN.splitAsStream(plainText).iterator();
                    final String random = parts.next();
                    final String encodedApplication = parts.next();
                    final String encodedParentApplication = parts.next();
                    final String encodedParentPassword = parts.next();

                    try {
                        result = this.passwordEncoder.matches(application, encodedApplication) &&
                            this.passwordEncoder.matches(parentApplication, encodedParentApplication) &&
                            this.passwordEncoder.matches(rawParentPassword, encodedParentPassword);
                    } catch (final Exception ignored) {
                        return FALSE;
                    }
                } else {
                    final String rawToken = this.decryptProperty(token);
                    result = this.isPasswordMatch(rawParentPassword, rawToken);
                }
            } else {
                if (isNotEmpty(application) && isNotEmpty(parentApplication)) {
                    result = this.isPasswordMatch(rawParentPassword, token);
                } else {
                    result = FALSE;
                }
            }
        }
        return result;
    }

    Boolean isPasswordMatch(final String expected, final String actual) {
        return (expected != null ? expected : "").equals(actual != null ? actual : "");
    }

    String decryptProperty(final String value) {
        return decryptProperty(value, this.encryptor);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy