Sunday, June 23, 2013

Integrating JSF web application with Openam using Spring Saml Extension.

  Integrating JSF web application with Openam using Spring Saml Extension.
Or Single Sign on With JSF
Or Java Single Sign on
Or Integrate Single sign on Using Spring SAML Extension.
Or Integrating Spring SAML Extension with Openam  (or any SAML provider).
Or Spring and Open AM security
Or Single sign on in JSF or Single Sign on with JSF
Or Spring security with openam

As discussed in my earlier article we can have an different set ups
Case1: SP (Your Java Web app) <===> IDP Proxy   <=====>  (IDP1, IDP2.....)
or if you just have one IDP then obviously there is no point in using IDP proxy in which case you setup will be like
Case2: SP (Your Java Web app or Openam generated Fedlet)  <=====>  (IDP)

So depending on CASE1 or CASE2 you will generate Fedlet on IDP Proxy or IDP respectively.

So What is Fedlet ?
- Well Fedlet is small web based openam client that can  be generated once you install openam. This web application will have few jsps and will help you to test your openam setup and it has enough code to send SAML requests and receive SAML Responses

Now lets get in to details as how to plugin in your JSF webapp with Openam ?
1. Lets say you have JSF webapp with a simple page with following url
http://abc.com:8080/TST/landingPage.jsf

STEP1: Login to openam  (IDP Proxy if its CASE1  else IDP if its CASE2) and click on generate the Fedlet. When you generate the Fedlet choose your
  • Realm (You can use root "/" realm, but better use different realm. I used realm001)
  • Circle of Trust : Let say "Cot1"
  • Identityprovider:  (The url of your openam Identity provider will appear in drop down)
  • Fedlet Name :http://abc.com:8080/TST (Can be anything but I just use url)
  •  Fedlet destination url : http://abc.com:8080/TST
Once this is done you will see an Openam screen which shows where the fedlet.war is stored.

STEP2: Now in fedlet war there is a "conf" directory with 4 xmls
  •   sp.xml
  •   sp-extended.xml
  •   idp.xml
  •   idp-extended.xml
I have noticed that sp.xml that is generated by default in fedlet is missing signing and encryption public key.So I copied the public key for signing and encryption from idp.xml. Assuming the same keys that are used by openam will be used by your java web application as well.
i.e

Copy content from idp.xml starting from  to sp.xml.
 <KeyDescriptor use="signing">
……..
</KeyDescriptor>
<KeyDescriptor use="encryption">
…….
</KeyDescriptor>

STEP3: We can use the idp.xml that’s found in Fedlet/conf directory. This is the xml that will allow our JSF application to send SAML request to Openam.

STEP4:. We can use the sp.xml that’s found in the Fedlet/conf directory. This will have information for service provider (TST JSF application ).There are few changes we need to make here in order for the response url’s to be intercepted by Spring SAML extension filters.
e.g. I generated Fedlet for entity id http://abc.com:8080/TST and here is how the changes look in sp.xml.

Comment the following in sp.xml
FROM:
<!--   Default Fedlet generated Log out Urls.
<SingleLogoutService
       Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
       Location="http://abc.com:8080/TST/fedletSloRedirect"
       ResponseLocation="http://abc.com:8080/TST/fedletSloRedirect" />
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
       Location="http://abc.com:8080/TST/fedletSloPOST"
       ResponseLocation="http://abc.com:8080/TST/fedletSloPOST" />
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
       Location="http://abc.com:8080/TST/fedletSloSoap" />
-->

 Change the above Single logout service url’s to as shown below (Basically in every URL we added "/saml/SingleLogout/alias/realm001/sp" )  where realm001 is the realm and “/sp” is the service provider url we choose when we created openam.Be extra careful when replacing this url to have correct realm name.
In case you have used root realm then you can replace the below urls with /saml/SingleLogout/alias/sp

TO: (Within sp.xml)
 <SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="http://abc.com:8080/TST/saml/SingleLogout/alias/realm001/sp"
ResponseLocation="http://abc.com:8080/TST/saml/SingleLogout/alias/realm001/sp" />
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://abc.com:8080/TST/saml/SingleLogout/alias/realm001/sp"
ResponseLocation="http://abc.com:8080/TST/saml/SingleLogout/alias/realm001/sp" />
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="http://abc.com:8080/TST/saml/SingleLogout/alias/realm001/sp" />
                       
