Starting with  Promoted Build 36  of  SailFin,  Metro 1.3 users can perform Programmatic Authorization decisions inside their  SEI Implementations. 

1. What is the API to be used for Programmatic Authorization ?

 The API  has been there in JAXWS since the very beginning,  javax.xml.ws.WebServiceContext; Specifically the method isUserInRole(String role).

2. How are Roles defined ?

The Standard JavaEE methods for defining Roles for WebApplications within the web.xml file can be used for WebServices as well.  For example the web.xml below defines two roles doctor and patient.

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
        </welcome-file-list>
    <security-role>
        <description>A doctor</description>
        <role-name>doctor</role-name>
    </security-role>
    <security-role>
        <description>A patient</description>
        <role-name>patient</role-name>
    </security-role>
</web-app>

 

3. How is the Principal to Role Mapping Handled ?

GlassFish provides facilities for doing this.  It is an entire topic of its own and  most people writing WebApplications for GlassFish know how to do this.  The following two links have more details on this topic.

http://blogs.sun.com/monzillo/entry/principal_2_role_mapping_and
http://docs.sun.com/app/docs/doc/819-3672/beacr

4. What are the Configuration Steps to Enable Programmatic Authorization

In general when making use of NON-SAML Security Mechanism(s) such as UsernameToken, X509Certificate or Kerberos Tokens, the only thing one needs to do before he/she can use the Programmatic Authorization API is to make sure steps 2 and 3 above are taken care of (i.e define your Roles and Define your Principal to Role Mapping). This is because the Metro runtime can automatically establish the incoming Caller Identity for those Mechanisms.  However for SAML based Mechanisms the Runtime would not know what is the incoming Caller Identity since the Principal and other Authorization Information would generally be inside the SAML Assertion sent by the Caller and only the WebService can decide what is the exact caller Identity.

So in Metro 1.3 we have extended the  SAMLAssertionValidator interface to allow the SEI developer to decide and set the principal that identifies the incoming caller (presumably after analyzing the contents of the incoming SAML assertion and any other additional Context). The new interface is com.sun.xml.wss.impl.callback.SAMLValidator interface.  The sample below would show an implementation of the interface.

5. How do i Integrate my UsernameToken/Certificate Authentication  in Metro WebService with GlassFish Realms

When using UsernameToken or Certificate based scenarios, one can make use of  GlassFish Realms, and the associated assing-groups facilities to automatically assign additional  group memberships to the incoming Caller.  These Groups may then be mapped to Roles as appropriate within sun-web.xml or using default-principal-to-role-mapping feature of GlassFish (see 3 above).

https://glassfish.dev.java.net/javaee5/docs/DG/beabg.html#beabo
https://glassfish.dev.java.net/javaee5/security/faq.html#diffauthmodulerealm
http://blogs.sun.com/swchan/entry/jdbcrealm_in_glassfish
http://blogs.sun.com/swchan/entry/assign_groups

So a WebService making use of  UsernameToken authentication mechanisms can declare the realm against which the username/password needs to be authenticated.  The realm declaration however is not under the  <login-config/> element of web.xml as defined in  JavaEE. This is because the login-config element was meant for WebApplications.

<login-config>
<auth-method>FORM</auth-method>
<realm-name>LDAPRealm</realm-name>
<form-login-config>
<form-login-page>/pages/logon.jsp</form-login-page>
<form-error-page>/pages/logonError.jsp</form-error-page>
</form-login-config>
</login-config>
Specifically  the auth-method is required inside a login-config and those methods  FORM/BASIC etc are not applicable for SOAP Message Security.  So the alternative way to define the realm for your webapplication is currently to package your WAR file into an EAR and define the realm inside sun-application.xml.  Netbeans can be used to easily create a Web Application Packaged as an EAR. And then one can add a sun-application.xml file defining the realm. See example below.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-application PUBLIC '-//Sun Microsystems, Inc.//DTD Application Server 9.0 Java EE Application 5.0//EN' 'http://www.sun.com/software/appserver/dtds/sun-application_5_0-0.dtd'>
<sun-application>
   <realm>JDBCRealm</realm>
</sun-application>

In the above sample i have defined the realm to be the JDBCRealm supported by GlassFish.

Note:  The SailFin Builds of GlassFish support a new JDBCDigestRealm.  However the PasswordDigest Authentication Mechanism of  Metro is still not integrated with the JDBCDigestRealm. This would come soon in near future releases of Metro.

6.Sample SEI Impl showing Programmatic Authorization

