Wednesday, May 7, 2014

Creating and Validating SAML assertions with Spring-WS's Wss4jSecurityInterceptor

Full SAML 2.0 support for both the server and client side was recently added to the Wss4jSecurityInterceptor in the 2.2.0.RC1 version of Spring-WS. The following is a simple example demonstrating this new functionality.

Spring-WS is a framework for implementing SOAP services using a "contract first" model. You can read more about the project and the reason behind the contract first approach here http://docs.spring.io/spring-ws/site/reference/html/why-contract-first.html. In order to secure your SOAP services Spring-WS supports using the WS-Security extension to the SOAP standard. The WS-Security standard includes several ways to convey authentication and authorization information in your SOAP messages, one of which is using SAML assertions.

I am going to assume if you are reading this that you know how to set up a basic Spring-WS project, but if not you can look at the full code for my example here https://github.com/jaminh/spring-saml-example-war, or one of the numerous other tutorials (http://docs.spring.io/spring-ws/site/reference/html/tutorial.html).

We will start by going over configuring the Wss4jSecurityInterceptor for validating SAML tokens on the server side.

<sws:interceptors>  
    <bean id="securityInterceptor" class="org.springframework.ws.soap.security.wss4j.Wss4jSecurityInterceptor">  
     <property name="validationActions" value="Timestamp SAMLTokenSigned Signature"/>  
     <property name="validationSignatureCrypto" ref="crypto"/>  
    </bean>  
</sws:interceptors>  
<bean id="crypto" class="org.springframework.ws.soap.security.wss4j.support.CryptoFactoryBean">  
   <property name="keyStorePassword" value="password"/>  
   <property name="defaultX509Alias" value="selfsigned"/>  
   <property name="keyStoreLocation" value="classpath:/keystore.jks"/>  
</bean>  

The validation actions are set to "Timestamp" "SAMLTokenSigned" and "Signature". The timestamp is not really necessary for validating SAML assertions but it is generally a good idea to include in your message security anyway.

Adding "SAMLTokenSigned" requires that a SAML token is included in the Security header and the SAML token itself must be signed. Signing allows the receiver of the message to verify that the SAML token came from an entity they can trust. You could also use "SAMLTokenUnsigned" if you don't need that extra level of verification.

The "Signature" option may or may not be required depending on which strategy you use for subject confirmation. SAML has three methods for subject confirmation: Holder-of-Key, Sender-vouches, and Bearer. In the Holder-of-Key method, which is used in this example, the issuer of the SAML assertion includes the subjects public key. In order to prove that the message sender is the subject specified in the assertion the sender must sign the message with the corresponding private key. This makes it so that even if someone else gets the SAML assertion they can not send messages with it. In order to process the signature used for Holder-of-Key the "Signature" option must be added.

In order to verify the signed SAML assertion the interceptor must include a "Crypto" which contains the list of trusted certificates. Spring-WS provides a factory for creating the Crypto from a JKS keystore. In this example I am just using a self signed certificate. You can find instructions for generating JKS keystores here http://www.sslshopper.com/article-how-to-create-a-self-signed-certificate-using-java-keytool.html.

Next we will go over configuring the client to send SAML assertions.

The first thing you will have to do is create an CallbackHandler for generating the SAML token. The following is based off such a CallbackHandler used by the wss4j tests.

http://svn.apache.org/repos/asf/webservices/wss4j/branches/1_6_x-fixes/src/test/java/org/apache/ws/security/common/SAML2CallbackHandler.java

