Friday, July 24, 2009

Binding WS-SecureConversation Bootstrap Identity to WebLogic Server Identity

In WebLogic Server, when you configure a web service to use WS-SecureConversation, you have a number of choices for how to "bootstrap" the conversation. Besides calling a Security Token Service (STS), you can also send a WS-Trust RequestSecurityToken (RST) message to the endpoint, and receive a SecureConversationToken in return. This token is then typically used to derive keys to sign and encrypt subsequent messages. But, how should the initial exchange where the context is created be secured?

WebLogic Server gives a number of options including 1 and 2 way SSL, Basic Authentication, and UsernameToken. With the exception of 1 way SSL, all of the other policies require the web service consumer to provide an identity. This identity is validated by WebLogic Server in the normal way. The web service can then also have a policy that requires another identity when invoking the service. For example, use 2 way SSL to bootstrap the secure conversation and then SAML to provide identity for the actual service. This makes a lot of sense if you want a separate bootstrap identity, but what if you don't? What if you want to use a single identity to both bootstrap the conversation and identify the consumer? Is there a way to do it without simply sending the same token in both the bootstrap request and the subsequent messages?

This was the question posed to me recently by a customer. There is nothing in the WS-SecureConversation standard that says the bootstrap identity should be preserved, but the requirement does seem pretty reasonable. Also, in discussing this issue with a colleague he made the observation that WS-SecureConversation is like "SSL for Messages". The SSL standard does not require that the original client certificate is passed for identity on every request, but many, if not all implementations do. So, following that analogy, I set off this week to try to get this type of functionality working inside of WLS.

So, after trying what felt like every conceivable approach, and a lot of late nights, this is how you can do it. There are a couple things that you have to know about the bootstrapping process and the WLS web services stack. The first is that during the bootstrapping process a session is created, but that its not associated with the bootstrap user. The second is that the WLS web services client will send the JSESSIONID cookie on subsequent requests. The third is that a HTTP based web service is really two different resources inside of WLS - one of type <url> and one of type <webservices> . The idea is to capture the session, get it associated with Subject created by the authentication of the bootstrap identity, and then push that Subject onto the Servlet stack, making it available for the web-service. As long as the client sends the same JSESSIONID cookie, the bootstrap identity will be preserved.

The solution leverages a SessionEventListener to capture the HttpSession that is created during the bootstrapping process. The session is stored in a ThreadLocal.

public class SessionListener implements HttpSessionListener {
private HttpSession session = null;

public void sessionCreated(HttpSessionEvent event) {
session = event.getSession();
WSCSubjectThreadLocal.getWSCSubjectThreadLocal().set(session);
}

public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
}
}


With the session stored in the ThreadLocal, the next thing to happen is to capture the bootstrap identity in the form of the Subject, and add it to the session. This can be done through a custom AuthenticationProvider, but this a very unusual provider. Its required, its configured to be the last provider in the realm, and its only purpose is in the commit method to capture the subject.

public boolean commit() {

HttpSession session =
WSCSubjectThreadLocal.getWSCSubjectThreadLocal().get();

if (session!=null) {
session.setAttribute("wsscSubject",this.subject);
WSCSubjectThreadLocal.getWSCSubjectThreadLocal().remove();
}
return true;
}


So, at this point the bootstrapping is complete. The Subject is stored as an attribute in the session and the client is passed the JSESSIONID cookie. Assuming that the client sends the cookie in the next request, all that is left to do is to push the Subject on to the stack. To accomplish this, I used a custom AuthorizationProvider. This provider is only looking for requests, and always returns a PEMIT. Its only purpose if to push the Subject onto the stack.

public Result isAccessAllowed(
Subject subject, Map map, Resource resource,
ContextHandler contextHandler,Direction direction) {

if (resource.getType().equals("")) {

HttpServletRequest request = (HttpServletRequest)contextHandler.getValue("HttpServletRequest");

if (subject.getPrincipals().size()==0) {

HttpSession session = request.getSession();

Subject theWSCSubject = (Subject)session.getAttribute("wsscSubject");

if (theWSCSubject!=null) { ServletAuthentication.runAs(theWSCSubject,request);
}
}
}
return Result.PERMIT;
}


The application needs to be deployed with the Custom Roles and Policies. If the URL is protected in the deployment descriptor, the bootstrapping process will fail - the user is not authorized. Its also worth noting the limitation that the identity is tied to the session and the client is responsible for sending the session in the cookie in the transport. This means that a single client won't be able to maintain two conversations concurrently with the same server. Also, since the identity is not included in the message, this solution is best suited for single party operations - client calls WebLogic Server, and WLS processes the message. Although, since there is a real identity inside of WLS, the identity can be pretty easily propagated using the CredentialMappers (PKI/SAML) of WLS.


1 comment:

  1. This comment has been removed by a blog administrator.

    ReplyDelete

Note: Only a member of this blog may post a comment.