/**
* 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.cxf.ws.security.wss4j;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.security.auth.Subject;
import org.apache.cxf.binding.soap.SoapFault;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.SoapVersion;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.interceptor.security.DefaultSecurityContext;
import org.apache.cxf.interceptor.security.RolePrefixSecurityContextImpl;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.rt.security.claims.ClaimCollection;
import org.apache.cxf.rt.security.saml.claims.SAMLSecurityContext;
import org.apache.cxf.rt.security.saml.utils.SAMLUtils;
import org.apache.cxf.rt.security.utils.SecurityUtils;
import org.apache.cxf.security.SecurityContext;
import org.apache.cxf.ws.security.SecurityConstants;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.principal.SAMLTokenPrincipal;
import org.apache.wss4j.common.saml.SamlAssertionWrapper;
import org.apache.wss4j.stax.securityEvent.KerberosTokenSecurityEvent;
import org.apache.wss4j.stax.securityEvent.KeyValueTokenSecurityEvent;
import org.apache.wss4j.stax.securityEvent.SamlTokenSecurityEvent;
import org.apache.wss4j.stax.securityEvent.UsernameTokenSecurityEvent;
import org.apache.wss4j.stax.securityEvent.WSSecurityEventConstants;
import org.apache.wss4j.stax.securityEvent.X509TokenSecurityEvent;
import org.apache.wss4j.stax.securityToken.SubjectAndPrincipalSecurityToken;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.stax.securityEvent.SecurityEvent;
import org.apache.xml.security.stax.securityEvent.SecurityEventConstants.Event;
import org.apache.xml.security.stax.securityToken.SecurityTokenConstants.TokenUsage;
/**
* This interceptor handles parsing the StaX WS-Security results (events) + sets up the
* security context appropriately.
*/
public class StaxSecurityContextInInterceptor extends AbstractPhaseInterceptor<SoapMessage> {
/**
* This configuration tag specifies the default attribute name where the roles are present
* The default is "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role".
*/
public static final String SAML_ROLE_ATTRIBUTENAME_DEFAULT =
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role";
public StaxSecurityContextInInterceptor() {
super(Phase.PRE_PROTOCOL);
}
@Override
public void handleMessage(SoapMessage soapMessage) throws Fault {
@SuppressWarnings("unchecked")
final List<SecurityEvent> incomingSecurityEventList =
(List<SecurityEvent>)soapMessage.get(SecurityEvent.class.getName() + ".in");
if (incomingSecurityEventList != null) {
try {
doResults(soapMessage, incomingSecurityEventList);
} catch (WSSecurityException e) {
throw createSoapFault(soapMessage.getVersion(), e);
}
}
}
private void doResults(SoapMessage msg, List<SecurityEvent> incomingSecurityEventList) throws WSSecurityException {
// Now go through the results in a certain order to set up a security context. Highest priority is first.
List<Event> desiredSecurityEvents = new ArrayList<>();
desiredSecurityEvents.add(WSSecurityEventConstants.SAML_TOKEN);
desiredSecurityEvents.add(WSSecurityEventConstants.USERNAME_TOKEN);
desiredSecurityEvents.add(WSSecurityEventConstants.KERBEROS_TOKEN);
desiredSecurityEvents.add(WSSecurityEventConstants.X509Token);
desiredSecurityEvents.add(WSSecurityEventConstants.KeyValueToken);
for (Event desiredEvent : desiredSecurityEvents) {
SubjectAndPrincipalSecurityToken token = null;
try {
token = getSubjectPrincipalToken(incomingSecurityEventList, desiredEvent, msg);
} catch (XMLSecurityException ex) {
// proceed
}
if (token != null) {
Principal p = token.getPrincipal();
Subject subject = token.getSubject();
if (subject != null) {
String roleClassifier =
(String)msg.getContextualProperty(SecurityConstants.SUBJECT_ROLE_CLASSIFIER);
if (roleClassifier != null && !"".equals(roleClassifier)) {
String roleClassifierType =
(String)msg.getContextualProperty(SecurityConstants.SUBJECT_ROLE_CLASSIFIER_TYPE);
if (roleClassifierType == null || "".equals(roleClassifierType)) {
roleClassifierType = "prefix";
}
msg.put(
SecurityContext.class,
new RolePrefixSecurityContextImpl(subject, roleClassifier, roleClassifierType)
);
} else {
msg.put(SecurityContext.class, new DefaultSecurityContext(subject));
}
break;
} else if (p != null) {
Object receivedAssertion = null;
if (desiredEvent == WSSecurityEventConstants.SAML_TOKEN) {
String roleAttributeName = (String)SecurityUtils.getSecurityPropertyValue(
SecurityConstants.SAML_ROLE_ATTRIBUTENAME, msg);
if (roleAttributeName == null || roleAttributeName.length() == 0) {
roleAttributeName = SAML_ROLE_ATTRIBUTENAME_DEFAULT;
}
receivedAssertion = ((SAMLTokenPrincipal)token.getPrincipal()).getToken();
if (receivedAssertion != null) {
ClaimCollection claims =
SAMLUtils.getClaims((SamlAssertionWrapper)receivedAssertion);
Set<Principal> roles =
SAMLUtils.parseRolesFromClaims(claims, roleAttributeName, null);
SAMLSecurityContext context =
new SAMLSecurityContext(p, roles, claims);
msg.put(SecurityContext.class, context);
}
} else {
msg.put(SecurityContext.class, createSecurityContext(p));
}
break;
}
}
}
}
private SubjectAndPrincipalSecurityToken getSubjectPrincipalToken(List<SecurityEvent> incomingSecurityEventList,
Event desiredEvent,
Message msg) throws XMLSecurityException {
for (SecurityEvent event : incomingSecurityEventList) {
if (desiredEvent == event.getSecurityEventType()) {
if (event.getSecurityEventType() == WSSecurityEventConstants.USERNAME_TOKEN
&& isUsernameTokenEventAllowed((UsernameTokenSecurityEvent)event, msg)) {
return ((UsernameTokenSecurityEvent)event).getSecurityToken();
} else if (event.getSecurityEventType() == WSSecurityEventConstants.SAML_TOKEN
&& isSamlEventAllowed((SamlTokenSecurityEvent)event, msg)) {
return ((SamlTokenSecurityEvent)event).getSecurityToken();
} else if (event.getSecurityEventType() == WSSecurityEventConstants.X509Token
&& isUsedForPublicKeySignature(((X509TokenSecurityEvent)event).getSecurityToken())) {
return ((X509TokenSecurityEvent)event).getSecurityToken();
} else if (event.getSecurityEventType() == WSSecurityEventConstants.KeyValueToken
&& isUsedForPublicKeySignature(((KeyValueTokenSecurityEvent)event).getSecurityToken())) {
return ((KeyValueTokenSecurityEvent)event).getSecurityToken();
} else if (event.getSecurityEventType() == WSSecurityEventConstants.KERBEROS_TOKEN) {
return ((KerberosTokenSecurityEvent)event).getSecurityToken();
}
}
}
return null;
}
private boolean isUsedForPublicKeySignature(
SubjectAndPrincipalSecurityToken token
) throws XMLSecurityException {
if (token == null) {
return false;
}
// Check first of all that the token is used for Signature
List<TokenUsage> tokenUsages = token.getTokenUsages();
boolean usedForSignature = false;
if (tokenUsages != null) {
for (TokenUsage usage : tokenUsages) {
if ("MainSignature".equals(usage.getName())) {
usedForSignature = true;
break;
}
}
}
if (!usedForSignature) {
return false;
}
// Now check that a PublicKey/X509Certificate was used
return token.getPublicKey() != null
|| (token.getX509Certificates() != null && token.getX509Certificates().length > 0);
}
private boolean isSamlEventAllowed(SamlTokenSecurityEvent event, Message msg) {
if (event == null) {
return false;
}
boolean allowUnsignedSamlPrincipals =
SecurityUtils.getSecurityPropertyBoolean(
SecurityConstants.ENABLE_UNSIGNED_SAML_ASSERTION_PRINCIPAL, msg, false
);
// The SAML Assertion must be signed by default
return event.getSecurityToken() != null
&& event.getSecurityToken().getSamlAssertionWrapper() != null
&& (allowUnsignedSamlPrincipals || event.getSecurityToken().getSamlAssertionWrapper().isSigned());
}
private boolean isUsernameTokenEventAllowed(UsernameTokenSecurityEvent event, Message msg) {
if (event == null) {
return false;
}
boolean allowUTNoPassword =
SecurityUtils.getSecurityPropertyBoolean(
SecurityConstants.ENABLE_UT_NOPASSWORD_PRINCIPAL, msg, false
);
// The "no password" case is not allowed by default
return event.getSecurityToken() != null
&& (allowUTNoPassword || event.getSecurityToken().getPassword() != null);
}
private SecurityContext createSecurityContext(final Principal p) {
return new SecurityContext() {
public Principal getUserPrincipal() {
return p;
}
public boolean isUserInRole(String arg0) {
return false;
}
};
}
/**
* Create a SoapFault from a WSSecurityException, following the SOAP Message Security
* 1.1 specification, chapter 12 "Error Handling".
*
* When the Soap version is 1.1 then set the Fault/Code/Value from the fault code
* specified in the WSSecurityException (if it exists).
*
* Otherwise set the Fault/Code/Value to env:Sender and the Fault/Code/Subcode/Value
* as the fault code from the WSSecurityException.
*/
private SoapFault createSoapFault(SoapVersion version, WSSecurityException e) {
SoapFault fault;
javax.xml.namespace.QName faultCode = e.getFaultCode();
if (version.getVersion() == 1.1 && faultCode != null) {
fault = new SoapFault(e.getMessage(), e, faultCode);
} else {
fault = new SoapFault(e.getMessage(), e, version.getSender());
if (version.getVersion() != 1.1 && faultCode != null) {
fault.setSubCode(faultCode);
}
}
return fault;
}
}