1:  /**  
2:  * Licensed to the Apache Software Foundation (ASF) under one  
3:  * or more contributor license agreements. See the NOTICE file  
4:  * distributed with this work for additional information  
5:  * regarding copyright ownership. The ASF licenses this file  
6:  * to you under the Apache License, Version 2.0 (the  
7:  * "License"); you may not use this file except in compliance  
8:  * with the License. You may obtain a copy of the License at  
9:  *  
10:  * http://www.apache.org/licenses/LICENSE-2.0  
11:  *  
12:  * Unless required by applicable law or agreed to in writing,  
13:  * software distributed under the License is distributed on an  
14:  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY  
15:  * KIND, either express or implied. See the License for the  
16:  * specific language governing permissions and limitations  
17:  * under the License.  
18:  */  
19:    
20:  package org.apache.ws.security.common;  
21:    
22:  import java.io.IOException;  
23:    
24:  import javax.security.auth.callback.Callback;  
25:  import javax.security.auth.callback.UnsupportedCallbackException;  
26:    
27:  import org.apache.ws.security.components.crypto.Crypto;  
28:  import org.apache.ws.security.components.crypto.CryptoType;  
29:  import org.apache.ws.security.saml.ext.SAMLCallback;  
30:  import org.apache.ws.security.saml.ext.bean.KeyInfoBean;  
31:  import org.apache.ws.security.saml.ext.bean.SubjectBean;  
32:  import org.apache.ws.security.saml.ext.builder.SAML2Constants;  
33:  import org.opensaml.common.SAMLVersion;  
34:    
35:  /**  
36:  * A Callback Handler implementation for a SAML 2 assertion. By default it creates an  
37:  * authentication assertion using Sender Vouches.  
38:  */  
39:  public class SAML2CallbackHandler extends AbstractSAMLCallbackHandler {  
40:      
41:    public SAML2CallbackHandler(Crypto crypto, String alias) throws Exception {  
42:      if (certs == null) {  
43:        CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);  
44:        cryptoType.setAlias(alias);  
45:        certs = crypto.getX509Certificates(cryptoType);  
46:      }  
47:        
48:      subjectName = "uid=joe,ou=people,ou=saml-demo,o=example.com";  
49:      subjectQualifier = "www.example.com";  
50:      confirmationMethod = SAML2Constants.CONF_HOLDER_KEY;  
51:    }  
52:      
53:    public void handle(Callback[] callbacks)  
54:      throws IOException, UnsupportedCallbackException {  
55:      for (int i = 0; i < callbacks.length; i++) {  
56:        if (callbacks[i] instanceof SAMLCallback) {  
57:          SAMLCallback callback = (SAMLCallback) callbacks[i];  
58:          callback.setSamlVersion(SAMLVersion.VERSION_20);  
59:          callback.setIssuer(issuer);  
60:          SubjectBean subjectBean =  
61:            new SubjectBean(  
62:              subjectName, subjectQualifier, confirmationMethod  
63:            );  
64:          if (SAML2Constants.CONF_HOLDER_KEY.equals(confirmationMethod)) {  
65:            try {  
66:              KeyInfoBean keyInfo = createKeyInfo();  
67:              subjectBean.setKeyInfo(keyInfo);  
68:            } catch (Exception ex) {  
69:              throw new IOException("Problem creating KeyInfo: " + ex.getMessage());  
70:            }  
71:          }  
72:          callback.setSubject(subjectBean);  
73:          createAndSetStatement(null, callback);  
74:        } else {  
75:          throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");  
76:        }  
77:      }  
78:    }  
79:      
80:  }  

http://svn.apache.org/repos/asf/webservices/wss4j/branches/1_6_x-fixes/src/test/java/org/apache/ws/security/common/AbstractSAMLCallbackHandler.java

