blob: 5ce2ec17817ff6b1baf6d230fca920f97aa216ce [file] [log] [blame]
/**
* 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
*
* 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 org.apache.wss4j.dom.message.token;
import java.security.Principal;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;
import javax.xml.namespace.QName;
import org.apache.wss4j.common.bsp.BSPEnforcer;
import org.apache.wss4j.common.bsp.BSPRule;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.principal.WSUsernameTokenPrincipalImpl;
import org.apache.wss4j.common.util.DOM2Writer;
import org.apache.wss4j.common.util.DateUtil;
import org.apache.wss4j.common.util.UsernameTokenUtil;
import org.apache.wss4j.common.util.WSCurrentTimeSource;
import org.apache.wss4j.common.util.WSTimeSource;
import org.apache.wss4j.common.util.XMLUtils;
import org.apache.wss4j.dom.WSConstants;
import org.apache.wss4j.dom.util.WSSecurityUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
/**
* UsernameToken according to WS Security specifications, UsernameToken profile.
*
* Enhanced to support digest password type for username token signature
* Enhanced to support passwordless usernametokens as allowed by spec.
*/
public class UsernameToken {
public static final String BASE64_ENCODING = WSConstants.SOAPMESSAGE_NS + "#Base64Binary";
public static final String PASSWORD_TYPE = "passwordType";
public static final int DEFAULT_ITERATION = 1000;
public static final QName TOKEN =
new QName(WSConstants.WSSE_NS, WSConstants.USERNAME_TOKEN_LN);
private static final org.slf4j.Logger LOG =
org.slf4j.LoggerFactory.getLogger(UsernameToken.class);
private Element element;
private Element elementUsername;
private Element elementPassword;
private Element elementNonce;
private Element elementCreated;
private Element elementSalt;
private Element elementIteration;
private int iteration = DEFAULT_ITERATION;
private String passwordType;
private boolean hashed = true;
private boolean passwordsAreEncoded;
private Instant created;
/**
* Constructs a <code>UsernameToken</code> object and parses the
* <code>wsse:UsernameToken</code> element to initialize it.
*
* @param elem the <code>wsse:UsernameToken</code> element that contains
* the UsernameToken data
* @param allowNamespaceQualifiedPasswordTypes whether to allow (wsse)
* namespace qualified password types or not (for interop with WCF)
* @param bspEnforcer a BSPEnforcer instance to enforce BSP rules
* @throws WSSecurityException
*/
public UsernameToken(
Element elem,
boolean allowNamespaceQualifiedPasswordTypes,
BSPEnforcer bspEnforcer
) throws WSSecurityException {
element = elem;
QName el = new QName(element.getNamespaceURI(), element.getLocalName());
if (!el.equals(TOKEN)) {
throw new WSSecurityException(
WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
"badElement",
new Object[] {TOKEN, el}
);
}
elementUsername =
XMLUtils.getDirectChildElement(
element, WSConstants.USERNAME_LN, WSConstants.WSSE_NS
);
elementPassword =
XMLUtils.getDirectChildElement(
element, WSConstants.PASSWORD_LN, WSConstants.WSSE_NS
);
elementNonce =
XMLUtils.getDirectChildElement(
element, WSConstants.NONCE_LN, WSConstants.WSSE_NS
);
elementCreated =
XMLUtils.getDirectChildElement(
element, WSConstants.CREATED_LN, WSConstants.WSU_NS
);
elementSalt =
XMLUtils.getDirectChildElement(
element, WSConstants.SALT_LN, WSConstants.WSSE11_NS
);
elementIteration =
XMLUtils.getDirectChildElement(
element, WSConstants.ITERATION_LN, WSConstants.WSSE11_NS
);
if (elementUsername == null) {
throw new WSSecurityException(
WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
"badUsernameToken",
new Object[] {"Username is missing"}
);
}
checkBSPCompliance(bspEnforcer);
hashed = false;
if (elementSalt != null && (elementPassword != null || elementIteration == null)) {
//
// If the UsernameToken is to be used for key derivation, the (1.1)
// spec says that it cannot contain a password, and it must contain
// an Iteration element
//
throw new WSSecurityException(
WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
"badUsernameToken",
new Object[] {"Password is missing"}
);
}
// Guard against a malicious user sending a bogus iteration value
if (elementIteration != null) {
String iter = XMLUtils.getElementText(elementIteration);
if (iter != null) {
try {
iteration = Integer.parseInt(iter);
if (iteration < 0 || iteration > 10000) {
throw new WSSecurityException(
WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
"badUsernameToken",
new Object[] {"Iteration is missing"}
);
}
} catch (NumberFormatException ex) {
throw new WSSecurityException(
WSSecurityException.ErrorCode.FAILURE, ex, "decoding.general"
);
}
}
}
if (elementPassword != null) {
if (elementPassword.hasAttributeNS(null, WSConstants.PASSWORD_TYPE_ATTR)) {
passwordType = elementPassword.getAttributeNS(null, WSConstants.PASSWORD_TYPE_ATTR);
} else if (elementPassword.hasAttributeNS(
WSConstants.WSSE_NS, WSConstants.PASSWORD_TYPE_ATTR)
) {
if (allowNamespaceQualifiedPasswordTypes) {
passwordType =
elementPassword.getAttributeNS(
WSConstants.WSSE_NS, WSConstants.PASSWORD_TYPE_ATTR
);
} else {
throw new WSSecurityException(
WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
"badUsernameToken",
new Object[] {"The Password Type is not allowed to be namespace qualified"}
);
}
}
}
if (WSConstants.PASSWORD_DIGEST.equals(passwordType)) {
hashed = true;
if (elementNonce == null || elementCreated == null) {
throw new WSSecurityException(
WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN,
"badUsernameToken",
new Object[] {"Nonce or Created is missing"}
);
}
}
if (elementCreated != null) {
String createdString = getCreated();
if (createdString != null && !"".equals(createdString)) {
try {
created = ZonedDateTime.parse(createdString).toInstant();
} catch (DateTimeParseException e) {
throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY, e);
}
}
}
}
/**
* Constructs a <code>UsernameToken</code> object according to the defined
* parameters. <p/> This constructs set the password encoding to
* {@link WSConstants#PASSWORD_DIGEST}
*
* @param doc the SOAP envelope as <code>Document</code>
*/
public UsernameToken(boolean milliseconds, Document doc) {
this(milliseconds, doc, WSConstants.PASSWORD_DIGEST);
}
/**
* Constructs a <code>UsernameToken</code> object according to the defined
* parameters.
*
* @param doc the SOAP envelope as <code>Document</code>
* @param pwType the required password encoding, either
* {@link WSConstants#PASSWORD_DIGEST} or
* {@link WSConstants#PASSWORD_TEXT} or
* {@link WSConstants#PW_NONE} <code>null</code> if no
* password required
*/
public UsernameToken(boolean milliseconds, Document doc, String pwType) {
this(milliseconds, doc, new WSCurrentTimeSource(), pwType);
}
public UsernameToken(boolean milliseconds, Document doc, WSTimeSource timeSource, String pwType) {
element =
doc.createElementNS(WSConstants.WSSE_NS, "wsse:" + WSConstants.USERNAME_TOKEN_LN);
elementUsername =
doc.createElementNS(WSConstants.WSSE_NS, "wsse:" + WSConstants.USERNAME_LN);
elementUsername.appendChild(doc.createTextNode(""));
element.appendChild(elementUsername);
if (pwType != null) {
elementPassword =
doc.createElementNS(WSConstants.WSSE_NS, "wsse:" + WSConstants.PASSWORD_LN);
elementPassword.appendChild(doc.createTextNode(""));
element.appendChild(elementPassword);
passwordType = pwType;
if (passwordType.equals(WSConstants.PASSWORD_DIGEST)) {
addNonce(doc);
addCreated(milliseconds, timeSource, doc);
} else {
hashed = false;
}
}
}
/**
* Add the WSSE Namespace to this UT. The namespace is not added by default for
* efficiency purposes.
*/
public void addWSSENamespace() {
XMLUtils.setNamespace(element, WSConstants.WSSE_NS, WSConstants.WSSE_PREFIX);
}
/**
* Add the WSU Namespace to this UT. The namespace is not added by default for
* efficiency purposes.
*/
public void addWSUNamespace() {
element.setAttributeNS(XMLUtils.XMLNS_NS, "xmlns:" + WSConstants.WSU_PREFIX, WSConstants.WSU_NS);
}
/**
* Creates and adds a Nonce element to this UsernameToken
*/
public void addNonce(Document doc) {
if (elementNonce != null) {
return;
}
byte[] nonceValue = null;
try {
nonceValue = WSSecurityUtil.generateNonce(16);
} catch (WSSecurityException ex) {
LOG.debug(ex.getMessage(), ex);
return;
}
elementNonce = doc.createElementNS(WSConstants.WSSE_NS, "wsse:" + WSConstants.NONCE_LN);
elementNonce.appendChild(doc.createTextNode(org.apache.xml.security.utils.XMLUtils.encodeToString(nonceValue)));
elementNonce.setAttributeNS(null, "EncodingType", BASE64_ENCODING);
element.appendChild(elementNonce);
}
/**
* Creates and adds a Created element to this UsernameToken
*/
public void addCreated(boolean milliseconds, Document doc) {
addCreated(milliseconds, new WSCurrentTimeSource(), doc);
}
/**
* Creates and adds a Created element to this UsernameToken
*/
public void addCreated(boolean milliseconds, WSTimeSource timeSource, Document doc) {
if (elementCreated != null) {
return;
}
elementCreated =
doc.createElementNS(
WSConstants.WSU_NS, WSConstants.WSU_PREFIX + ":" + WSConstants.CREATED_LN
);
Instant currentTime = timeSource.now();
DateTimeFormatter formatter = DateUtil.getDateTimeFormatter(milliseconds);
elementCreated.appendChild(doc.createTextNode(currentTime.atZone(ZoneOffset.UTC).format(formatter)));
element.appendChild(elementCreated);
}
/**
* Adds a Salt element to this UsernameToken.
*
* @param doc The Document for the UsernameToken
* @param saltValue The salt to add.
* @param mac If <code>true</code> then an optionally generated value is
* usable for a MAC
*/
public void addSalt(Document doc, byte[] saltValue, boolean mac) {
elementSalt =
doc.createElementNS(
WSConstants.WSSE11_NS, WSConstants.WSSE11_PREFIX + ":" + WSConstants.SALT_LN
);
XMLUtils.setNamespace(element, WSConstants.WSSE11_NS, WSConstants.WSSE11_PREFIX);
elementSalt.appendChild(doc.createTextNode(org.apache.xml.security.utils.XMLUtils.encodeToString(saltValue)));
element.appendChild(elementSalt);
}
/**
* Creates and adds a Iteration element to this UsernameToken
*/
public void addIteration(Document doc, int iteration) {
String text = "" + iteration;
elementIteration =
doc.createElementNS(
WSConstants.WSSE11_NS, WSConstants.WSSE11_PREFIX + ":" + WSConstants.ITERATION_LN
);
XMLUtils.setNamespace(element, WSConstants.WSSE11_NS, WSConstants.WSSE11_PREFIX);
elementIteration.appendChild(doc.createTextNode(text));
element.appendChild(elementIteration);
this.iteration = iteration;
}
/**
* Get the user name.
*
* @return the data from the user name element.
*/
public String getName() {
return XMLUtils.getElementText(elementUsername);
}
/**
* Set the user name.
*
* @param name sets a text node containing the use name into the user name
* element.
*/
public void setName(String name) {
Text node = getFirstNode(elementUsername);
node.setData(name);
}
/**
* Get the nonce.
*
* @return the data from the nonce element.
*/
public String getNonce() {
return XMLUtils.getElementText(elementNonce);
}
/**
* Get the created timestamp.
*
* @return the data from the created time element.
*/
public String getCreated() {
return XMLUtils.getElementText(elementCreated);
}
/**
* Return the Created Element as a Date object
* @return the Created Date
*/
public Instant getCreatedDate() {
return created;
}
/**
* Gets the password string. This is the password as it is in the password
* element of a username token. Thus it can be either plain text or the
* password digest value.
*
* @return the password string or <code>null</code> if no such node exists.
*/
public String getPassword() {
String password = XMLUtils.getElementText(elementPassword);
// See WSS-219
if (password == null && elementPassword != null) {
return "";
}
return password;
}
/**
* Return true if this UsernameToken contains a Password element
*/
public boolean containsPasswordElement() {
return elementPassword != null;
}
/**
* Get the Salt value of this UsernameToken.
*
* @return Returns the binary Salt value or <code>null</code> if no Salt
* value is available in the username token.
* @throws WSSecurityException
*/
public byte[] getSalt() throws WSSecurityException {
String salt = XMLUtils.getElementText(elementSalt);
if (salt != null) {
return org.apache.xml.security.utils.XMLUtils.decode(salt);
}
return null;
}
/**
* Get the Iteration value of this UsernameToken.
*
* @return Returns the Iteration value. If no Iteration was specified in the
* username token the default value according to the specification
* is returned.
*/
public int getIteration() {
return iteration;
}
/**
* Get the hashed indicator. If the indicator is <code>true> the password of the
* <code>UsernameToken</code> was encoded using {@link WSConstants#PASSWORD_DIGEST}
*
* @return the hashed indicator.
*/
public boolean isHashed() {
return hashed;
}
/**
* @return Returns the passwordType.
*/
public String getPasswordType() {
return passwordType;
}
/**
* Sets the password string. This function sets the password in the
* <code>UsernameToken</code> either as plain text or encodes the password
* according to the WS Security specifications, UsernameToken profile, into
* a password digest.
*
* @param pwd the password to use
*/
public void setPassword(String pwd) {
if (pwd == null) {
if (passwordType != null) {
throw new IllegalArgumentException("pwd == null but a password is needed");
} else {
// Ignore setting the password.
return;
}
}
Text node = getFirstNode(elementPassword);
try {
if (hashed) {
byte[] decodedNonce = org.apache.xml.security.utils.XMLUtils.decode(getNonce());
if (passwordsAreEncoded) {
node.setData(UsernameTokenUtil.doPasswordDigest(decodedNonce, getCreated(),
org.apache.xml.security.utils.XMLUtils.decode(pwd)));
} else {
node.setData(UsernameTokenUtil.doPasswordDigest(decodedNonce, getCreated(), pwd));
}
} else {
node.setData(pwd);
}
if (passwordType != null) {
elementPassword.setAttributeNS(null, "Type", passwordType);
}
} catch (Exception e) {
LOG.debug(e.getMessage(), e);
}
}
/**
* @param passwordsAreEncoded whether passwords are encoded
*/
public void setPasswordsAreEncoded(boolean passwordsAreEncoded) {
this.passwordsAreEncoded = passwordsAreEncoded;
}
/**
* @return whether passwords are encoded
*/
public boolean getPasswordsAreEncoded() {
return passwordsAreEncoded;
}
/**
* Returns the first text node of an element.
*
* @param e the element to get the node from
* @return the first text node or <code>null</code> if node is null or is
* not a text node
*/
private Text getFirstNode(Element e) {
Node node = e.getFirstChild();
return node != null && Node.TEXT_NODE == node.getNodeType() ? (Text) node : null;
}
/**
* Returns the dom element of this <code>UsernameToken</code> object.
*
* @return the <code>wsse:UsernameToken</code> element
*/
public Element getElement() {
return element;
}
/**
* Returns the string representation of the token.
*
* @return a XML string representation
*/
public String toString() {
return DOM2Writer.nodeToString(element);
}
/**
* Gets the id.
*
* @return the value of the <code>wsu:Id</code> attribute of this username
* token
*/
public String getID() {
return element.getAttributeNS(WSConstants.WSU_NS, "Id");
}
/**
* Set the id of this username token.
*
* @param id
* the value for the <code>wsu:Id</code> attribute of this
* username token
*/
public void setID(String id) {
element.setAttributeNS(WSConstants.WSU_NS, WSConstants.WSU_PREFIX + ":Id", id);
}
/**
* This method gets a derived key as defined in WSS Username Token Profile.
*
* @param rawPassword The raw password to use to derive the key
* @return Returns the derived key as a byte array
* @throws WSSecurityException
*/
public byte[] getDerivedKey(BSPEnforcer bspEnforcer, String rawPassword) throws WSSecurityException {
if (rawPassword == null) {
LOG.warn("The raw password was null");
throw new WSSecurityException(WSSecurityException.ErrorCode.FAILED_AUTHENTICATION);
}
if (elementSalt == null) {
// We must have a salt element to use this token for a derived key
bspEnforcer.handleBSPRule(BSPRule.R4217);
}
if (elementIteration == null) {
// we must have an iteration element to use this token for a derived key
bspEnforcer.handleBSPRule(BSPRule.R4218);
} else {
String iter = XMLUtils.getElementText(elementIteration);
try {
if (iter == null || Integer.parseInt(iter) < 1000) {
bspEnforcer.handleBSPRule(BSPRule.R4218);
}
} catch (NumberFormatException ex) {
throw new WSSecurityException(
WSSecurityException.ErrorCode.FAILURE, ex, "decoding.general"
);
}
}
int iteration = getIteration();
byte[] salt = getSalt();
if (passwordsAreEncoded) {
return UsernameTokenUtil.generateDerivedKey(org.apache.xml.security.utils.XMLUtils.decode(rawPassword),
salt, iteration);
} else {
return UsernameTokenUtil.generateDerivedKey(rawPassword, salt, iteration);
}
}
/**
* Return whether the UsernameToken represented by this class is to be used
* for key derivation as per the UsernameToken Profile 1.1. It does this by
* checking that the username token has salt and iteration values.
*
* @throws WSSecurityException
*/
public boolean isDerivedKey() throws WSSecurityException {
if (elementSalt != null && elementIteration != null) {
return true;
}
return false;
}
/**
* Create a WSUsernameTokenPrincipal from this UsernameToken object
*/
public Principal createPrincipal() throws WSSecurityException {
WSUsernameTokenPrincipalImpl principal =
new WSUsernameTokenPrincipalImpl(getName(), isHashed());
String nonce = getNonce();
if (nonce != null) {
principal.setNonce(org.apache.xml.security.utils.XMLUtils.decode(nonce));
}
principal.setPassword(getPassword());
principal.setCreatedTime(getCreated());
return principal;
}
/**
* Return true if the "Created" value is before the current time minus the timeToLive
* argument, and if the Created value is not "in the future".
*
* @param timeToLive the value in seconds for the validity of the Created time
* @param futureTimeToLive the value in seconds for the future validity of the Created time
* @return true if the UsernameToken is before (now-timeToLive), false otherwise
*/
public boolean verifyCreated(
int timeToLive,
int futureTimeToLive
) {
return DateUtil.verifyCreated(created, timeToLive, futureTimeToLive);
}
@Override
public int hashCode() {
int result = 17;
String username = getName();
if (username != null) {
result = 31 * result + username.hashCode();
}
String password = getPassword();
if (password != null) {
result = 31 * result + password.hashCode();
}
String passwordType = getPasswordType();
if (passwordType != null) {
result = 31 * result + passwordType.hashCode();
}
String nonce = getNonce();
if (nonce != null) {
result = 31 * result + nonce.hashCode();
}
String created = getCreated();
if (created != null) {
result = 31 * result + created.hashCode();
}
try {
byte[] salt = getSalt();
if (salt != null) {
result = 31 * result + Arrays.hashCode(salt);
}
} catch (WSSecurityException ex) {
LOG.debug(ex.getMessage(), ex);
}
result = 31 * result + Integer.valueOf(getIteration()).hashCode();
return result;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof UsernameToken)) {
return false;
}
UsernameToken usernameToken = (UsernameToken)object;
if (!compare(usernameToken.getName(), getName())) {
return false;
}
if (!compare(usernameToken.getPassword(), getPassword())) {
return false;
}
if (!compare(usernameToken.getPasswordType(), getPasswordType())) {
return false;
}
if (!compare(usernameToken.getNonce(), getNonce())) {
return false;
}
if (!compare(usernameToken.getCreated(), getCreated())) {
return false;
}
try {
byte[] salt = usernameToken.getSalt();
if (!Arrays.equals(salt, getSalt())) {
return false;
}
} catch (WSSecurityException ex) {
LOG.debug(ex.getMessage(), ex);
}
int iteration = usernameToken.getIteration();
if (iteration != getIteration()) {
return false;
}
return true;
}
private boolean compare(String item1, String item2) {
if (item1 == null && item2 != null) {
return false;
} else if (item1 != null && !item1.equals(item2)) {
return false;
}
return true;
}
/**
* A method to check that the UsernameToken is compliant with the BSP spec.
* @throws WSSecurityException
*/
private void checkBSPCompliance(BSPEnforcer bspEnforcer) throws WSSecurityException {
List<Element> passwordElements =
WSSecurityUtil.getDirectChildElements(
element, WSConstants.PASSWORD_LN, WSConstants.WSSE_NS
);
// We can only have one password element
if (passwordElements.size() > 1) {
LOG.debug("The Username Token had more than one password element");
bspEnforcer.handleBSPRule(BSPRule.R4222);
}
// We must have a password type
if (passwordElements.size() == 1) {
Element passwordChild = passwordElements.get(0);
String type = passwordChild.getAttributeNS(null, WSConstants.PASSWORD_TYPE_ATTR);
if (type == null || "".equals(type)) {
LOG.debug("The Username Token password does not have a Type attribute");
bspEnforcer.handleBSPRule(BSPRule.R4201);
}
}
List<Element> createdElements =
WSSecurityUtil.getDirectChildElements(
element, WSConstants.CREATED_LN, WSConstants.WSU_NS
);
// We can only have one created element
if (createdElements.size() > 1) {
LOG.debug("The Username Token has more than one created element");
bspEnforcer.handleBSPRule(BSPRule.R4223);
}
List<Element> nonceElements =
WSSecurityUtil.getDirectChildElements(
element, WSConstants.NONCE_LN, WSConstants.WSSE_NS
);
// We can only have one nonce element
if (nonceElements.size() > 1) {
LOG.debug("The Username Token has more than one nonce element");
bspEnforcer.handleBSPRule(BSPRule.R4225);
}
if (nonceElements.size() == 1) {
Element nonce = nonceElements.get(0);
String encodingType = nonce.getAttributeNS(null, "EncodingType");
// Encoding Type must be equal to Base64Binary
if (encodingType == null || "".equals(encodingType)) {
bspEnforcer.handleBSPRule(BSPRule.R4220);
} else if (!WSConstants.BASE64_ENCODING.equals(encodingType)) {
LOG.debug("The Username Token's nonce element has a bad encoding type");
bspEnforcer.handleBSPRule(BSPRule.R4221);
}
}
}
}