STEP5:  Change AssertionConsumerService (ACS) URL’S in sp.xml
FROM:
<!-- Default Fedlet generated
<AssertionConsumerService isDefault="true"
      index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
      Location="http://abc.com:8080/TST/fedletapplication" />
<AssertionConsumerService index="1"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
Location="http://abc.com:8080/TST/fedletapplication" />
-->  
TO:
<AssertionConsumerService isDefault="true"
index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://abc.com:8080/TST/saml/SSO/alias/realm001/sp" />
<AssertionConsumerService index="1"
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
Location="http://abc.com:8080/TST/saml/SSO/alias/realm001/sp" />

Here the realm I used for testing was "realm001" in case you have used root realm then your url will be  saml/SSO/alias/sp

STEP6:


Please comment the following name id formats in sp.xml and leave transient name id format.

<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>

<!-- We don't need this as we use  just transient
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName</NameIDFormat>
 -->
 
STEP7: In case you want your requests signed and assertions signed in sp.xml set

AuthnRequestsSigned="true" WantAssertionsSigned="true"


I had authentication turned and use a sample cert. For testing you can use the test cert provided by openam . Never use this "test" cert in a production environment.

STEP8:  Edit the sp-extended.xml by fixing metalias to include realm name and also verify the following atttiburtes


These 3 tags will have values only if you have openam proxy. If you have just openam IDP and your SP is talking to IDP directly do no set values for these tags.

<Attribute name="enableIDPProxy">
                <Value>true</Value>
</Attribute>
<Attribute name="idpProxyCount">
                <Value>1</Value></Attribute>
<Attribute name="idpProxyList">
    <Value>http://idp.abc.com:8080/openam</Value> //Strangely you use the IDP url though the tag says proxy
</Attribute>


//Set the cert names for signing and encryption.
<Attribute name="signingCertAlias">
                <Value>test</Value> //In production you can use separate cert for signing and encryption
</Attribute>
<Attribute name="encryptionCertAlias">
                <Value>test</Value> //In production you can use separate cert for signing and encryption
</Attribute>

Change All want*Encrypted properties (Only if you want everything encrypted. I did it when I tested)

 <Attribute name="wantAttributeEncrypted">
   <Value>true</Value>
  </Attribute>
  <Attribute name="wantAssertionEncrypted">
   <Value>true</Value>
  </Attribute>
  <Attribute name="wantNameIDEncrypted">
   <Value>true</Value>
  </Attribute>
 <Attribute name="wantAssertionEncrypted">
    <Value>true</Value>
</Attribute>

 All want*Signed properties (Only if you want everything Signed. I did it when I tested)

<Attribute name="wantPOSTResponseSigned">
          <Value>true</Value>
       </Attribute>
       <Attribute name="wantArtifactResponseSigned">
           <Value>true</Value>
       </Attribute>
       <Attribute name="wantLogoutRequestSigned">
           <Value>true</Value>
       </Attribute>
       <Attribute name="wantLogoutResponseSigned">
           <Value>true</Value>
       </Attribute>
       <Attribute name="wantMNIRequestSigned">
           <Value>true</Value>
       </Attribute>
       <Attribute name="wantMNIResponseSigned">
           <Value>true</Value>
 </Attribute>
In sp-extended.xml add realm name (In case you are using root you dont need to change anything just use the existing values)
metaAlias="/realm001/sp"
metaAlias="/realm001/attrQuery"
metaAlias="/realm001/pep"

In idp-extended.xml add realm name
metaAlias="/realm001/idp" 
metaAlias="/realm001/sp"

In sp-extended.xml set
 hosted="0"  (This tells openam that the entity you are trying  to upload (i.e. your SP) is remote and not locally hosted on Openam itself)