As a sample this document makes use of  the  SAML Authorization over SSL  security mechanism configured from NetBeans for my Metro 1.3 WebService.  The idea is that the NameIdentifier of the SAML Assertion would be used to decide the incoming Caller Principal.  The sample defines two roles "doctor"and "patient".  Here is how the web.xml would look like :

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
        </welcome-file-list>
    <security-role>
        <description>A doctor</description>
        <role-name>doctor</role-name>
    </security-role>
    <security-role>
        <description>A patient</description>
        <role-name>patient</role-name>
   </security-role>
  </web-app>

In this sample, for the sake of simplicity,  i would just make use of  explicit principal to role mapping in sun-web.xml.  This approach is cumbersome and does not scale as the number of different registered users of a WebService can easily run into thousands.  Please see the other approaches of doing P2R Mapping as described by the links in section 3 above. So here is my sun-web.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 Servlet 2.5//EN" "http://www.sun.com/software/appserver/dtds/sun-web-app_2_5-0.dtd">
<sun-web-app error-url="">
  <context-root>/SAMLSSL</context-root>
  <security-role-mapping>
    <role-name>doctor</role-name>
    <principal-name>DrRobert</principal-name>
  </security-role-mapping>
  <security-role-mapping>
    <role-name>patient</role-name>
    <principal-name>MrSick</principal-name>
  </security-role-mapping>
  <class-loader delegate="true"/>
  <jsp-config>
    <property name="keepgenerated" value="true">
      <description>Keep a copy of the generated servlet class' java code.</description>
    </property>
  </jsp-config>
</sun-web-app>


Here is the code that i have implemented inside my SAMLValidator.  The main logic here is to look for a NameIdentifier with name  DrRobert  and only if that is found i would set a Principal as understood by glassfish.  Note that the sample uses a Proprietary GlassFish Principal Class. In near future release of Metro we would add a Metro API that would allow setting the principal inside the Subject.  The Subject is the one that the GlassFish Container would use for subsequent authorization decisions.  Below is the simplistic code of my SAML Validator. In reality the decision code inside the SAMLValidator can be complex, making use of other context information and may lookup some databases etc.

package test;

import com.sun.xml.wss.XWSSecurityException;
import com.sun.xml.wss.impl.callback.SAMLAssertionValidator.SAMLValidationException;
import com.sun.xml.wss.saml.util.SAMLUtil;
import java.security.Principal;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.auth.Subject;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class MySAMLValidator implements com.sun.xml.wss.impl.callback.SAMLValidator {

    //It is enough to just handle this new Method
    public void validate(XMLStreamReader arg0, Map arg1, Subject arg2) throws SAMLValidationException {
        try {
           
              Element domSamlAssertion = SAMLUtil.createSAMLAssertion(arg0);
            //Do a SAML:Conditions validation to make sure the SAML assertion is Valid
            SAMLUtil.validateTimeInConditionsStatement(domSamlAssertion);
            //get hold of the SAML:NameIdentifier element
            NodeList nidList = domSamlAssertion.getElementsByTagNameNS("urn:oasis:names:tc:SAML:1.0:assertion","NameIdentifier");
            Node nid = null;
            if (nidList.getLength() > 0) {
                nid = nidList.item(0);
            }
            String child = nid.getFirstChild().getNodeValue();
           
//set the Subject Principal's upon successful validaton.
            // Set the Principal into the Subject if the NameIdentifier is a Valid Doctor
            //TODO: Provider Metro API to create and add a Principal to subject.
            if (child.startsWith("CN=DrRobert")) {
                   Principal p = new com.sun.enterprise.deployment.PrincipalImpl("DrRobert");
                   arg2.getPrincipals().add(p);
            }

        } catch (XWSSecurityException ex) {
            Logger.getLogger(MySAMLValidator.class.getName()).log(Level.SEVERE, null, ex);
            throw new SAMLValidationException(ex);
        } catch (XMLStreamException ex) {
            Logger.getLogger(MySAMLValidator.class.getName()).log(Level.SEVERE, null, ex);
            throw new SAMLValidationException(ex);
        }
    }

    public void validate(Element arg0, Map arg1, Subject arg2) throws SAMLValidationException {
        throw new UnsupportedOperationException("SAMLValidator : Operation Should not be Called1.");
    }
   
    //Older methods which do not take a subject argument
    public void validate(Element arg0) throws SAMLValidationException {
        throw new UnsupportedOperationException("SAMLValidator : Operation Should not be Called2.");
    }
    //Older methods which do not take a subject argument
    public void validate(XMLStreamReader arg0) throws SAMLValidationException {
        throw new UnsupportedOperationException("SAMLValidator : Operation Should not be Called3. Because we overrider the new Methods");
    }
   
}