1:  /**  
2:  * Licensed to the Apache Software Foundation (ASF) under one  
3:  * or more contributor license agreements. See the NOTICE file  
4:  * distributed with this work for additional information  
5:  * regarding copyright ownership. The ASF licenses this file  
6:  * to you under the Apache License, Version 2.0 (the  
7:  * "License"); you may not use this file except in compliance  
8:  * with the License. You may obtain a copy of the License at  
9:  *  
10:  * http://www.apache.org/licenses/LICENSE-2.0  
11:  *  
12:  * Unless required by applicable law or agreed to in writing,  
13:  * software distributed under the License is distributed on an  
14:  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY  
15:  * KIND, either express or implied. See the License for the  
16:  * specific language governing permissions and limitations  
17:  * under the License.  
18:  */  
19:    
20:  package org.apache.ws.security.common;  
21:    
22:  import org.apache.ws.security.WSConstants;  
23:  import org.apache.ws.security.message.WSSecEncryptedKey;  
24:  import org.apache.ws.security.saml.ext.SAMLCallback;  
25:  import org.apache.ws.security.saml.ext.bean.ActionBean;  
26:  import org.apache.ws.security.saml.ext.bean.AttributeBean;  
27:  import org.apache.ws.security.saml.ext.bean.AttributeStatementBean;  
28:  import org.apache.ws.security.saml.ext.bean.AuthenticationStatementBean;  
29:  import org.apache.ws.security.saml.ext.bean.AuthDecisionStatementBean;  
30:  import org.apache.ws.security.saml.ext.bean.KeyInfoBean;  
31:  import org.apache.ws.security.saml.ext.bean.SubjectBean;  
32:  import org.apache.ws.security.saml.ext.bean.KeyInfoBean.CERT_IDENTIFIER;  
33:  import org.w3c.dom.Document;  
34:  import org.w3c.dom.Element;  
35:    
36:  import javax.security.auth.callback.CallbackHandler;  
37:  import javax.xml.parsers.DocumentBuilder;  
38:  import javax.xml.parsers.DocumentBuilderFactory;  
39:    
40:  import java.security.cert.X509Certificate;  
41:  import java.util.Collections;  
42:    
43:  /**  
44:  * A base implementation of a Callback Handler for a SAML assertion. By default it creates an  
45:  * authentication assertion.  
46:  */  
47:  public abstract class AbstractSAMLCallbackHandler implements CallbackHandler {  
48:      
49:    public enum Statement {  
50:      AUTHN, ATTR, AUTHZ  
51:    };  
52:      
53:    protected String subjectName = null;  
54:    protected String subjectQualifier = null;  
55:    protected String confirmationMethod = null;  
56:    protected X509Certificate[] certs;  
57:    protected Statement statement = Statement.AUTHN;  
58:    protected CERT_IDENTIFIER certIdentifier = CERT_IDENTIFIER.X509_CERT;  
59:    protected byte[] ephemeralKey = null;  
60:    protected String issuer = null;  
61:      
62:    public void setConfirmationMethod(String confMethod) {  
63:      confirmationMethod = confMethod;  
64:    }  
65:      
66:    public void setStatement(Statement statement) {  
67:      this.statement = statement;  
68:    }  
69:      
70:    public void setCertIdentifier(CERT_IDENTIFIER certIdentifier) {  
71:      this.certIdentifier = certIdentifier;  
72:    }  
73:      
74:    public void setCerts(X509Certificate[] certs) {  
75:      this.certs = certs;  
76:    }  
77:      
78:    public byte[] getEphemeralKey() {  
79:      return ephemeralKey;  
80:    }  
81:      
82:    public void setIssuer(String issuer) {  
83:      this.issuer = issuer;  
84:    }  
85:      
86:    /**  
87:  * Note that the SubjectBean parameter should be null for SAML2.0  
88:  */  
89:    protected void createAndSetStatement(SubjectBean subjectBean, SAMLCallback callback) {  
90:      if (statement == Statement.AUTHN) {  
91:        AuthenticationStatementBean authBean = new AuthenticationStatementBean();  
92:        if (subjectBean != null) {  
93:          authBean.setSubject(subjectBean);  
94:        }  
95:        authBean.setAuthenticationMethod("Password");  
96:        callback.setAuthenticationStatementData(Collections.singletonList(authBean));  
97:      } else if (statement == Statement.ATTR) {  
98:        AttributeStatementBean attrBean = new AttributeStatementBean();  
99:        if (subjectBean != null) {  
100:          attrBean.setSubject(subjectBean);  
101:        }  
102:        AttributeBean attributeBean = new AttributeBean();  
103:        attributeBean.setSimpleName("role");  
104:        attributeBean.setAttributeValues(Collections.singletonList("user"));  
105:        attrBean.setSamlAttributes(Collections.singletonList(attributeBean));  
106:        callback.setAttributeStatementData(Collections.singletonList(attrBean));  
107:      } else {  
108:        AuthDecisionStatementBean authzBean = new AuthDecisionStatementBean();  
109:        if (subjectBean != null) {  
110:          authzBean.setSubject(subjectBean);  
111:        }  
112:        ActionBean actionBean = new ActionBean();  
113:        actionBean.setContents("Read");  
114:        authzBean.setActions(Collections.singletonList(actionBean));  
115:        authzBean.setResource("endpoint");  
116:        authzBean.setDecision(AuthDecisionStatementBean.Decision.PERMIT);  
117:        callback.setAuthDecisionStatementData(Collections.singletonList(authzBean));  
118:      }  
119:    }  
120:      
121:    protected KeyInfoBean createKeyInfo() throws Exception {  
122:      KeyInfoBean keyInfo = new KeyInfoBean();  
123:      if (statement == Statement.AUTHN) {  
124:        keyInfo.setCertificate(certs[0]);  
125:        keyInfo.setCertIdentifer(certIdentifier);  
126:      } else if (statement == Statement.ATTR) {  
127:        // Build a new Document  
128:        DocumentBuilderFactory docBuilderFactory =  
129:          DocumentBuilderFactory.newInstance();  
130:        docBuilderFactory.setNamespaceAware(true);  
131:        DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();  
132:        Document doc = docBuilder.newDocument();  
133:             
134:        // Create an Encrypted Key  
135:        WSSecEncryptedKey encrKey = new WSSecEncryptedKey();  
136:        encrKey.setKeyIdentifierType(WSConstants.X509_KEY_IDENTIFIER);  
137:        encrKey.setUseThisCert(certs[0]);  
138:        encrKey.prepare(doc, null);  
139:        ephemeralKey = encrKey.getEphemeralKey();  
140:        Element encryptedKeyElement = encrKey.getEncryptedKeyElement();  
141:          
142:        // Append the EncryptedKey to a KeyInfo element  
143:        Element keyInfoElement =  
144:          doc.createElementNS(  
145:            WSConstants.SIG_NS, WSConstants.SIG_PREFIX + ":" + WSConstants.KEYINFO_LN  
146:          );  
147:        keyInfoElement.setAttributeNS(  
148:          WSConstants.XMLNS_NS, "xmlns:" + WSConstants.SIG_PREFIX, WSConstants.SIG_NS  
149:        );  
150:        keyInfoElement.appendChild(encryptedKeyElement);  
151:          
152:        keyInfo.setElement(keyInfoElement);  
153:      }  
154:      return keyInfo;  
155:    }  
156:  }  

