com.centurylink.mdw.services.util.AuthUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.centurylink.mdw.services.util.AuthUtils.java

Source

/*
 * Copyright (C) 2017 CenturyLink, Inc.
 *
 * 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 com.centurylink.mdw.services.util;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.codec.binary.Base64;
import org.json.JSONObject;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.Verification;
import com.centurylink.mdw.app.ApplicationContext;
import com.centurylink.mdw.auth.Authenticator;
import com.centurylink.mdw.auth.LdapAuthenticator;
import com.centurylink.mdw.auth.MdwSecurityException;
import com.centurylink.mdw.cache.CacheService;
import com.centurylink.mdw.cache.impl.PackageCache;
import com.centurylink.mdw.config.PropertyManager;
import com.centurylink.mdw.constant.PropertyNames;
import com.centurylink.mdw.java.CompiledJavaCache;
import com.centurylink.mdw.model.listener.Listener;
import com.centurylink.mdw.services.cache.CacheRegistration;
import com.centurylink.mdw.util.HmacSha1Signature;
import com.centurylink.mdw.util.StringHelper;
import com.centurylink.mdw.util.log.LoggerUtil;
import com.centurylink.mdw.util.log.StandardLogger;

public class AuthUtils {

    private static StandardLogger logger = LoggerUtil.getStandardLogger();

    public static final String AUTHORIZATION_HEADER_AUTHENTICATION = "Authorization";
    public static final String GIT_HUB_SECRET_KEY = "GitHub";
    public static final String SLACK_TOKEN = "MDW_SLACK_TOKEN";
    public static final String MDW_APP_TOKEN = "MDW_APP_TOKEN";
    public static final String MDW_AUTH = "mdwAuth";
    public static final String MDW_JWT_CUSTOM_KEY = "MDW_JWT_CUSTOM_KEY";

    private static final String APPTOKENCACHE = "com.centurylink.mdw.central.AppCache";

    public static final String JWTTOKENCACHE = "com.centurylink.mdw.authCTL.JwtTokenCache";
    private static final String CTLJWTPKG = "com.centurylink.mdw.authCTL";
    private static final String CTLJWTAUTH = "com.centurylink.mdw.authCTL.MdwAuthenticatorCTL";

    private static Map<String, Properties> customProviders = null; // Configured JWT Custom Providers
    private static JWTVerifier verifier = null;
    private static Map<String, JWTVerifier> verifierCustom = new ConcurrentHashMap<>();
    private static long maxAge = 0;

    public static boolean authenticate(String authMethod, Map<String, String> headers) {
        return authenticate(authMethod, headers, null);
    }

    public static boolean authenticate(String authMethod, Map<String, String> headers, String payload) {
        // avoid any fishiness -- only we should populate this header
        headers.remove(Listener.AUTHENTICATED_USER_HEADER);
        if (authMethod.equals(AUTHORIZATION_HEADER_AUTHENTICATION)) {
            return authenticateAuthorizationHeader(headers);
        } else if (authMethod.equals(GIT_HUB_SECRET_KEY)) {
            return authenticateGitHubSecretKey(headers, payload);
        } else if (authMethod.equals(SLACK_TOKEN)) {
            return authenticateSlackToken(headers, payload);
        } else if (authMethod.equals(MDW_APP_TOKEN)) {
            return authenticateMdwAppToken(headers, payload);
        } else {
            throw new IllegalArgumentException("Unsupported authentication method: " + authMethod);
        }
    }

    /**
      * <p>
      * Currently uses the metainfo property "Authorization" and checks
      * specifically for Basic Authentication or MDW-JWT Authentication.
      * In the future, probably change this.
      * </p>
      * <p>
      * TODO: call this from every protocol channel
      * If nothing else we should to this to avoid spoofing the AUTHENTICATED_USER_HEADER.
      * </p>
      * @param headers
      * @return
      */
    private static boolean authenticateAuthorizationHeader(Map<String, String> headers) {
        String hdr = headers.get(Listener.AUTHORIZATION_HEADER_NAME);
        if (hdr == null)
            hdr = headers.get(Listener.AUTHORIZATION_HEADER_NAME.toLowerCase());

        headers.remove(Listener.AUTHORIZATION_HEADER_NAME);
        headers.remove(Listener.AUTHORIZATION_HEADER_NAME.toLowerCase());

        if (hdr != null && hdr.startsWith("Basic"))
            return checkBasicAuthenticationHeader(hdr, headers);
        else if (hdr != null && hdr.startsWith("Bearer"))
            return checkBearerAuthenticationHeader(hdr, headers);

        return false;
    }

    private static boolean authenticateGitHubSecretKey(Map<String, String> headers, String payload) {
        String signature = headers.get(Listener.X_HUB_SIGNATURE);
        String key = System.getenv(PropertyNames.MDW_GITHUB_SECRET_TOKEN);
        try {
            String payloadSig = "sha1="
                    + HmacSha1Signature.getHMACHexdigestSignature(payload.trim().getBytes("UTF-8"), key);
            if (payloadSig.equals(signature)) {
                headers.put(Listener.AUTHENTICATED_USER_HEADER, "mdwapp"); // TODO: honor serviceUser in access.yaml
                return true;
            }
        } catch (Exception ex) {
            logger.severeException("Secret key authentication failure", ex);
            return false;
        }
        return false;
    }

    private static boolean authenticateSlackToken(Map<String, String> headers, String payload) {
        boolean okay = false;
        if (payload.startsWith("payload=")) { // TODO: handle multiple params
            try {
                String decodedPayload = URLDecoder.decode(payload.substring(8), "utf-8");
                JSONObject json = new JSONObject(decodedPayload);
                okay = json.has("token") && json.getString("token").equals(System.getenv(SLACK_TOKEN));
                if (okay) {
                    json.remove("token");
                    headers.put(Listener.AUTHENTICATED_USER_HEADER, "mdwapp"); // TODO: honor serviceUser in access.yaml
                    headers.put(Listener.METAINFO_REQUEST_PAYLOAD, json.toString());
                }
            } catch (UnsupportedEncodingException ex) {
                throw new RuntimeException("Apparently utf-8 is out of fashion", ex);
            }
        } else {
            // JSON request
            JSONObject json = new JSONObject(payload);
            okay = json.has("token") && json.getString("token").equals(System.getenv(SLACK_TOKEN));
            if (okay) {
                json.remove("token");
                headers.put(Listener.AUTHENTICATED_USER_HEADER, "mdwapp"); // TODO: honor serviceUser in access.yaml
                headers.put(Listener.METAINFO_REQUEST_PAYLOAD, json.toString());
            }
        }
        return okay;
    }

    private static boolean authenticateMdwAppToken(Map<String, String> headers, String payload) {
        // If routing is not enabled, do not authenticate this way
        if (!PropertyManager.getBooleanProperty(PropertyNames.MDW_ROUTING_REQUESTS_ENABLED, false))
            return false;

        // If appId and Token were not provided in header, do not authenticate this way
        if (headers.get(Listener.METAINFO_MDW_APP_ID) == null
                || headers.get(Listener.METAINFO_MDW_APP_TOKEN) == null)
            return false;

        String appId = headers.get(Listener.METAINFO_MDW_APP_ID);
        String providedToken = headers.get(Listener.METAINFO_MDW_APP_TOKEN);
        String realToken = "";
        CacheService appTokenCacheInstance = CacheRegistration.getInstance().getCache(APPTOKENCACHE);
        try {
            Method compiledAssetGetter = appTokenCacheInstance.getClass().getMethod("getAppToken", String.class);
            realToken = (String) compiledAssetGetter.invoke(appTokenCacheInstance, appId);
        } catch (Exception ex) {
            logger.severeException("Exception trying to retreieve App token from cache", ex);
        }

        // If the provided token doesn't match real token for specified appId, fail authentication
        if (providedToken == null || !providedToken.equals(realToken)) {
            logger.debug("Routing request failed authentication using MDW Application Token for " + appId);
            return false;
        }

        logger.debug("Routing request authenticated using MDW Application Token for " + appId);
        headers.put(Listener.AUTHENTICATED_USER_HEADER, "mdwapp"); // TODO: honor serviceUser in access.yaml
        headers.remove(Listener.METAINFO_MDW_APP_TOKEN);

        return true;
    }

    private static boolean checkBearerAuthenticationHeader(String authHeader, Map<String, String> headers) {
        try {
            // Do NOT try to authenticate if it's not Bearer
            if (authHeader == null || !authHeader.startsWith("Bearer"))
                throw new Exception("Invalid MDW Auth Header"); // This should never happen

            authHeader = authHeader.replaceFirst("Bearer ", "");
            DecodedJWT jwt = JWT.decode(authHeader); // Validate it is a JWT and see which kind of JWT it is

            if (MDW_AUTH.equals(jwt.getIssuer())) // JWT was issued by MDW Central
                verifyMdwJWT(authHeader, headers);
            else if (verifierCustom.get(jwt.getIssuer()) != null
                    || (PropertyManager.getInstance().getProperties(PropertyNames.MDW_JWT) != null
                            && PropertyManager.getInstance().getProperties(PropertyNames.MDW_JWT).values()
                                    .contains(jwt.getIssuer()))) // Support for other issuers of JWTs
                verifyCustomJWT(authHeader, jwt.getAlgorithm(), jwt.getIssuer(), headers);
            else
                throw new Exception("Invalid JWT Issuer");
        } catch (Throwable ex) {
            if (!ApplicationContext.isDevelopment()) {
                headers.put(Listener.AUTHENTICATION_FAILED,
                        "Authentication failed for JWT '" + authHeader + "' " + ex.getMessage());
                logger.severeException("Authentication failed for JWT '" + authHeader + "' " + ex.getMessage(), ex);
            }
            return false;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(
                    "authentication successful for user '" + headers.get(Listener.AUTHENTICATED_USER_HEADER) + "'");
        }
        if (PropertyManager.getBooleanProperty(PropertyNames.MDW_JWT_PRESERVE, false))
            headers.put(Listener.AUTHENTICATED_JWT, authHeader);
        return true;
    }

    /**
     * @return true if no authentication at all or authentication is successful
     */
    private static boolean checkBasicAuthenticationHeader(String authorizationHeader, Map<String, String> headers) {
        String user = "Unknown";
        try {
            // Do NOT try to authenticate if it's not Basic auth
            if (authorizationHeader == null || !authorizationHeader.startsWith("Basic"))
                throw new Exception("Invalid Basic Auth Header"); // This should never happen

            authorizationHeader = authorizationHeader.replaceFirst("Basic ", "");

            byte[] valueDecoded = Base64.decodeBase64(authorizationHeader.getBytes());
            authorizationHeader = new String(valueDecoded);

            String[] creds = authorizationHeader.split(":");

            if (creds.length < 2)
                throw new Exception("Invalid Basic Auth Header");

            user = creds[0];
            String pass = creds[1];

            if (ApplicationContext.isMdwAuth()) {
                if (PackageCache.getPackage(CTLJWTPKG) == null)
                    throw new Exception("Basic Auth is not allowed when authMethod is mdw");

                String token = null;
                CacheService jwtTokenCacheInstance = CacheRegistration.getInstance().getCache(JWTTOKENCACHE);
                try {
                    Method compiledAssetGetter = jwtTokenCacheInstance.getClass().getMethod("getToken",
                            String.class, String.class);
                    token = (String) compiledAssetGetter.invoke(jwtTokenCacheInstance, user, pass);
                } catch (Exception ex) {
                    logger.severeException("Exception trying to retreieve App token from cache", ex);
                }
                boolean validated = false;
                if (!StringHelper.isEmpty(token)) { // Use token if this user was already validated
                    try {
                        // Use cached token
                        verifyMdwJWT(token, headers);
                        validated = true;
                    } catch (Exception e) {
                    } // Token might be expired or some other issue with it - re-authenticate
                }
                if (!validated) {
                    // Authenticate using com/centurylink/mdw/central/auth service hosted in MDW Central
                    com.centurylink.mdw.model.workflow.Package pkg = PackageCache.getPackage(CTLJWTPKG);
                    Authenticator jwtAuth = (Authenticator) CompiledJavaCache.getInstance(CTLJWTAUTH,
                            pkg.getCloudClassLoader(), pkg);
                    jwtAuth.authenticate(user, pass); // This will populate JwtTokenCache with token for next time
                }
            } else {
                ldapAuthenticate(user, pass);
            }

            headers.put(Listener.AUTHENTICATED_USER_HEADER, user);
            if (logger.isDebugEnabled()) {
                logger.debug("authentication successful for user '" + user + "'");
            }
        } catch (Exception ex) {
            if (!ApplicationContext.isDevelopment()) {
                headers.put(Listener.AUTHENTICATION_FAILED,
                        "Authentication failed for '" + user + "'. " + ex.getMessage());
                logger.severeException("Authentication failed for user '" + user + "'. " + ex.getMessage(), ex);
            }
            return false;
        }
        return true;
    }

    public static void ldapAuthenticate(String user, String password) throws MdwSecurityException {
        String ldapProtocol = PropertyManager.getProperty(PropertyNames.MDW_LDAP_PROTOCOL);
        if (ldapProtocol == null)
            ldapProtocol = PropertyManager.getProperty("LDAP/Protocol"); // compatibility
        if (ldapProtocol == null)
            ldapProtocol = "ldap";
        String ldapHost = PropertyManager.getProperty(PropertyNames.MDW_LDAP_HOST);
        if (ldapHost == null)
            ldapHost = PropertyManager.getProperty("LDAP/Host");
        String ldapPort = PropertyManager.getProperty(PropertyNames.MDW_LDAP_PORT);
        if (ldapPort == null)
            ldapPort = PropertyManager.getProperty("LDAP/Port");
        String ldapUrl = ldapProtocol + "://" + ldapHost + ":" + ldapPort;
        String baseDn = PropertyManager.getProperty(PropertyNames.MDW_LDAP_BASE_DN);
        if (baseDn == null)
            baseDn = PropertyManager.getProperty("LDAP/BaseDN");
        LdapAuthenticator auth = new LdapAuthenticator(ldapUrl, baseDn);
        auth.authenticate(user, password);
    }

    private static void verifyMdwJWT(String token, Map<String, String> headers) throws Exception {
        // If first call, generate verifier
        JWTVerifier tempVerifier = verifier;
        if (tempVerifier == null)
            tempVerifier = createMdwTokenVerifier();

        if (tempVerifier == null)
            throw new Exception("Cannot generate MDW JWT verifier");

        DecodedJWT jwt = tempVerifier.verify(token); // Verifies JWT is valid

        // Verify token is not too old, if application specifies property for max token age - in seconds
        if (maxAge > 0 && jwt.getIssuedAt() != null) {
            if ((new Date().getTime() - jwt.getIssuedAt().getTime()) > maxAge)
                throw new Exception("JWT token has expired");
        }

        // Get the user JWT was created for
        if (!StringHelper.isEmpty(jwt.getSubject()))
            headers.put(Listener.AUTHENTICATED_USER_HEADER, jwt.getSubject());
        else
            throw new Exception("Received valid JWT token, but cannot identify the user");
    }

    private static synchronized JWTVerifier createMdwTokenVerifier() {
        JWTVerifier tempVerifier = verifier;
        if (tempVerifier == null) {
            String appToken = System.getenv(MDW_APP_TOKEN);
            if (StringHelper.isEmpty(appToken))
                logger.severe(
                        "Exception processing incoming message using MDW Auth token - Missing System environment variable "
                                + MDW_APP_TOKEN);
            else {
                try {
                    maxAge = PropertyManager.getIntegerProperty(PropertyNames.MDW_AUTH_TOKEN_MAX_AGE, 0) * 1000L; // MDW default is token never expires
                    Algorithm algorithm = Algorithm.HMAC256(appToken);
                    verifier = tempVerifier = JWT.require(algorithm).withIssuer(MDW_AUTH)
                            .withAudience(ApplicationContext.getAppId()).build(); //Reusable verifier instance
                } catch (IllegalArgumentException | UnsupportedEncodingException e) {
                    logger.severeException("Exception processing incoming message using MDW Auth token", e);
                }
            }
        }
        return tempVerifier;
    }

    private static void verifyCustomJWT(String token, String algorithm, String issuer, Map<String, String> headers)
            throws Exception {
        // If first call, generate verifier
        JWTVerifier tempVerifier = verifierCustom.get(issuer);
        if (tempVerifier == null)
            tempVerifier = createCustomTokenVerifier(algorithm, issuer);

        if (tempVerifier == null)
            throw new Exception("Cannot generate Custom JWT verifier for " + issuer);

        DecodedJWT jwt = tempVerifier.verify(token); // Verifies JWT is valid

        // Verify token is not too old, if application specifies property for max token age - in seconds
        if (maxAge > 0 && jwt.getIssuedAt() != null) {
            if ((new Date().getTime() - jwt.getIssuedAt().getTime()) > maxAge)
                throw new Exception("Custom JWT token has expired");
        }

        Properties props = customProviders.get(getCustomProviderGroupName(issuer));

        // Get the user JWT was created for (Claim specified in Property) - Check payload and header for the claim
        String user = jwt.getClaim(props.getProperty(PropertyNames.MDW_JWT_USER_CLAIM)).asString();
        if (StringHelper.isEmpty(user))
            user = jwt.getHeaderClaim(props.getProperty(PropertyNames.MDW_JWT_USER_CLAIM)).asString();

        if (!StringHelper.isEmpty(user))
            headers.put(Listener.AUTHENTICATED_USER_HEADER, user);
        else
            throw new Exception("Received valid Custom JWT token, but cannot identify the user");
    }

    private static synchronized JWTVerifier createCustomTokenVerifier(String algorithmName, String issuer) {
        JWTVerifier tempVerifier = verifierCustom.get(issuer);
        if (tempVerifier == null) {
            Properties props = null;
            if (customProviders == null) {
                props = PropertyManager.getInstance().getProperties(PropertyNames.MDW_JWT);
                customProviders = new HashMap<>();
                for (String pn : props.stringPropertyNames()) {
                    String[] pnParsed = pn.split("\\.", 4);
                    if (pnParsed.length == 4) {
                        String issuer_name = pnParsed[2];
                        String attrname = pnParsed[3];
                        Properties issuerSpec = customProviders.get(issuer_name);
                        if (issuerSpec == null) {
                            issuerSpec = new Properties();
                            customProviders.put(issuer_name, issuerSpec);
                        }
                        String v = props.getProperty(pn);
                        issuerSpec.put(attrname, v);
                    }
                }
            }

            props = customProviders.get(getCustomProviderGroupName(issuer));

            if (props == null) {
                logger.severe("Exception creating Custom JWT Verifier for " + issuer + " - Missing 'key' Property");
                return null;
            }

            String propAlg = props.getProperty(PropertyNames.MDW_JWT_ALGORITHM);
            if (StringHelper.isEmpty(algorithmName)
                    || (!StringHelper.isEmpty(propAlg) && !algorithmName.equals(propAlg))) {
                String message = "Exception creating Custom JWT Verifier - ";
                message = StringHelper.isEmpty(algorithmName) ? "Missing 'alg' claim in JWT"
                        : ("Mismatch algorithm with specified Property for " + issuer);
                logger.severe(message);
                return null;
            }
            String key = System.getenv("MDW_JWT_" + getCustomProviderGroupName(issuer).toUpperCase() + "_KEY");
            if (StringHelper.isEmpty(key)) {
                if (!algorithmName.startsWith("HS")) { // Only allow use of Key in MDW properties for asymmetric algorithms
                    key = props.getProperty(PropertyNames.MDW_JWT_KEY);
                    if (StringHelper.isEmpty(key)) {
                        logger.severe("Exception creating Custom JWT Verifier for " + issuer
                                + " - Missing 'key' Property");
                        return null;
                    }
                } else {
                    logger.severe("Could not find properties for JWT issuer " + issuer);
                    return null;
                }
            }

            try {
                maxAge = PropertyManager.getIntegerProperty(PropertyNames.MDW_AUTH_TOKEN_MAX_AGE, 0) * 1000L;

                Algorithm algorithm = null;
                Method algMethod = null;
                if (algorithmName.startsWith("HS")) { // HMAC
                    String methodName = "HMAC" + algorithmName.substring(2);
                    algMethod = Algorithm.none().getClass().getMethod(methodName, String.class);
                    algorithm = (Algorithm) algMethod.invoke(Algorithm.none(), key);
                } else if (algorithmName.startsWith("RS")) { // RSA
                    String methodName = "RSA" + algorithmName.substring(2);
                    byte[] publicBytes = Base64.decodeBase64(key.getBytes());
                    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
                    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
                    PublicKey pubKey = keyFactory.generatePublic(keySpec);
                    algMethod = Algorithm.none().getClass().getMethod(methodName, RSAPublicKey.class,
                            RSAPrivateKey.class);
                    algorithm = (Algorithm) algMethod.invoke(Algorithm.none(), pubKey, null);
                } else {
                    logger.severe(
                            "Exception creating Custom JWT Verifier - Unsupported Algorithm: " + algorithmName);
                    return null;
                }

                String subject = props.getProperty(PropertyNames.MDW_JWT_SUBJECT);

                Verification tmp = JWT.require(algorithm).withIssuer(issuer);
                tmp = StringHelper.isEmpty(subject) ? tmp : tmp.withSubject(subject);
                tempVerifier = tmp.build();
                verifierCustom.put(issuer, tempVerifier);
            } catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchMethodException | SecurityException
                    | IllegalAccessException | InvocationTargetException | InvalidKeySpecException e) {
                logger.severeException("Exception creating Custom JWT Verifier for " + issuer, e);
            }
        }
        return tempVerifier;
    }

    private static String getCustomProviderGroupName(String issuer) {
        if (customProviders != null) {
            for (String issuerName : customProviders.keySet()) {
                Properties props = customProviders.get(issuerName);
                if (issuer.equals(props.getProperty(PropertyNames.MDW_JWT_ISSUER)))
                    return issuerName;
            }
        }
        return null;
    }
}