In this document we show how to use PicketLink STS to validate SAML assertions and authenticate EJB3 clients on Weblogic. It's a slightly modified version of this article.
Software used for this exercise: JDK6, PicketLink 1.0.4-SNAPSHOT, Weblogic Server 10.3.2.0.
PicketLink STS was deployed on JBoss EAP 5.
Process overview
The client must first obtain the SAML assertion from PicketLink STS by sending a WS-Trust request to the token service. This process usually involves authentication of the client. After obtaining the SAML assertion from the STS, the client includes the assertion as a String in the security context of the EJB request before invoking an operation on the bean. This is required because Weblogic Authentication Providers only accept a String credential (the default CallbackHandler doen't handle Objects as credentials).
Upon receiving the invocation, the EJB container extracts the assertion and validates it by sending a WS-Trust validate message to the STS. If the assertion is considered valid by the STS (and the proof of possession token has been verified if needed), the client is authenticated.
On Weblogic, the SAML assertion validation process is handled by the SimpleSAML2STSAuthenticator. This is a custom Weblogic Authentication Provider that reads properties from a configurable file specified in the provider's configuration options and establishes communication with STS based on these properties. If the assertion is valid, a Principal is created using the assertion subject name and associated with the caller's Subject. The authenticator then proceeds to associate any user's groups with the Subject. The user's groups are also configured in a properties file specified in the provider's configuration options.
EJB3 Integration Example
In this section we present a sample EJB3 application that authenticates clients by validating their SAML assertions with PicketLink STS. The deployments for both the EJB3 application and the STS can be found attached in this document.
EJB3 Sample App
Our EJB3 application consists of a simple stateless session bean. The session interface can be seen bellow:
/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.test.security.ejb3; import java.security.Principal; /** * <p> * This is the remote interface of session beans used in the EJB3 security tests. * </p> * * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a> */ public interface SimpleSession { /** * <p> * This is a method available for regular users and administrators. Implementations must annotate either the class or * this method with {@code @RolesAllowed({"RegularUser", "Administrator"})} to enforce that only these roles should * be granted access to this method. * </p> * * @return the caller's {@code Principal}. */ public Principal invokeRegularMethod(); /** * <p> * This is a method available for administrators only. Implementations must annotate either the class or this method * with {@code @RolesAllowed({"Administrator"})} to enforce that only administrators should be granted access to * this method. * </p> * * @return the caller's {@code Principal}. */ public Principal invokeAdministrativeMethod(); /** * <p> * This is a method available for all authenticated users, regardless or role. Implementations must annotate this * method with {@code @PermitAll} to specify that all security roles should be granted access. * </p> * * @return the caller's {@code Principal}. */ public Principal invokeUnprotectedMethod(); /** * <p> * This is a method that is unavailable for everybody. Implementations must annotate this method with * {@code @DenyAll} to specify that access should be restricted for everybody. * </p> * * @return the caller's {@code Principal}. */ public Principal invokeUnavailableMethod(); }
And this is the implementation class:
/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.test.security.ejb3; import java.security.Principal; import javax.annotation.Resource; import javax.annotation.security.DenyAll; import javax.annotation.security.PermitAll; import javax.annotation.security.RolesAllowed; import javax.ejb.Remote; import javax.ejb.SessionContext; import javax.ejb.Stateless; /** * <p> * Stateless session bean implementation used in the EJB3 security tests. * </p> * * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a> */ @Stateless @Remote(SimpleSession.class) @RolesAllowed({"RegularUser", "Administrator"}) public class SimpleStatelessSessionBean implements SimpleSession { @Resource private SessionContext context; /* * (non-Javadoc) * * @see org.jboss.test.security.ejb3.SimpleSession#invokeRegularMethod() */ public Principal invokeRegularMethod() { // this method allows the same roles as the class. return this.context.getCallerPrincipal(); } /* * (non-Javadoc) * * @see org.jboss.test.security.ejb3.SimpleSession#invokerAdministrativeMethod() */ @RolesAllowed({"Administrator"}) public Principal invokeAdministrativeMethod() { // this method overrides the roles defined by the class to grant access to admnistrators only. return this.context.getCallerPrincipal(); } /* * (non-Javadoc) * * @see org.jboss.test.security.ejb3.SimpleSession#invokeUnprotectedMethod() */ @PermitAll public Principal invokeUnprotectedMethod() { // this method overrides the roles defined by the class to grant access to all roles. return this.context.getCallerPrincipal(); } /* * (non-Javadoc) * * @see org.jboss.test.security.ejb3.SimpleSession#invokeUnavailableMethod() */ @DenyAll public Principal invokeUnavailableMethod() { // this method should never be called - it overrides the class roles to deny access to all roles. return this.context.getCallerPrincipal(); } }
The session defines four methods: invokeRegularMethod (available to both Administrators and RegularUsers), invokeAdministrativeMethod (available to Administrators only), invokeUnprotectedMethod (available to all authenticated clients), and invokeUnavailableMethod (annotated with @DenyAll and thus unavailable to all roles).
The ejb3.jar also includes a META-INF/weblogic-ejb-jar.xml deployment descriptor to correctly map the roles without having to rely on the security realm defined in the Weblogic domain:
<?xml version="1.0" encoding="UTF-8"?> <weblogic-ejb-jar xmlns="http://www.bea.com/ns/weblogic/10.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.bea.com/ns/weblogic/10.0 http://www.bea.com/ns/weblogic/10.0/weblogic-ejb-jar.xsd"> <security-role-assignment> <role-name>Administrator</role-name> <principal-name>Administrator</principal-name> </security-role-assignment> <security-role-assignment> <role-name>RegularUser</role-name> <principal-name>RegularUser</principal-name> </security-role-assignment> <security-role-assignment> <role-name>Guest</role-name> <principal-name>Guest</principal-name> </security-role-assignment> </weblogic-ejb-jar>
PicketLink STS
Our PicketLink STS application is a tweaked version of the picketink-sts.war file that is available in the PicketLink project downloads page. More specifically, we created a new security domain for the STS in jboss-web.xml, included an application policy for the new domain that uses the UsersRolesLoginModule to authenticate STS clients, included the users and roles properties files, and changed the required role in web.xml to STSClient.
This is the content of the STS web.xml:
<?xml version="1.0"?> <!DOCTYPE web-app PUBLIC -//Sun Microsystems, Inc.//DTD Web Application 2.3//EN http://java.sun.com/dtd/web-app_2_3.dtd> <web-app> <servlet> <servlet-name>PicketLinkSTS</servlet-name> <servlet-class>org.picketlink.identity.federation.core.wstrust.PicketLinkSTS</servlet-class> </servlet> <servlet-mapping> <servlet-name>PicketLinkSTS</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> <security-constraint> <web-resource-collection> <web-resource-name>TokenService</web-resource-name> <url-pattern>/*</url-pattern> <http-method>GET</http-method> <http-method>POST</http-method> </web-resource-collection> <auth-constraint> <role-name>STSClient</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> <realm-name>PicketLinkSTSRealm</realm-name> </login-config> <security-role> <role-name>STSClient</role-name> </security-role> </web-app>
STS callers must all have the STSClient role in order to send a WS-Trust request to the STS.
The STS security domain is specified by the jboss-web.xml file:
<?xml version="1.0" encoding="UTF-8"?> <jboss-web> <security-domain>java:/jaas/sts-domain</security-domain> </jboss-web>
The application policy for the sts-domain is defined in the sts-jboss-beans.xml file:
<?xml version="1.0" encoding="UTF-8"?> <deployment xmlns="urn:jboss:bean-deployer:2.0"> <!-- ejb3 test application-policy definition --> <application-policy xmlns="urn:jboss:security-beans:1.0" name="sts-domain"> <authentication> <login-module code="org.jboss.security.auth.spi.UsersRolesLoginModule" flag="required"> <module-option name="usersProperties">sts-users.properties</module-option> <module-option name="rolesProperties">sts-roles.properties</module-option> </login-module> </authentication> </application-policy> </deployment>
The sts-users.properties specify the username/passwords of the STS callers:
JBoss=JBoss UserA=PassA UserB=PassB UserC=PassC
The sts-roles.properties specify the roles of the STS callers:
JBoss=STSClient UserA=STSClient UserB=STSClient UserC=STSClient
Notice that the JBoss user represents the JBoss server during the SAML validation process. All other users are the clients of the EJB3 sample application - they send a message to the STS to acquire a SAML assertion before calling the methods on the EJB3 application.
Client Application
The SAML2EJB3WeblogicTest shows what happens when each of the users (UserA, UserB, and UserC) acquire a SAML assertion from PicketLinkSTS and invoke all methods on the sample EJB3. Let's take a look at the code:
/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ import java.security.Principal; import java.util.Properties; import javax.ejb.EJBAccessException; import javax.naming.Context; import javax.naming.InitialContext; import org.jboss.test.security.ejb3.SimpleSession; import org.picketlink.identity.federation.api.wstrust.WSTrustClient; import org.picketlink.identity.federation.api.wstrust.WSTrustClient.SecurityInfo; import org.picketlink.identity.federation.core.wstrust.SamlCredential; import org.picketlink.identity.federation.core.wstrust.WSTrustException; import org.picketlink.identity.federation.core.wstrust.plugins.saml.SAMLUtil; import org.w3c.dom.Element; import weblogic.jndi.WLInitialContextFactory; public class SAML2EJB3WeblogicTest { public static void main(String[] args) throws Exception { System.out.println("\nTesting UserA"); testUserA(); System.out.println("\nTesting UserB"); testUserB(); System.out.println("\nTesting UserC"); testUserC(); } private static SimpleSession test(String username, String password) throws Exception { Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY, WLInitialContextFactory.class.getName()); env.put(Context.PROVIDER_URL, "t3://127.0.0.1:7001"); env.put(Context.SECURITY_PRINCIPAL, username); WSTrustClient client = new WSTrustClient("PicketLinkSTS", "PicketLinkSTSPort", "http://127.0.0.1:8080/picketlink-sts-1.0.0/PicketLinkSTS", new SecurityInfo(username, password)); Element assertion = null; try { assertion = client.issueToken(SAMLUtil.SAML2_TOKEN_TYPE); } catch (WSTrustException wse) { System.out.println("Unable to issue assertion: " + wse.getMessage()); wse.printStackTrace(); System.exit(1); } SamlCredential credential = new SamlCredential(assertion); String pwd = credential.getAssertionAsString(); env.put(Context.SECURITY_CREDENTIALS, pwd); InitialContext ctx = new InitialContext(env); SimpleSession session = (SimpleSession) ctx.lookup("SimpleSession#org.jboss.test.security.ejb3.SimpleSession"); return session; } private static void testUserA() throws Exception { SimpleSession session = test("UserA", "PassA"); Principal p = null; try { p = session.invokeRegularMethod(); System.out.println(p); } catch (Exception e) { e.printStackTrace(); } try { p = session.invokeAdministrativeMethod(); System.out.println(p); } catch (Exception e) { e.printStackTrace(); } try { p = session.invokeUnprotectedMethod(); System.out.println(p); } catch (Exception e) { e.printStackTrace(); } try { p = session.invokeUnavailableMethod(); } catch (EJBAccessException e) { System.out.println("Correctly failed to invoke unavailable method"); } } private static void testUserB() throws Exception { SimpleSession session = test("UserB", "PassB"); Principal p = null; try { p = session.invokeRegularMethod(); System.out.println(p); } catch (Exception e) { e.printStackTrace(); } try { p = session.invokeAdministrativeMethod(); } catch (Exception e) { System.out.println("Correctly failed to invoke administrative method"); } try { p = session.invokeUnprotectedMethod(); System.out.println(p); } catch (Exception e) { e.printStackTrace(); } try { p = session.invokeUnavailableMethod(); } catch (EJBAccessException e) { System.out.println("Correctly failed to invoke unavailable method"); } } private static void testUserC() throws Exception { SimpleSession session = test("UserC", "PassC"); Principal p = null; try { p = session.invokeRegularMethod(); } catch (Exception e) { System.out.println("Correctly failed to invoke regular method"); } try { p = session.invokeAdministrativeMethod(); } catch (Exception e) { System.out.println("Correctly failed to invoke administrative method"); } try { p = session.invokeUnprotectedMethod(); System.out.println(p); } catch (Exception e) { e.printStackTrace(); } try { p = session.invokeUnavailableMethod(); } catch (EJBAccessException e) { System.out.println("Correctly failed to invoke unavailable method"); } } }
As we can see, the assertion is first obtained using the WSTrustClient API. Once the assertion has been acquired, it is included in the InitialContext env as a String so that WLInitialContextFactory pushes it to the client-side security context. Then we attempt to call all methods on the sample EJB3 session and print the results of these calls.
Configuring the custom authentication provider on Weblogic
To configure SimpleSAML2STSAuthenticator copy the attached simpleSAML2STSAuthenticator.jar to $DOMAIN_HOME/lib/mbeantypes/. This library was created using Weblogic's MDF and MJF utils. This authentication provider depends on PicketLink and Log4j so copy the attached picketlink-fed-1.0.4-SNAPSHOT.jar and log4j.jar to $DOMAIN_HOME/lib/.
Start Weblogic and access the Administration Console application. Navigate to the active Security Realm. Go to the Providers tab and select to add a new provider. SimpleSAML2STSAuthenticator should be available as one of the options. Add one instance of this provider. Reorder the providers so that this new provider is the first one.
Select this provider to access the configuration options. The control flag should be set to SUFFICIENT as this authentication provider only checks SAML2 assertions. If a password is sent as the credential, the provider's login module is ignored and the next provider in the list will handle authentication.
In the provider's specific configuration, set up the roles properties file. This is the full path to a properties file containing the groups each user belongs to. Here is the example used:
UserA=RegularUser,Administrator UserB=RegularUser UserC=Guest
Set up also the STS configuration file. This is the full path to a properties file containing the information to set the STS connection. Here is the example used:
serviceName=PicketLinkSTS portName=PicketLinkSTSPort endpointAddress=http://localhost:8080/picketlink-sts-1.0.0/PicketLinkSTS username=JBoss password=JBoss
Restart Weblogic after setting up the provider so the changes take effect.
Deploying the application and running the client
Access the Administration Console application and navigate to Deployments. Select ejb3.jar and deploy it as an application with the default values (including DDOnly security configuration).
Run the client in a shell:
java -cp wlclient.jar:picketlink-fed-1.0.4-SNAPSHOT.jar:/opt/jboss-eap-5.0/jboss-as/client/jbossall-client.jar:. SAML2EJB3WeblogicTest
If everything is configured correctly, the output should look like this:
Testing UserA UserA UserA UserA Correctly failed to invoke unavailable method Testing UserB UserB Correctly failed to invoke administrative method UserB Correctly failed to invoke unavailable method Testing UserC Correctly failed to invoke regular method Correctly failed to invoke administrative method UserC Correctly failed to invoke unavailable method
Comments