The AbstractSAMLCallbackHandlerjust deals with the details of creating the XML for the KeyInfo and AuthenticationStatement. The SAML2CallbackHandler set up the subject values used in the assertion and gets access to the client certificate that has to be used to confirm the message signature.

Once you have a SAML CallbackHandler you can create the Wss4jSecurityInterceptor for the client.

 CryptoFactoryBean cryptoFactory = new CryptoFactoryBean();  
 cryptoFactory.setKeyStoreLocation(new ClassPathResource("keystore.jks"));  
 cryptoFactory.setKeyStorePassword("password");  
 cryptoFactory.afterPropertiesSet();  
 Crypto crypto = cryptoFactory.getObject();

 SAML2CallbackHandler samlCallbackHandler = new SAML2CallbackHandler(crypto, "selfsigned");  
 SAMLIssuerImpl issuer = new SAMLIssuerImpl();  
 issuer.setIssuerCrypto(crypto);  
 issuer.setIssuerKeyName("selfsigned");  
 issuer.setIssuerKeyPassword("password");  
 issuer.setIssuerName("selfsigned");  
 issuer.setSendKeyValue(false);  
 issuer.setSignAssertion(true);  
 issuer.setCallbackHandler(samlCallbackHandler);

 Wss4jSecurityInterceptor securityInterceptor = new Wss4jSecurityInterceptor();  
 securityInterceptor.setSecurementActions("Timestamp SAMLTokenSigned");  
 securityInterceptor.setSecurementSignatureCrypto(crypto);  
 securityInterceptor.setSecurementUsername("selfsigned");  
 securityInterceptor.setSecurementPassword("password");  
 securityInterceptor.setSamlIssuer(issuer);  