And here is the Security Policy of my WSIT Configuration File for the sample  WebService.  Note the configuration of the SAMLValidator.

 <wsp:Policy wsu:Id="SAMLServicePortBindingPolicy">
        <wsp:ExactlyOne>
            <wsp:All>
                <wsaws:UsingAddressing xmlns:wsaws="http://www.w3.org/2006/05/addressing/wsdl"/>
                <sp:TransportBinding>
                    <wsp:Policy>
                        <sp:TransportToken>
                            <wsp:Policy>
                                <sp:HttpsToken RequireClientCertificate="false"/>
                            </wsp:Policy>
                        </sp:TransportToken>
                        <sp:Layout>
                            <wsp:Policy>
                                <sp:Lax/>
                            </wsp:Policy>
                        </sp:Layout>
                        <sp:IncludeTimestamp/>
                        <sp:AlgorithmSuite>
                            <wsp:Policy>
                                <sp:Basic128/>
                            </wsp:Policy>
                        </sp:AlgorithmSuite>
                    </wsp:Policy>
                </sp:TransportBinding>
                <sp:Wss10>
                    <wsp:Policy>
                        <sp:MustSupportRefKeyIdentifier/>
                    </wsp:Policy>
                </sp:Wss10>
                <sp:SignedSupportingTokens>
                    <wsp:Policy>
                        <sp:SamlToken sp:IncludeToken="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy/IncludeToken/AlwaysToRecipient">
                            <wsp:Policy>
                                <sp:WssSamlV11Token10/>
                            </wsp:Policy>
                        </sp:SamlToken>
                    </wsp:Policy>
                </sp:SignedSupportingTokens> 
             <sc:ValidatorConfiguration wspp:visibility="private">
                <sc:Validator name="samlAssertionValidator" classname="test.MySAMLValidator" />
             </sc:ValidatorConfiguration>
            </wsp:All>
        </wsp:ExactlyOne>
    </wsp:Policy>


And Finally here is the code in my SEI implementation class where i am querying the Roles of the Incoming Caller :

package test;

import javax.annotation.Resource;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.xml.ws.WebServiceContext;


@WebService()
public class SAMLService {

    @Resource
    private WebServiceContext context;
    /**
     * Web service operation
     */
    @WebMethod(operationName = "operation")
    public String operation(@WebParam(name = "parameter")
    String parameter) {
        //TODO write your implementation code here:
        Boolean bool = context.isUserInRole("doctor");
        System.out.println("context.isUserInRole(\"doctor\")=" + bool);
        Boolean isPatient = context.isUserInRole("patient");
        System.out.println("context.isUserInRole(\"patient\")=" + isPatient);
        return "Hello " + parameter;
    }

}

7. SAMPLE WebService Client

The WebSerivce Client would need to use SSL, this can be achieved by specifying a https url for retrieving the WSDL, when creating the WebServiceReference within NetBeans (For example, in this sample i specified, https://somehost:8181/SAMLSSL/SAMLServiceService?wsdl). The WebService Client would need to implement the SAML CallbackHandler which would send a SAML Assertion containing NameIdentifier as "DrRobert".  Here is the code for the Client Side SAML CallbackHandler, which creates a Dummy SAML Assertion for now. In reality the CallbackHandler can authenticate to  a Security Token Service to obtain a Token.

package test;

import com.sun.xml.wss.impl.callback.SAMLCallback;
import com.sun.xml.wss.saml.Assertion;
import com.sun.xml.wss.saml.Conditions;
import com.sun.xml.wss.saml.NameIdentifier;
import com.sun.xml.wss.saml.SAMLAssertionFactory;
import com.sun.xml.wss.saml.Subject;
import com.sun.xml.wss.saml.SubjectConfirmation;
import java.io.IOException;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.List;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import org.w3c.dom.Element;


public class MySAMLCBH implements CallbackHandler {

    private  UnsupportedCallbackException unsupported =
                    new UnsupportedCallbackException(null,
                        "Unsupported Callback Type Encountered");
   
    private  static Element svAssertion = null;
    public static final String senderVouchesConfirmation =
    "urn:oasis:names:tc:SAML:1.0:cm:sender-vouches";
    
    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
         for (int i=0; i < callbacks.length; i++) {
            if (callbacks[i] instanceof SAMLCallback) {
                try{
                    SAMLCallback samlCallback = (SAMLCallback)callbacks[i];
                    if (samlCallback.getConfirmationMethod().equals(samlCallback.SV_ASSERTION_TYPE)){
                            samlCallback.setAssertionElement(createSVSAMLAssertion());
                            svAssertion=samlCallback.getAssertionElement();
                    }else{
                            throw new Exception("SAML Assertion Type is not matched.");
                    }
                }catch(Exception ex){
                        ex.printStackTrace();
                }
            } else {
                throw unsupported;
            }
        }
    }
   
    private static Element createSVSAMLAssertion() {
        Assertion assertion = null;
        try {
            // create the assertion id
            String assertionID = String.valueOf(System.currentTimeMillis());
            String issuer = "CN=Assertion Issuer,OU=AI,O=Assertion Issuer,L=Waltham,ST=MA,C=US";
           
             
              GregorianCalendar c = new GregorianCalendar();
            long beforeTime = c.getTimeInMillis();
            // roll the time by one hour
            long offsetHours = 60*60*1000;

            c.setTimeInMillis(beforeTime - offsetHours);
            GregorianCalendar before= (GregorianCalendar)c.clone();
           
              c = new GregorianCalendar();
            long afterTime = c.getTimeInMillis();
            c.setTimeInMillis(afterTime + offsetHours);
            GregorianCalendar after = (GregorianCalendar)c.clone();
           
              GregorianCalendar issueInstant = new GregorianCalendar();
            // statements
            List statements = new LinkedList();


            SAMLAssertionFactory factory = SAMLAssertionFactory.newInstance(SAMLAssertionFactory.SAML1_1);

            NameIdentifier nmId =
            factory.createNameIdentifier(
            "CN=DrRobert,OU=SU,O=SAML User,L=Los Angeles,ST=CA,C=US",
            null, "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName");

            SubjectConfirmation scf =
            factory.createSubjectConfirmation("urn:oasis:names:tc:SAML:1.0:cm:sender-vouches");
          
 
            Subject subj = factory.createSubject(nmId, scf);
          
            List attributes = new LinkedList();

            List attributeValues = new LinkedList();
            attributeValues.add("ATTRIBUTE1");
            attributes.add( factory.createAttribute(
                "attribute1",
                "urn:com:sun:xml:wss:attribute",
                 attributeValues));

            statements.add(
            factory.createAttributeStatement(subj, attributes));
           
              Conditions conditions = factory.createConditions(before, after, null, null, null);
           
              assertion = factory.createAssertion(assertionID, issuer, issueInstant,
            conditions, null, statements);

            return assertion.toElement(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}


Here is the Metro configuration file for the WebSerivce Client .  Note the configuration of  SAMLCallbackHandler (Note : this step can be achieved within Netbeans by specifying the name of the SAML CallbackHandler)

 <wsp:Policy wsu:Id="SAMLServicePortBindingPolicy">
        <wsp:ExactlyOne>
            <wsp:All>
                <sc:CallbackHandlerConfiguration wspp:visibility="private">
                    <sc:CallbackHandler name="samlHandler" classname="test.MySAMLCBH"/>
                </sc:CallbackHandlerConfiguration>
            </wsp:All>
        </wsp:ExactlyOne>
</wsp:Policy>


So when i run the client, here is what is printed out of the Role Query on the Server (as seen in GlassFish server.log file)

[#|2008-09-15T17:24:13.186+0530|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=17;_ThreadName=httpSSLWorkerThread-8181-0;|
context.isUserInRole("doctor")=true|#]
[#|2008-09-15T17:24:13.186+0530|WARNING|sun-appserver9.1|javax.enterprise.system.core.security|_ThreadID=17;_ThreadName=httpSSLWorkerThread-8181-0;_RequestID=d360250d-55e8-4c7e-bb6a-08af7722525d;|SEC5052: null Subject used in SecurityContext construction.|#]
[#|2008-09-15T17:24:13.186+0530|INFO|sun-appserver9.1|javax.enterprise.system.stream.out|_ThreadID=17;_ThreadName=httpSSLWorkerThread-8181-0;|
context.isUserInRole("patient")=false|#]

Anyone interested in the NetBeans Project(s) for the client and server can contact me (vbkumar.jayanti@sun.com)

Future

We are currently working on Support for Java EE 5 defined  Declarative Authorization based on Annotations @RolesAllowed,  @DeclareRoles etc. for Servlet based webservices.  Note that  Declarative method level authorization using these annotations is already supported in GlassFish for EJB WebServices.