/**
* Copyright (c) Codice Foundation
* <p>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
* <p>
* <p>
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.codice.ddf.security.validator.uname;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.security.auth.callback.CallbackHandler;
import javax.xml.bind.JAXBElement;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.sts.QNameConstants;
import org.apache.cxf.sts.STSPropertiesMBean;
import org.apache.cxf.sts.request.ReceivedToken;
import org.apache.cxf.sts.request.ReceivedToken.STATE;
import org.apache.cxf.sts.token.validator.TokenValidator;
import org.apache.cxf.sts.token.validator.TokenValidatorParameters;
import org.apache.cxf.sts.token.validator.TokenValidatorResponse;
import org.apache.cxf.ws.security.sts.provider.model.secext.AttributedString;
import org.apache.cxf.ws.security.sts.provider.model.secext.BinarySecurityTokenType;
import org.apache.cxf.ws.security.sts.provider.model.secext.PasswordString;
import org.apache.cxf.ws.security.sts.provider.model.secext.UsernameTokenType;
import org.apache.cxf.ws.security.tokenstore.SecurityToken;
import org.apache.karaf.jaas.config.JaasRealm;
import org.apache.wss4j.common.bsp.BSPEnforcer;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.principal.CustomTokenPrincipal;
import org.apache.wss4j.common.principal.WSUsernameTokenPrincipalImpl;
import org.apache.wss4j.dom.WSConstants;
import org.apache.wss4j.dom.engine.WSSConfig;
import org.apache.wss4j.dom.handler.RequestData;
import org.apache.wss4j.dom.message.token.UsernameToken;
import org.apache.wss4j.dom.validate.Credential;
import org.apache.wss4j.dom.validate.JAASUsernameTokenValidator;
import org.apache.wss4j.dom.validate.Validator;
import org.codice.ddf.parser.Parser;
import org.codice.ddf.parser.ParserConfigurator;
import org.codice.ddf.parser.ParserException;
import org.codice.ddf.security.common.FailedLoginDelayer;
import org.codice.ddf.security.handler.api.BaseAuthenticationToken;
import org.codice.ddf.security.handler.api.UPAuthenticationToken;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* DDFUsername BST validator for the STS.
*/
public class UPBSTValidator implements TokenValidator {
private static final Logger LOGGER = LoggerFactory.getLogger(UPBSTValidator.class);
private final Parser parser;
private final FailedLoginDelayer failedLoginDelayer;
protected Map<String, Validator> validators = new ConcurrentHashMap<>();
public UPBSTValidator(Parser parser, FailedLoginDelayer failedLoginDelayer) {
this.parser = parser;
this.failedLoginDelayer = failedLoginDelayer;
}
public void addRealm(ServiceReference<JaasRealm> serviceReference) {
Bundle bundle = FrameworkUtil.getBundle(UPBSTValidator.class);
if (null != bundle) {
JaasRealm realm = bundle.getBundleContext()
.getService(serviceReference);
LOGGER.trace("Adding validator for JaasRealm {}", realm.getName());
JAASUsernameTokenValidator validator = new JAASUsernameTokenValidator();
validator.setContextName(realm.getName());
validators.put(realm.getName(), validator);
}
}
public void removeRealm(ServiceReference<JaasRealm> serviceReference) {
Bundle bundle = FrameworkUtil.getBundle(UPBSTValidator.class);
if (null != bundle) {
JaasRealm realm = bundle.getBundleContext()
.getService(serviceReference);
LOGGER.trace("Removing validator for JaasRealm {}", realm.getName());
validators.remove(realm.getName());
}
}
/**
* Return true if this TokenValidator implementation is capable of validating the
* ReceivedToken argument.
*
* @param validateTarget
* @return true if the token can be handled
*/
public boolean canHandleToken(ReceivedToken validateTarget) {
return canHandleToken(validateTarget, null);
}
/**
* Return true if this TokenValidator implementation is capable of validating the
* ReceivedToken argument. The realm is ignored in this token Validator.
*
* @param validateTarget
* @param cxfRealm
* @return true if the token can be handled
*/
public boolean canHandleToken(ReceivedToken validateTarget, String cxfRealm) {
boolean canHandle = false;
UPAuthenticationToken usernameToken = getUsernameTokenFromTarget(validateTarget);
if (usernameToken != null) {
// currently realm is not being passed through (no RealmParser that determines the realm
// based on the web context. So this just looks at the realm passed in the credentials.
// This generic instance just looks for the default realms (DDF and Karaf)
if (usernameToken.getRealm() == null) {
LOGGER.trace("No realm specified in request");
canHandle = (validators != null);
} else if (validators != null && validators.containsKey(usernameToken.getRealm())) {
LOGGER.trace("Realm '{}' recognized - canHandleToken = true",
usernameToken.getRealm());
canHandle = true;
} else if (validators != null && "*".equals(usernameToken.getRealm())) {
LOGGER.trace("Realm '*' recognized - canHandleToken = true");
canHandle = true;
}
if (!canHandle) {
LOGGER.trace("Realm '{}' unrecognized - canHandleToken = false",
usernameToken.getRealm());
}
}
LOGGER.debug("Returning canHandle: {}", canHandle);
return canHandle;
}
/**
* Validate a Token using the given TokenValidatorParameters.
*
* @param tokenParameters
* @return TokenValidatorResponse
*/
public TokenValidatorResponse validateToken(TokenValidatorParameters tokenParameters) {
LOGGER.trace("Validating UPBST Token");
if (parser == null) {
throw new IllegalStateException("XMLParser must be configured.");
}
if (failedLoginDelayer == null) {
throw new IllegalStateException("Failed Login Delayer must be configured");
}
STSPropertiesMBean stsProperties = tokenParameters.getStsProperties();
Crypto sigCrypto = stsProperties.getSignatureCrypto();
CallbackHandler callbackHandler = stsProperties.getCallbackHandler();
RequestData requestData = new RequestData();
requestData.setSigVerCrypto(sigCrypto);
requestData.setWssConfig(WSSConfig.getNewInstance());
requestData.setCallbackHandler(callbackHandler);
TokenValidatorResponse response = new TokenValidatorResponse();
ReceivedToken validateTarget = tokenParameters.getToken();
validateTarget.setState(STATE.INVALID);
response.setToken(validateTarget);
if (!validateTarget.isBinarySecurityToken()) {
return response;
}
BinarySecurityTokenType binarySecurityType =
(BinarySecurityTokenType) validateTarget.getToken();
// Test the encoding type
String encodingType = binarySecurityType.getEncodingType();
if (!UPAuthenticationToken.BASE64_ENCODING.equals(encodingType)) {
LOGGER.trace("Bad encoding type attribute specified: {}", encodingType);
return response;
}
UPAuthenticationToken usernameToken = getUsernameTokenFromTarget(validateTarget);
if (usernameToken == null) {
return response;
}
UsernameTokenType usernameTokenType = getUsernameTokenType(usernameToken);
// Marshall the received JAXB object into a DOM Element
Element usernameTokenElement = null;
JAXBElement<UsernameTokenType> tokenType = new JAXBElement<>(QNameConstants.USERNAME_TOKEN,
UsernameTokenType.class,
usernameTokenType);
Document doc = DOMUtils.createDocument();
Element rootElement = doc.createElement("root-element");
List<String> ctxPath = new ArrayList<>(1);
ctxPath.add(UsernameTokenType.class.getPackage()
.getName());
ParserConfigurator configurator = parser.configureParser(ctxPath,
UPBSTValidator.class.getClassLoader());
try {
parser.marshal(configurator, tokenType, rootElement);
} catch (ParserException ex) {
LOGGER.info("Unable to parse username token", ex);
return response;
}
usernameTokenElement = (Element) rootElement.getFirstChild();
//
// Validate the token
//
WSSConfig wssConfig = WSSConfig.getNewInstance();
try {
boolean allowNamespaceQualifiedPasswordTypes =
requestData.isAllowNamespaceQualifiedPasswordTypes();
UsernameToken ut = new UsernameToken(usernameTokenElement,
allowNamespaceQualifiedPasswordTypes,
new BSPEnforcer());
// The parsed principal is set independent whether validation is successful or not
response.setPrincipal(new CustomTokenPrincipal(ut.getName()));
if (ut.getPassword() == null) {
return response;
}
String tokenId = String.format("%s:%s:%s",
usernameToken.getUsername(),
usernameToken.getPassword(),
usernameToken.getRealm());
// See if the UsernameToken is stored in the cache
int hash = tokenId.hashCode();
SecurityToken secToken = null;
if (tokenParameters.getTokenStore() != null) {
secToken = tokenParameters.getTokenStore()
.getToken(Integer.toString(hash));
if (secToken != null && secToken.getTokenHash() != hash) {
secToken = null;
} else if (secToken != null) {
validateTarget.setState(STATE.VALID);
}
}
if (secToken == null) {
Credential credential = new Credential();
credential.setUsernametoken(ut);
if (usernameToken.getRealm() != null && !"*".equals(usernameToken.getRealm())) {
Validator validator = validators.get(usernameToken.getRealm());
if (validator != null) {
try {
validator.validate(credential, requestData);
validateTarget.setState(STATE.VALID);
LOGGER.debug("Validated user against realm {}",
usernameToken.getRealm());
} catch (WSSecurityException ex) {
LOGGER.debug("Not able to validate user against realm {}",
usernameToken.getRealm());
}
}
} else {
Set<Map.Entry<String, Validator>> entries = validators.entrySet();
for (Map.Entry<String, Validator> entry : entries) {
try {
entry.getValue()
.validate(credential, requestData);
validateTarget.setState(STATE.VALID);
LOGGER.debug("Validated user against realm {}", entry.getKey());
break;
} catch (WSSecurityException ex) {
LOGGER.debug("Not able to validate user against realm {}",
entry.getKey());
}
}
}
}
Principal principal = createPrincipal(ut.getName(),
ut.getPassword(),
ut.getPasswordType(),
ut.getNonce(),
ut.getCreated());
// Store the successfully validated token in the cache
if (tokenParameters.getTokenStore() != null && secToken == null && STATE.VALID.equals(
validateTarget.getState())) {
secToken = new SecurityToken(ut.getID());
secToken.setToken(ut.getElement());
int hashCode = tokenId.hashCode();
String identifier = Integer.toString(hashCode);
secToken.setTokenHash(hashCode);
tokenParameters.getTokenStore()
.add(identifier, secToken);
}
response.setPrincipal(principal);
response.setTokenRealm(null);
validateTarget.setPrincipal(principal);
} catch (WSSecurityException ex) {
LOGGER.debug("Unable to validate token.", ex);
}
if (response.getToken().getState() != STATE.VALID) {
failedLoginDelayer.delay(response.getToken()
.getPrincipal()
.getName());
}
return response;
}
/**
* Create a principal based on the authenticated UsernameToken.
*/
private Principal createPrincipal(String username, String passwordValue, String passwordType,
String nonce, String createdTime) {
boolean hashed = false;
if (WSConstants.PASSWORD_DIGEST.equals(passwordType)) {
hashed = true;
}
WSUsernameTokenPrincipalImpl principal = new WSUsernameTokenPrincipalImpl(username, hashed);
if (nonce != null) {
principal.setNonce(nonce.getBytes(StandardCharsets.UTF_8));
}
principal.setPassword(passwordValue);
principal.setCreatedTime(createdTime);
principal.setPasswordType(passwordType);
return principal;
}
public UsernameTokenType getUsernameTokenType(UPAuthenticationToken token) {
UsernameTokenType usernameTokenType = new UsernameTokenType();
AttributedString user = new AttributedString();
user.setValue(token.getUsername());
usernameTokenType.setUsername(user);
// Add a password
PasswordString password = new PasswordString();
password.setValue(token.getPassword());
password.setType(WSConstants.PASSWORD_TEXT);
JAXBElement<PasswordString> passwordType =
new JAXBElement<PasswordString>(QNameConstants.PASSWORD,
PasswordString.class,
password);
usernameTokenType.getAny()
.add(passwordType);
return usernameTokenType;
}
private UPAuthenticationToken getUsernameTokenFromTarget(ReceivedToken validateTarget) {
Object token = validateTarget.getToken();
if ((token instanceof BinarySecurityTokenType)
&& UPAuthenticationToken.UP_TOKEN_VALUE_TYPE.equals(((BinarySecurityTokenType) token).getValueType())) {
String encodedCredential = ((BinarySecurityTokenType) token).getValue();
LOGGER.debug("Encoded username/password credential: {}", encodedCredential);
BaseAuthenticationToken base = null;
try {
base = UPAuthenticationToken.parse(encodedCredential, true);
return new UPAuthenticationToken(base.getPrincipal()
.toString(),
base.getCredentials()
.toString(),
base.getRealm());
} catch (WSSecurityException e) {
LOGGER.info("Unable to parse {} from encodedToken.",
UPAuthenticationToken.class.getSimpleName(),
e);
return null;
}
}
return null;
}
}