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#diffauthmoduler...
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.