
com.centurylink.mdw.services.util.AuthUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mdw-services Show documentation
Show all versions of mdw-services Show documentation
MDW is a workflow framework specializing in microservice orchestration
/*
* 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 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.asset.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.log.LoggerUtil;
import com.centurylink.mdw.util.log.StandardLogger;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.json.JSONObject;
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;
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 customProviders = null; // Configured JWT Custom Providers
private static JWTVerifier verifier = null;
private static Map verifierCustom = new ConcurrentHashMap<>();
private static long maxAge = 0;
public static boolean authenticate(String authMethod, Map headers) {
return authenticate(authMethod, headers, null);
}
public static boolean authenticate(String authMethod, Map 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);
}
}
/**
*
* Currently uses the metainfo property "Authorization" and checks
* specifically for Basic Authentication or MDW-JWT Authentication.
* In the future, probably change this.
*
*
* TODO: call this from every protocol channel
* If nothing else we should to this to avoid spoofing the AUTHENTICATED_USER_HEADER.
*
* @param headers headers.
* @return boolean.
*/
private static boolean authenticateAuthorizationHeader(Map 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 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.error("Secret key authentication failure", ex);
return false;
}
return false;
}
private static boolean authenticateSlackToken(Map headers, String payload) {
boolean okay;
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 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.error("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 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.error("Authentication failed for JWT '"+authHeader+"' " + ex.getMessage(), ex);
}
return false;
}
if (logger.isDebugEnabled()) {
logger.debug("Bearer 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 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.error("Exception trying to retreieve App token from cache", ex);
}
boolean validated = false;
if (!StringUtils.isBlank(token)) { // Use token if this user was already validated
try {
// Use cached token
verifyMdwJWT(token, headers);
validated = true;
} catch (Exception ignored) {} // 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.getClassLoader(), 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("Basic authentication successful for user '"+user+"'");
}
}
catch (Exception ex) {
if (!ApplicationContext.isDevelopment()) {
headers.put(Listener.AUTHENTICATION_FAILED, "Authentication failed for '"+user+"'. "+ex.getMessage());
logger.error("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 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 (!StringUtils.isBlank(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 (StringUtils.isBlank(appToken))
logger.error("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.error("Exception processing incoming message using MDW Auth token", e);
}
}
}
return tempVerifier;
}
private static void verifyCustomJWT(String token, String algorithm, String issuer, Map 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 (StringUtils.isBlank(user))
user = jwt.getHeaderClaim(props.getProperty(PropertyNames.MDW_JWT_USER_CLAIM)).asString();
if (!StringUtils.isBlank(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;
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.error("Exception creating Custom JWT Verifier for " + issuer + " - Missing 'key' Property");
return null;
}
String propAlg = props.getProperty(PropertyNames.MDW_JWT_ALGORITHM);
if (StringUtils.isBlank(algorithmName) || (!StringUtils.isBlank(propAlg) && !algorithmName.equals(propAlg))) {
String message = "Exception creating Custom JWT Verifier - ";
message = StringUtils.isBlank(algorithmName) ? "Missing 'alg' claim in JWT" : ("Mismatch algorithm with specified Property for " + issuer);
logger.error(message);
return null;
}
String customProvider = getCustomProviderGroupName(issuer);
if (customProvider != null)
customProvider = customProvider.toUpperCase();
String key = System.getenv("MDW_JWT_" + customProvider + "_KEY");
if (StringUtils.isBlank(key)) {
if (!algorithmName.startsWith("HS")) { // Only allow use of Key in MDW properties for asymmetric algorithms
key = props.getProperty(PropertyNames.MDW_JWT_KEY);
if (StringUtils.isBlank(key)) {
logger.error("Exception creating Custom JWT Verifier for " + issuer + " - Missing 'key' Property");
return null;
}
}
else {
logger.error("Could not find properties for JWT issuer " + issuer);
return null;
}
}
try {
maxAge = PropertyManager.getIntegerProperty(PropertyNames.MDW_AUTH_TOKEN_MAX_AGE, 0) * 1000L;
Algorithm algorithm;
Method algMethod;
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.error("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 = StringUtils.isBlank(subject) ? tmp : tmp.withSubject(subject);
tempVerifier = tmp.build();
verifierCustom.put(issuer, tempVerifier);
}
catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchMethodException | SecurityException | IllegalAccessException | InvocationTargetException | InvalidKeySpecException e) {
logger.error("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;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy