This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
The following commit(s) were added to refs/heads/master by this push:
new 2a67fc398 Properly wire UsernameToken + RSA15 flags for the streaming
layer (#717)
2a67fc398 is described below
commit 2a67fc398cbe7cb4b9315773bf73b008ed6d9e95
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Sep 21 09:29:11 2026 +0100
Properly wire UsernameToken + RSA15 flags for the streaming layer (#717)
---
THREAT-MODEL.md | 27 ++++
src/site/asciidoc/streaming.adoc | 8 ++
.../wss4j/policy/stax/enforcer/PolicyEnforcer.java | 124 ++++++++++++++---
.../policy/stax/enforcer/PolicyInputProcessor.java | 16 ++-
.../policy/stax/test/ScopedEngineDefaultsTest.java | 155 +++++++++++++++++++++
.../testdata/wsdl/mixedPasswordPolicies.wsdl | 98 +++++++++++++
6 files changed, 404 insertions(+), 24 deletions(-)
diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md
index 5852447bf..0ea98b153 100644
--- a/THREAT-MODEL.md
+++ b/THREAT-MODEL.md
@@ -573,6 +573,33 @@ on each is captured in §14 Q10–Q11.
- *(documented:
`ws-security-policy-stax/src/test/java/.../VulnerabliltyVectorsTest.java`)*
+### P12 — An engine default relaxed by policy is scoped to the operation the
message invokes (StAX policy mode)
+
+- **Condition**: streaming engine in policy mode (`PolicyInputProcessor`),
+ where the caller has not itself set `AllowUsernameTokenNoPassword` or
+ `AllowRSA15KeyTransportAlgorithm`. Those are explicit overrides by the
+ caller and are left alone.
+- **Violation symptom**: one operation of an endpoint asks for
+ `sp:NoPassword`, or for an AlgorithmSuite whose asymmetric key wrap is
+ rsa-1_5, and the engine's corresponding hardened default is thereby
+ lowered for every other operation of that endpoint — so a
+ password-less UsernameToken is accepted for an operation whose policy
+ names no UsernameToken at all, and is examined by no assertion.
+- **Mechanism**: the relaxation is decided while the security header is
+ read, before the Body names the operation. It is therefore scoped to
+ the operation's own policy where SOAPAction already selected it, and
+ otherwise granted across the policy set and reimposed once the
+ operation is known.
+- **Residual**: an rsa-1_5 unwrap granted across the policy set is
+ performed before the operation is known. The message is rejected, but
+ the unwrap has happened, so the oracle surface of §8 P4 is reachable
+ for an operation whose own policy forbids rsa-1_5. Only a
+ SOAPAction-selected operation avoids that.
+- **Severity**: **security-critical** for the UsernameToken case;
+ `VALID-HARDENING` for the rsa-1_5 case.
+- *(documented:
+ `ws-security-policy-stax/src/test/java/.../ScopedEngineDefaultsTest.java`)*
+
## §9 Security properties the project does *not* provide
State each plainly so a triager can route an inbound report to the
diff --git a/src/site/asciidoc/streaming.adoc b/src/site/asciidoc/streaming.adoc
index 3d5484c85..705fc26e1 100644
--- a/src/site/asciidoc/streaming.adoc
+++ b/src/site/asciidoc/streaming.adoc
@@ -87,6 +87,14 @@ nothing on its own. Check the effective policy if you rely
on a nested
assertion being enforced.
* Where a compact policy offers several alternatives within a nested policy,
only the first is read.
+ * A hardened engine default that WS-SecurityPolicy relaxes - accepting a
+password-less UsernameToken for an sp:NoPassword policy, or rsa-1_5 key
transport
+for an AlgorithmSuite that asks for it - has to be decided while the security
+header is read, before the Body says which operation the message invokes. Where
+SOAPAction has already selected the operation only that operation's policy is
+consulted. Otherwise the relaxation is granted across the endpoint's operations
+and reimposed once the operation is known, which rejects the message but, for
+rsa-1_5, only after the key has been unwrapped.
* The REQUIRE_SIGNED_ENCRYPTED_DATA_ELEMENTS
("requireSignedEncryptedDataElements")
configuration tag is not read by the streaming code and is not enforced by it.
The tag is shared between both stacks, and the streaming ConfigurationConverter
diff --git
a/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyEnforcer.java
b/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyEnforcer.java
index db1322d96..c3280c74f 100644
---
a/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyEnforcer.java
+++
b/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyEnforcer.java
@@ -104,8 +104,10 @@ import
org.apache.wss4j.policy.stax.assertionStates.X509TokenAssertionState;
import org.apache.wss4j.stax.ext.WSSConstants;
import org.apache.wss4j.stax.securityEvent.NoSecuritySecurityEvent;
import org.apache.wss4j.stax.securityEvent.OperationSecurityEvent;
+import org.apache.wss4j.stax.securityEvent.UsernameTokenSecurityEvent;
import org.apache.wss4j.stax.securityEvent.WSSecurityEventConstants;
import org.apache.xml.security.exceptions.XMLSecurityException;
+import org.apache.xml.security.stax.securityEvent.AlgorithmSuiteSecurityEvent;
import org.apache.xml.security.stax.securityEvent.SecurityEvent;
import org.apache.xml.security.stax.securityEvent.SecurityEventConstants;
import org.apache.xml.security.stax.securityEvent.SecurityEventListener;
@@ -156,6 +158,8 @@ public class PolicyEnforcer implements
SecurityEventListener {
private boolean faultOccurred;
private final PolicyAsserter policyAsserter;
private boolean soap12;
+ private boolean usernameTokenNoPasswordRelaxedForAllOperations;
+ private boolean rsa15KeyTransportRelaxedForAllOperations;
public PolicyEnforcer(List<OperationPolicy> operationPolicies, String
soapAction, boolean initiator,
String actorOrRole, int attachmentCount,
PolicyAsserter policyAsserter, boolean soap12
@@ -892,6 +896,11 @@ public class PolicyEnforcer implements
SecurityEventListener {
new IllegalArgumentException(message));
}
+ //The operation is known now, so an engine default relaxed on the
strength of
+ //the whole policy set can be reimposed where this operation's
policy does not
+ //ask for it.
+ verifyRelaxedEngineDefaults();
+
try {
Iterator<SecurityEvent> securityEventIterator =
securityEventQueue.descendingIterator();
while (securityEventIterator.hasNext()) {
@@ -923,18 +932,25 @@ public class PolicyEnforcer implements
SecurityEventListener {
}
/**
- * Returns true if any configured operation policy contains a
UsernameToken assertion
- * that explicitly allows password-less tokens (sp:NoPassword). Used to
decide whether
- * the engine's hardened default (rejecting password-less UsernameTokens)
may be
- * relaxed in policy mode.
+ * Returns true if the policy that governs this message contains a
UsernameToken
+ * assertion that explicitly allows password-less tokens (sp:NoPassword).
Used to
+ * decide whether the engine's hardened default (rejecting password-less
+ * UsernameTokens) may be relaxed in policy mode.
+ *
+ * This is asked while the security header is being read, so the operation
is known
+ * only where SOAPAction already selected its policy. Failing that the
question can be
+ * answered across the configured operations and no more, which relaxes
the default for
+ * a message that may turn out to invoke an operation whose own policy
does not allow
+ * it. Record that, so that verifyRelaxedEngineDefaults() can reimpose the
default once
+ * the operation is known.
*/
public boolean isUsernameTokenNoPasswordAllowedByPolicy() {
+ if (effectivePolicy != null) {
+ return allowsUsernameTokenNoPassword(effectivePolicy);
+ }
for (OperationPolicy operationPolicy : operationPolicies) {
- org.apache.neethi.Policy policy = operationPolicy.getPolicy();
- if (policy != null && policyContains(policy,
- assertion -> assertion instanceof UsernameToken
- && ((UsernameToken)assertion).getPasswordType()
- == UsernameToken.PasswordType.NoPassword)) {
+ if (allowsUsernameTokenNoPassword(operationPolicy)) {
+ usernameTokenNoPasswordRelaxedForAllOperations = true;
return true;
}
}
@@ -942,28 +958,92 @@ public class PolicyEnforcer implements
SecurityEventListener {
}
/**
- * Returns true if any configured operation policy contains an
AlgorithmSuite whose
- * asymmetric key wrap is RSA v1.5. Used to decide whether the engine's
hardened
- * default (rejecting rsa-1_5 key transport) may be relaxed in policy mode.
+ * Returns true if the policy that governs this message contains an
AlgorithmSuite whose
+ * asymmetric key wrap is RSA v1.5. Used to decide whether the engine's
hardened default
+ * (rejecting rsa-1_5 key transport) may be relaxed in policy mode. Scoped
to the
+ * operation, and recorded when it cannot be, exactly as above.
*/
public boolean isRSA15KeyTransportAllowedByPolicy() {
+ if (effectivePolicy != null) {
+ return allowsRSA15KeyTransport(effectivePolicy);
+ }
for (OperationPolicy operationPolicy : operationPolicies) {
- org.apache.neethi.Policy policy = operationPolicy.getPolicy();
- if (policy != null && policyContains(policy, assertion -> {
- if (!(assertion instanceof AlgorithmSuite)) {
- return false;
- }
- AlgorithmSuite.AlgorithmSuiteType algorithmSuiteType =
- ((AlgorithmSuite)assertion).getAlgorithmSuiteType();
- return algorithmSuiteType != null
- &&
SPConstants.KW_RSA15.equals(algorithmSuiteType.getAsymmetricKeyWrap());
- })) {
+ if (allowsRSA15KeyTransport(operationPolicy)) {
+ rsa15KeyTransportRelaxedForAllOperations = true;
return true;
}
}
return false;
}
+ private static boolean allowsUsernameTokenNoPassword(OperationPolicy
operationPolicy) {
+ Policy policy = operationPolicy.getPolicy();
+ return policy != null && policyContains(policy,
+ assertion -> assertion instanceof UsernameToken
+ && ((UsernameToken)assertion).getPasswordType()
+ == UsernameToken.PasswordType.NoPassword);
+ }
+
+ private static boolean allowsRSA15KeyTransport(OperationPolicy
operationPolicy) {
+ Policy policy = operationPolicy.getPolicy();
+ return policy != null && policyContains(policy, assertion -> {
+ if (!(assertion instanceof AlgorithmSuite)) {
+ return false;
+ }
+ AlgorithmSuite.AlgorithmSuiteType algorithmSuiteType =
+ ((AlgorithmSuite)assertion).getAlgorithmSuiteType();
+ return algorithmSuiteType != null
+ &&
SPConstants.KW_RSA15.equals(algorithmSuiteType.getAsymmetricKeyWrap());
+ });
+ }
+
+ /**
+ * An engine default that was relaxed on the strength of the whole set of
operation
+ * policies - because the operation was not yet known when the security
header was read
+ * - must still hold for the operation the message turned out to invoke.
Otherwise one
+ * operation that asks for sp:NoPassword or for rsa-1_5 lowers the
engine's floor for
+ * every other operation of the endpoint, and whether that is caught
depends on whether
+ * the effective policy happens to carry an assertion that rejects what
was accepted: a
+ * password-less UsernameToken is not examined at all by a policy that
names no
+ * UsernameToken.
+ */
+ private void verifyRelaxedEngineDefaults() throws WSSecurityException {
+ if (usernameTokenNoPasswordRelaxedForAllOperations
+ && !allowsUsernameTokenNoPassword(effectivePolicy)) {
+ for (SecurityEvent securityEvent : securityEventQueue) {
+ if (securityEvent instanceof UsernameTokenSecurityEvent
+ &&
((UsernameTokenSecurityEvent)securityEvent).getSecurityToken() != null
+ && WSSConstants.UsernameTokenPasswordType.PASSWORD_NONE
+ ==
((UsernameTokenSecurityEvent)securityEvent).getUsernameTokenPasswordType()) {
+ rejectRelaxedEngineDefault("a UsernameToken with no
password");
+ }
+ }
+ }
+ if (rsa15KeyTransportRelaxedForAllOperations
+ && !allowsRSA15KeyTransport(effectivePolicy)) {
+ for (SecurityEvent securityEvent : securityEventQueue) {
+ if (securityEvent instanceof AlgorithmSuiteSecurityEvent) {
+ AlgorithmSuiteSecurityEvent algorithmSuiteSecurityEvent =
+ (AlgorithmSuiteSecurityEvent)securityEvent;
+ if
(WSSConstants.Asym_Key_Wrap.equals(algorithmSuiteSecurityEvent.getAlgorithmUsage())
+ &&
WSSConstants.NS_XENC_RSA15.equals(algorithmSuiteSecurityEvent.getAlgorithmURI()))
{
+ rejectRelaxedEngineDefault("rsa-1_5 key transport");
+ }
+ }
+ }
+ }
+ }
+
+ private void rejectRelaxedEngineDefault(String what) throws
WSSecurityException {
+ String message = "The message uses " + what + ", which the policy for
operation "
+ + effectivePolicy.getOperationName() + " does not allow";
+ LOG.warn("{}; rejecting the message", message);
+ securityEventQueue.clear();
+ throw new WSSecurityException(
+ WSSecurityException.ErrorCode.INVALID_SECURITY,
+ new IllegalArgumentException(message));
+ }
+
private static boolean policyContains(PolicyComponent policyComponent,
Predicate<Assertion> predicate) {
if (policyComponent instanceof PolicyOperator) {
for (PolicyComponent childComponent
diff --git
a/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyInputProcessor.java
b/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyInputProcessor.java
index 78a4d3ef2..90d7264fd 100644
---
a/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyInputProcessor.java
+++
b/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyInputProcessor.java
@@ -254,11 +254,23 @@ public class PolicyInputProcessor extends
AbstractInputProcessor {
// unconditionally, which silently reversed both defaults even
when the
// configured policy contained no assertion that re-imposes the
check. Only
// relax an engine default when the policy actually covers it.
- if (policyEnforcer.isRSA15KeyTransportAllowedByPolicy()) {
+ // Where the caller has already relaxed a default itself there is
nothing to
+ // grant, and asking would arm a re-check against a policy the
caller has
+ // deliberately overruled, so leave that case alone. The
properties are
+ // optional on this processor - a caller may construct it with
none - and then
+ // the caller has relaxed nothing.
+ XMLSecurityProperties properties = getSecurityProperties();
+ WSSSecurityProperties securityProperties =
+ properties instanceof WSSSecurityProperties ?
(WSSSecurityProperties) properties : null;
+ boolean callerAllowsRSA15 =
+ securityProperties != null &&
securityProperties.isAllowRSA15KeyTransportAlgorithm();
+ boolean callerAllowsNoPassword =
+ securityProperties != null &&
securityProperties.isAllowUsernameTokenNoPassword();
+ if (!callerAllowsRSA15 &&
policyEnforcer.isRSA15KeyTransportAllowedByPolicy()) {
inputProcessorChain.getSecurityContext().put(
WSSConstants.PROP_ALLOW_RSA15_KEYTRANSPORT_ALGORITHM,
Boolean.TRUE);
}
- if (policyEnforcer.isUsernameTokenNoPasswordAllowedByPolicy()) {
+ if (!callerAllowsNoPassword &&
policyEnforcer.isUsernameTokenNoPasswordAllowedByPolicy()) {
inputProcessorChain.getSecurityContext().put(
WSSConstants.PROP_ALLOW_USERNAMETOKEN_NOPASSWORD,
Boolean.TRUE.toString());
}
diff --git
a/ws-security-policy-stax/src/test/java/org/apache/wss4j/policy/stax/test/ScopedEngineDefaultsTest.java
b/ws-security-policy-stax/src/test/java/org/apache/wss4j/policy/stax/test/ScopedEngineDefaultsTest.java
new file mode 100644
index 000000000..b97605567
--- /dev/null
+++
b/ws-security-policy-stax/src/test/java/org/apache/wss4j/policy/stax/test/ScopedEngineDefaultsTest.java
@@ -0,0 +1,155 @@
+/**
+ * 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.policy.stax.test;
+
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+
+import javax.xml.namespace.QName;
+
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.apache.wss4j.common.util.DateUtil;
+import org.apache.wss4j.policy.stax.enforcer.PolicyEnforcer;
+import org.apache.wss4j.policy.stax.enforcer.PolicyEnforcerFactory;
+import org.apache.wss4j.stax.ext.WSSConstants;
+import org.apache.wss4j.stax.impl.securityToken.UsernameSecurityTokenImpl;
+import org.apache.wss4j.stax.securityEvent.OperationSecurityEvent;
+import org.apache.wss4j.stax.securityEvent.UsernameTokenSecurityEvent;
+import org.apache.wss4j.stax.securityToken.WSSecurityTokenConstants;
+import org.apache.xml.security.exceptions.XMLSecurityException;
+import org.apache.xml.security.stax.impl.util.IDGenerator;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The engine's hardened defaults - rejecting password-less UsernameTokens and
rsa-1_5 key
+ * transport - are relaxed in policy mode so that the corresponding policy
assertions can
+ * take over. The relaxation belongs to the operation whose policy asks for
it, not to every
+ * operation of the endpoint.
+ *
+ * The WSDL behind these tests has two operations: one asks for sp:NoPassword,
the other
+ * names no UsernameToken at all, so nothing in its own policy would reject a
password-less
+ * token that the engine has already accepted.
+ */
+public class ScopedEngineDefaultsTest extends AbstractPolicyTestBase {
+
+ private static final String NAMESPACE =
"http://www.example.net/MixedPolicyService";
+ private static final QName NO_PASSWORD_OPERATION = new QName(NAMESPACE,
"noPasswordOperation");
+ private static final QName PASSWORD_OPERATION = new QName(NAMESPACE,
"passwordOperation");
+
+ /**
+ * Where SOAPAction has already selected the operation, only that
operation's policy
+ * decides. The operation that asks for no password gets the relaxation;
the one that
+ * does not, does not.
+ */
+ @Test
+ public void testRelaxationIsScopedToTheOperationSelectedBySOAPAction()
throws Exception {
+
assertTrue(newPolicyEnforcer("noPasswordOperationAction").isUsernameTokenNoPasswordAllowedByPolicy());
+
assertFalse(newPolicyEnforcer("passwordOperationAction").isUsernameTokenNoPasswordAllowedByPolicy());
+ }
+
+ /**
+ * Without a SOAPAction the operation is not known while the security
header is being
+ * read, so the question can only be answered across every configured
operation. The
+ * engine default is still relaxed, as before, or a deployment whose
operation cannot be
+ * determined that early would stop working.
+ */
+ @Test
+ public void testRelaxationStillGrantedWhenTheOperationIsNotYetKnown()
throws Exception {
+
assertTrue(newPolicyEnforcer("").isUsernameTokenNoPasswordAllowedByPolicy());
+ }
+
+ /**
+ * ...but once the operation turns out to be the one whose policy says
nothing about
+ * UsernameTokens, a password-less token accepted under that relaxation is
rejected.
+ */
+ @Test
+ public void
testPasswordlessTokenRejectedForAnOperationThatDoesNotAllowIt() throws
Exception {
+ PolicyEnforcer policyEnforcer = newPolicyEnforcer("");
+ assertTrue(policyEnforcer.isUsernameTokenNoPasswordAllowedByPolicy());
+ policyEnforcer.registerSecurityEvent(passwordlessUsernameTokenEvent());
+
+ OperationSecurityEvent operationSecurityEvent = new
OperationSecurityEvent();
+ operationSecurityEvent.setOperation(PASSWORD_OPERATION);
+
+ WSSecurityException ex = assertThrows(WSSecurityException.class,
+ () ->
policyEnforcer.registerSecurityEvent(operationSecurityEvent));
+
+ assertEquals("The message uses a UsernameToken with no password, which
the policy for "
+ + "operation " + PASSWORD_OPERATION + " does not
allow",
+ ex.getCause().getMessage());
+ assertEquals(WSSecurityException.INVALID_SECURITY, ex.getFaultCode());
+ }
+
+ /**
+ * The same message is accepted for the operation whose policy does ask for
+ * sp:NoPassword.
+ */
+ @Test
+ public void testPasswordlessTokenAcceptedForTheOperationThatAllowsIt()
throws Exception {
+ PolicyEnforcer policyEnforcer = newPolicyEnforcer("");
+ assertTrue(policyEnforcer.isUsernameTokenNoPasswordAllowedByPolicy());
+ policyEnforcer.registerSecurityEvent(passwordlessUsernameTokenEvent());
+
+ OperationSecurityEvent operationSecurityEvent = new
OperationSecurityEvent();
+ operationSecurityEvent.setOperation(NO_PASSWORD_OPERATION);
+
+ policyEnforcer.registerSecurityEvent(operationSecurityEvent);
+ }
+
+ /**
+ * A message that carries no password-less token is unaffected, even
though the
+ * relaxation was granted across the endpoint.
+ */
+ @Test
+ public void testOperationWithoutAPasswordlessTokenIsUnaffected() throws
Exception {
+ PolicyEnforcer policyEnforcer = newPolicyEnforcer("");
+ assertTrue(policyEnforcer.isUsernameTokenNoPasswordAllowedByPolicy());
+
+ OperationSecurityEvent operationSecurityEvent = new
OperationSecurityEvent();
+ operationSecurityEvent.setOperation(PASSWORD_OPERATION);
+
+ policyEnforcer.registerSecurityEvent(operationSecurityEvent);
+ }
+
+ private PolicyEnforcer newPolicyEnforcer(String soapAction) throws
Exception {
+ PolicyEnforcerFactory policyEnforcerFactory =
PolicyEnforcerFactory.newInstance(
+
this.getClass().getClassLoader().getResource("testdata/wsdl/mixedPasswordPolicies.wsdl"));
+ return policyEnforcerFactory.newPolicyEnforcer(soapAction, false,
null, 0, false);
+ }
+
+ private UsernameTokenSecurityEvent passwordlessUsernameTokenEvent() throws
XMLSecurityException {
+ UsernameTokenSecurityEvent usernameTokenSecurityEvent = new
UsernameTokenSecurityEvent();
+
usernameTokenSecurityEvent.setUsernameTokenProfile(WSSConstants.NS_USERNAMETOKEN_PROFILE11);
+ ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
+ String created = DateUtil.getDateTimeFormatter(true).format(now);
+ UsernameSecurityTokenImpl usernameSecurityToken = new
UsernameSecurityTokenImpl(
+ WSSConstants.UsernameTokenPasswordType.PASSWORD_NONE,
+ "username", null, created, null, new byte[10], 10L,
+ null, IDGenerator.generateID(null),
+
WSSecurityTokenConstants.KEYIDENTIFIER_SECURITY_TOKEN_DIRECT_REFERENCE);
+
usernameSecurityToken.addTokenUsage(WSSecurityTokenConstants.TOKENUSAGE_SUPPORTING_TOKENS);
+ usernameTokenSecurityEvent.setSecurityToken(usernameSecurityToken);
+ return usernameTokenSecurityEvent;
+ }
+}
diff --git
a/ws-security-policy-stax/src/test/resources/testdata/wsdl/mixedPasswordPolicies.wsdl
b/ws-security-policy-stax/src/test/resources/testdata/wsdl/mixedPasswordPolicies.wsdl
new file mode 100644
index 000000000..015ceb6ac
--- /dev/null
+++
b/ws-security-policy-stax/src/test/resources/testdata/wsdl/mixedPasswordPolicies.wsdl
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<wsdl:definitions
+ name="MixedPolicyService"
+ targetNamespace="http://www.example.net/MixedPolicyService"
+ xmlns:tns="http://www.example.net/MixedPolicyService"
+ xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
+ xmlns:sp="http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702"
+
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+ xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
+ >
+
+ <!-- Two operations of one endpoint, only one of which asks for
password-less
+ UsernameTokens. The other names no UsernameToken at all, so nothing
in its own
+ policy would reject one that the engine has already accepted. -->
+
+ <wsdl:types>
+ <xs:schema targetNamespace="http://www.example.net/MixedPolicyService"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="noPasswordOperation" type="xs:string"/>
+ <xs:element name="passwordOperation" type="xs:string"/>
+ </xs:schema>
+ </wsdl:types>
+
+ <wsp:Policy wsu:Id="NoPasswordPolicy">
+ <wsp:ExactlyOne>
+ <wsp:All>
+ <sp:SupportingTokens>
+ <wsp:Policy>
+ <sp:UsernameToken>
+ <wsp:Policy>
+ <sp:NoPassword/>
+ </wsp:Policy>
+ </sp:UsernameToken>
+ </wsp:Policy>
+ </sp:SupportingTokens>
+ </wsp:All>
+ </wsp:ExactlyOne>
+ </wsp:Policy>
+
+ <wsdl:message name="noPasswordOperationRequest">
+ <wsdl:part name="parameters" element="tns:noPasswordOperation"/>
+ </wsdl:message>
+ <wsdl:message name="passwordOperationRequest">
+ <wsdl:part name="parameters" element="tns:passwordOperation"/>
+ </wsdl:message>
+
+ <wsdl:portType name="MixedPolicyPort">
+ <wsdl:operation name="noPasswordOperation">
+ <wsdl:input message="tns:noPasswordOperationRequest"/>
+ </wsdl:operation>
+ <wsdl:operation name="passwordOperation">
+ <wsdl:input message="tns:passwordOperationRequest"/>
+ </wsdl:operation>
+ </wsdl:portType>
+
+ <wsdl:binding name="MixedPolicySOAPBinding" type="tns:MixedPolicyPort">
+ <soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document"/>
+ <wsdl:operation name="noPasswordOperation">
+ <wsp:PolicyReference URI="#NoPasswordPolicy"/>
+ <soap:operation soapAction="noPasswordOperationAction"
style="document"/>
+ <wsdl:input>
+ <soap:body use="literal"/>
+ </wsdl:input>
+ </wsdl:operation>
+ <wsdl:operation name="passwordOperation">
+ <soap:operation soapAction="passwordOperationAction"
style="document"/>
+ <wsdl:input>
+ <soap:body use="literal"/>
+ </wsdl:input>
+ </wsdl:operation>
+ </wsdl:binding>
+
+ <wsdl:service name="MixedPolicyService">
+ <wsdl:port name="MixedPolicy" binding="tns:MixedPolicySOAPBinding">
+ <soap:address location="http://localhost:8080/MixedPolicyService"/>
+ </wsdl:port>
+ </wsdl:service>
+</wsdl:definitions>