STEP 9:
Since we changed assertion consumer url in the sp.xml we need to update this url in IDP proxy, as IDP proxy will have Feldet  url which we have changed to keep Spring SAML extension happy.
1.   Go to  IDP Proxy (ie. http://idp.abc.com:8080/openam/UI/Login in my case)
2.   Navigate to  Federation > entity providers.
3.   Delete the service provider . ie. Entry which we added to generate Fedlet for our JSF app URL  (eg: http://abc.com:8080/TST)
4.   Make sure the same entity is also removed in Circle of Trust
5.   Click on import entity and select  sp.xml and sp-extended.xml respectively



STEP10:
Now in openam IDP (If you ar using proxy go to proxy) Under Federation > entity providers > your SP (ie. http://abc.com:8080/TST) and you should see signing and encryption with value of the certs.

NOTE:This note is applicable only if you are using openam as a proxy. IGNORE THIS is you are using openam as IDP.
Login in to Openam Proxy >Go to Federation  >  Entity Providers  (http://abc.com:8080/TST) -- Basically the remote Service provider )>  SP> Advanced >  IDP Proxy and make sure proxy count is set to 1 (i.e number one) and proxy url  is set to your IDP url (yes the proxy url is idp url).The proxy uses this value to send request to IDP.

STEP11: 
Verify that the acs url you have added in sp.xml is present in openam.
Login to IDP ( if you are using proxy login into openam proxy)> Click on Federation tab
Go to Entity providers section and click on the URL that we used to generate Fedlet (In this case we used http://abc.com:8080/TST )
While on SP tab go to “Services” screen scroll down to the section which has“Assertion consumer Service”
From: http://abc.com:8080/TST/fedletapplication
To: http://abc.com:8080/TST/saml/SSO/alias/realm001/sp

NOTE: I did not use single logout service. In case you want change the logout URL’s on this page. (Locally I changed all logout urls to “TST/saml/SingleLogout/alias/realm001/sp”)

Congratulations !!!! Your configuration to plugin SP with openam is complete.
We now need to focus on Spring code changes on you JSF web app.


 Spring SAML extension integration with your JSF application
or spring security saml tutorial
or spring security saml tutorial
or spring security jsf example

Here I will brief you about how to plugin spring code with your JSF application.

Please download the sample web application provided by Vladmir Schaufer and explore a bit


You should be able to download "spring-security-saml-1.0.0.RC2-dist.zip" or which ever is the most recent one. Use maven and do a build and you will get a deploy-able war file 


To run this example please read the java doc provided by the author 


Now in the zip file spring-security-saml-1.0.0.RC2\sample\src\main\resources\security\securityContext.xml
Let’s use this sample spring security xml file as a starting point for integrating our application.
If you read the documentation provided by Vladmir Schaufer you will now be familiar with the Spring security xml. However to make it work for our web app I really had to comment quite a few things.

So here is the Original Spring Security xml file (original-securityContext.xml)

Here is the modified file (rams-applicationContext-security.xml).This file has comments in each section indicating why I commented a xml entry. You can download the modified xml file from below url
 
I always had a question as why spring SAML uses the filters with urls patterns predefined.
  <security:filter-chain pattern="/saml/login/**" filters="samlEntryPoint"/>
  <security:filter-chain pattern="/saml/logout/**" filters="samlLogoutFilter"/>
  //We dont need metadata to be displayed comment this.

<!-- <security:filter-chain pattern="/saml/metadata/**" filters="metadataDisplayFilter"/> -->
  <security:filter-chain pattern="/saml/SSO/**" filters="samlWebSSOProcessingFilter"/>
  <security:filter-chain pattern="/saml/SSOHoK/**" filters="samlWebSSOHoKProcessingFilter"/>
  <security:filter-chain pattern="/saml/SingleLogout/**" filters="samlLogoutProcessingFilter"/>
  //We dont need Springdiscovery as we have one idp.So commented it.

<!--  <security:filter-chain pattern="/saml/discovery/**" filters="samlIDPDiscovery"/> -->

This is how the spring filters will intercepts the urls.

Don't mess with this patterns.Thats the reason we changed the ACS and logout urls as mentioned in  STEP5.
Once you have this rams-applicationContext-security.xml all it needs is the below files in your path or classpath. (See the rams-applicationContext-security.xml)
  • sp.xml
  • idp.xml
  • java keystore with name keystore.jks  (I copied from openam for testing)- I will add one article on  keystores later.
  • Your openam IDP URL  (I used url as http://idp.abc.com:8080/openam 
NOTE: In case you are using proxy there is one section which says "ProxyCount" which you need to address. If you read the comment in the security xml you will know what to do.


Ok now if you see my modified spring xml the only code you need to write is the class com.tst.web.security.SAMLUserDetailsServiceImpl.java 
which is implementation for org.springframework.security.saml.userdetails.SAMLUserDetailsService
I will add the sample code here. I had integrated openam with Active directory and had configured openam to retrun the following attributes.
"ActiveDirGroups" and  "UserName" 
So when Openam sent the SAML Response it was sending these attributes. So we need to parse and build UserDetails object as below.

package com.tst.web.security;
package com.tst.web.security; import java.net.URL; import java.util.ArrayList; import java.util.List; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.apache.log4j.xml.DOMConfigurator; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.saml.SAMLCredential; import org.springframework.security.saml.userdetails.SAMLUserDetailsService; /** * @author twreddy The SAMLUserDetailsService interface is similar to * UserDetailsService with difference that SAML data is used in order * obtain information about the user. So inspect the SAMLCredential * object and return such a data in a form of application specific * UserDetails object */ public class SAMLUserDetailsServiceImpl implements SAMLUserDetailsService { // SAML Response PAY load attributes. // Some of the attributes we get back in SAML RESPONSE // We mapped this in openam so it should be there in SAML response. public static final String GROUP_MEMBER_ATTR_NAME = "ActiveDirGroups"; public static final String USER_ID = "UserName"; private static Logger logger = LogManager.getLogger(FMSAMLUserDetailsService.class); /* * This is the method spring will invoke. */ public Object loadUserBySAML(SAMLCredential credential) throws UsernameNotFoundException { logger = getLOSLogger(this); // Want to see if this object is null ... logger.info("credential=" + credential); if (credential != null) { String samlCredentilAsStr = ""; if (logger.isDebugEnabled()) { // I dont want read this object every time. // Will do only of we have enabled debugging. samlCredentilAsStr = ToStringBuilder.reflectionToString(credential); logger.debug("samlCredentilAsStr=" + samlCredentilAsStr); } String userId = getUserId(credential); logger.debug("userId=" + userId); UserDetails userDetails = getUserWithRoles(credential, userId); return userDetails; } throw new UsernameNotFoundException( "SAMLCredential is null. Cant extract " + GROUP_MEMBER_ATTR_NAME + " and or " + USER_ID + " in Saml Response. " ); } /** * Extracts all groups and creates a UserDetail object * and returns it. * @param credential * @param userId * @return */ private UserDetails getUserWithRoles(SAMLCredential credential, String userId) { UserDetails userDetails = null; // We dont have access to password ?? // So using name as password for now. List<SimpleGrantedAuthority> grantedRolesList = getGrantedAuthorities( GROUP_MEMBER_ATTR_NAME, credential); logger.info(" roles For :" + userId + " grantedRolesList=" + grantedRolesList); // This object needs userid and password. I dont have password // Faking it with user id. Password may really be needed when // you try to Authenticate this object using authManager. userDetails = new User(userId, userId, grantedRolesList); return userDetails; } /** * Reads the Attributes that are in SAML Pay load response. Our Group Meber * ship Is a long String: * CN=FILE_EDIT_USER,OU=Devtest,DC= We need to get only Group name which FILE_EDIT_USER * * @param name * @param credential * @return */ private List<SimpleGrantedAuthority> getGrantedAuthorities( final String name, final SAMLCredential credential) { List<SimpleGrantedAuthority> grantedRolesList = new ArrayList<SimpleGrantedAuthority>(); List<String> attrValueList = getAttributeValue(name,credential); for (String attrValue : attrValueList) { if (attrValue != null) { // All we are doing here is extracting the first part in the // group membership // eg: // CN=FILE_EDIT_USER,OU=Devtest,DC= We need to get only Group name which FILE_EDIT_USER logger.debug(" Raw Attribute=" + attrValue); String roleName = attrValue.split(",")[0].split("=")[1]; // We need just "FILE_EDIT_USER" logger.debug(" Cleaned up group name=" + roleName); logger.debug(" group name with case changed=" + roleName); SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority( roleName); grantedRolesList.add(grantedAuthority); } else { logger.warn("One of the Group attrValue was null."); } } return grantedRolesList; } /** * Given SAMLCredential inspects the object and returns the values * for give attribute name. * @param name * @param credential * @return */ public static List<String> getAttributeValue(final String name, final SAMLCredential credential) { List<String> attrValList = new ArrayList<String>(); Attribute attribute = credential.getAttributeByName(name); logger.debug(" Parsing name=" + name + " in SAMLCredential"); if (attribute != null) { List<XMLObject> attributes = attribute.getAttributeValues(); if ((attributes != null) && (attributes.size() > 0)) { for (XMLObject object : attributes) { XSString attrb = (XSString) object; String attrValue = attrb.getValue(); if (attrValue != null) { // clean unwanted strings here in the role logger.debug("name=" + name + " attrValue=" + attrValue); attrValList.add(attrValue); } } } } return attrValList; } /** * Reads the uid from SAML credential if present * @param credential * @return */ public static String getUserId(SAMLCredential credential) { String userId = null; List<String> userIdValueList = getAttributeValue(USER_ID, credential); logger.debug(" USER_ID="+ USER_ID +" userIdValueList=" + userIdValueList); if (userIdValueList.size() > 0) { userId = userIdValueList.get(0); } return userId; } }




So now you have spring security xml and the code to parse SAML repsonse ready. Just plugin. The srping xml in to your web.xml. Since the code above parsed the roles the user is we need to use those roles to enable or disable feature sin JSF pages which I will explain shortly.

In your webxml 
I have attached the sample web.xml
https://sites.google.com/site/reddymails/Home/web.xml?attredirects=0&d=1

    <listener-class>
        org.springframework.web.context.ContextLoaderListener
    </listener-class>
    </listener> 
    <context-param>
       <param-name>contextConfigLocation</param-name>
 <param-value>
  /WEB-INF/rams-applicationContext-security.xml
        </param-value>
      </context-param>
    <!--  Spring Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
 <filter-name>springSecurityFilterChain</filter-name>
 <url-pattern>/*</url-pattern>
</filter-mapping>



How do to we read the User name from Spring security context ? 
I did some reading and here is code that saves you a day :)
The assumption is that your are sending "Username" as one of the attributes in SAML response.
Once you parse store it in Httpsession that way you don't run this code multiple times for same user.

public static final String USER_ID = "UserName";

  public String getCurentUserName(){
// Read from Security context as SAML Pay loads gets  your name.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SAMLCredential samlCredential = (SAMLCredential) authentication.getCredentials();
String userName = getUserId(samlCredential);
return userName;
}

 /**
 * Reads the uid from SAML credential if present
 * @param credential
 * @return
 */
public static String getUserId(SAMLCredential credential) {
  String userId = null;
List<String> userIdValueList = getAttributeValue(USER_ID, credential);
logger.debug(" USER_ID="+ USER_ID +" userIdValueList=" + userIdValueList);
 if (userIdValueList.size() > 0) {
  userId = userIdValueList.get(0);
 }
 return userId;
}

With this you are done with single sign on. Howevere If you want to use Spring Security you can read th below few lines
Once you have all this code you can plugin spring role based security if you need.

Just follow these steps
<!--  To add Security tag tld to  JSF you can read more at.
more at 
http://static.springsource.org/spring-webflow/docs/2.2.x/reference/html/ch13s09.html
  http://doanduyhai.wordpress.com/2012/02/26/spring-security-part-v-security-tags/
-->

Assuuming you have configured the Spring security tld now you can add name space in JSF page as shown below.
xmlns:security="http://www.springframework.org/security/tags"

And inside your JSF page now you can use tags like 

rendered="#{security:areAnyGranted('FILE_READ_GROUP,FILE_EDIT_GROUP')}">
rendered="#security:areAllGranted('FILE_ALL_OPERATIONS_GROUP)"
disabled="#{security:areNotGranted('FILE_READ_GROUP)}">

Where FILE_READ_GROUP, FILE_ALL_OPERATIONS are Active directory groups the current user is in.

Hope this article take some pain of integrating openam with Spring SAML and also helps you in Spring Security integration with JSF application.
 
NOTE:If your web application is .net based you can try using .net fedelt
 provided by openam.I don't have much knowledge to comment on .net.
If you like my articles or have some suggestion feel free to drop a message.