First we need to create the Crypto from our JKS keystore for both signing the SAML assertion and signing the message for the Holder-of-Key subject confirmation. Next the SAML2CallbackHandler is created to pass to a SAMLIssuer. The SAMLIssuer takes care of generating the whole SAML assertion and signing it if told to do so. Finally you can create the Wss4jSecurityInterceptor. You will notice for the client you only need to specify "Timestamp" and "SAMLTokenSigned" for the actions. On the client side wss4j is able to figure out that if Holder-of-Key subject confirmation is being used then a signature has to be included so the "Signature" action is not required.

10 comments:

  1. Hi.
    I am new to spring web services and SAML. I am working on implementing SAML in our web service. Is there any code sample which you can share? Like where should the CallBackHandler be defined?

    ReplyDelete
    Replies
    1. You can find an example here: https://github.com/jaminh/spring-saml-example-war. You will want to look in the src/test/java directory specifically for an example on adding SAML for the client. The CallBackHandler is defined there in the example project.

      Delete
  2. Hi Jamin
    Thank you for great sample. Is there a way to set the callback using ApplicationContext.xml instead of java code?

    Thank you

    ReplyDelete
  3. Hi Jamin,

    How can I read the nameId/username element from SAML assertion after the Wss4j interceptor validates the incoming SAML?

    Thanks

    ReplyDelete
  4. You can get the results of the WS-Security processing from the message context http://docs.spring.io/spring-ws/site/apidocs/org/springframework/ws/context/MessageContext.html#getProperty%28java.lang.String%29. The results are stored in a property WSHandlerConstants.RECV_RESULTS http://people.apache.org/~coheigea/stage/wss4j/1.6.4/site/apidocs/org/apache/ws/security/handler/WSHandlerConstants.html#RECV_RESULTS.
    This will get you the WSHandlerResult http://people.apache.org/~coheigea/stage/wss4j/1.6.4/site/apidocs/org/apache/ws/security/handler/WSHandlerResult.html. The SAML assertion result will be tagged with http://people.apache.org/~coheigea/stage/wss4j/1.6.4/site/apidocs/org/apache/ws/security/WSSecurityEngineResult.html#TAG_SAML_ASSERTION.

    ReplyDelete
  5. Hi, is there a tutorial for use .opensaml.saml2 with webservice soap and springBoot application?

    ReplyDelete
  6. I don't have a specific tutorial for SAML with spring boot but it should be pretty straight forward to set up. There is a tutorial for a basic Spring WS using Spring Boot here https://spring.io/guides/gs/producing-web-service/. From there you should be able to configure the Wss4jSecurity interceptor in the WebServiceConfig class and then add it to the list of interceptors similar to how it is being done here http://memorynotfound.com/spring-ws-intercept-request-response-soap-messages/. Hope that helps.

    ReplyDelete
  7. Hi Jamin, can I apply the same logic/code here for Rest web service? I have set up a spring boot Rest web service and now I want to secure it with SAML. Thank you.

    ReplyDelete
    Replies
    1. Most of the code in this example is dealing with adding SAML into a SOAP message specifically. You may want to try looking at the Spring Security SAML project http://projects.spring.io/spring-security-saml/ which is geared towards adding SAML to regular web applications.

      Delete


  8. 0

    down vote

    favorite




    Hi Jamin,
    Can you help me please to validate SAML assertion in server side with soap Spring-WS ?
    you find more détails in the following link:
    http://stackoverflow.com/questions/41490720/spring-ws-soap-saml-assertion-validation-server-side

    ReplyDelete