JAX-WS Authentication in a nustshell

In the context of an HTTP transaction, BASIC access authentication is a method for a web browser or other client program to provide a user name and password when making a request.
This tutorial shows how to perform BASIC authentication for JAX-WS Web services. Then, as an alternative, we will learn how to use Apache CXF Interceptors as authentication mechanism.

One advantage of the BASIC access authentication is that all web browsers support it. It is often used by Intranet private systems, where it’s not necessary a strict security policy.

How are credentials encoded with HTTP Basic authentication ?
Before transmission, the user name is appended with a colon and concatenated with the password. The resulting string is encoded with the Base64 algorithm. For example, given the user name ‘Aladdin’ and password ‘open sesame’, the string ‘Aladdin:open sesame’ is Base64 encoded, resulting in ‘QWxhZGRpbjpvcGVuIHNlc2FtZQ==’.

Where are credentials stored ? not in the SOAP packet but in the HTTP Header.
Here is a DUMP of a SOAP request using BASIC Authentication:

POST /windcpms/ws/CfmsInterfaceService HTTP/1.1
Content-Type: text/xml; charset=UTF-8
Accept: */*
Authorization: Basic YWRtaW5BQUE6dGVzdG53aw==
SOAPAction: ""
User-Agent: Apache CXF 2.5.0
Cache-Control: no-cache
Pragma: no-cache
Host: 127.0.0.1:8080
Connection: keep-alive
Content-Length: 368

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>. . . . .</soap:Body>
</soap:Envelope>

That being said, there are several ways to achieve basic authentication. Let’s see how to do it with JAX-WS

JAX-WS Authentication

To enable authentication with JAX-WS the simplest strategy is to use the @WebContext annotation, which declares the authMethod:

@Stateless
@SecurityDomain("mydomain")
@RolesAllowed("dev")
@WebContext(contextRoot="/webapp", urlPattern="/*", authMethod="BASIC", transportGuarantee="NONE", secureWSDLAccess=false)
public class EndpointEJB implements EndpointInterface
{
  ...
}

As you can see, we have specified also the @SecurityDomain to be used for authentication. The SecurityDomain can be defined differently based on your current Security framework (Elytron / Picketbox). Anyhow, it can also be declared in jboss-web.xml as follows:

<jboss-web>
   <security-domain>mydomain</security-domain>
</jboss-web>

Finally, include the login-config section in your web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">
  <security-constraint>
    <web-resource-collection>
      <web-resource-name>All web resources</web-resource-name>
      <url-pattern>/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>SampleRole</role-name>
    </auth-constraint>
  </security-constraint>
  <login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>mydomain</realm-name>
  </login-config>
  <security-role>
    <role-name>dev</role-name>
  </security-role>
</web-app>

A web service client may use the javax.xml.ws.BindingProvider interface to set the username/password:

URL wsdlURL = new File("resources/jaxws/samples/context/WEB-INF/wsdl/TestEndpoint.wsdl").toURL();
QName qname = new QName("http://org.jboss.ws/jaxws/context", "TestEndpointService");
Service service = Service.create(wsdlURL, qname);
port = (TestEndpoint)service.getPort(TestEndpoint.class);
 
BindingProvider bp = (BindingProvider)port;
bp.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, "user");
bp.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, "password");

Apache CXF Authentication

If you are using Apache CXF, a very simple and effective way is adding an Interceptor which is triggered when your Web services are Invoked.
Here is a sample one:

package com.sample.ws;

import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.cxf.binding.soap.interceptor.SoapHeaderInterceptor;
import org.apache.cxf.configuration.security.AuthorizationPolicy;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.ws.addressing.EndpointReferenceType;

 


public class BasicAuthAuthorizationInterceptor extends
 SoapHeaderInterceptor {   

  @Override
  public void handleMessage(Message message) throws Fault {

  AuthorizationPolicy policy = message.get(AuthorizationPolicy.class);
 
 
  // If the policy is not set, the user did not specify credentials.
  // 401 is sent to the client to indicate that authentication is required.
  if (policy == null) {
    sendErrorResponse(message, HttpURLConnection.HTTP_UNAUTHORIZED);
  return;
  }

  String username = policy.getUserName();
  String password = policy.getPassword();


 // CHECK USERNAME AND PASSWORD
  if (!checkLogin(username,password)) {
    System.out.println("handleMessage: Invalid username or password for user: " +   policy.getUserName());
  sendErrorResponse(message, HttpURLConnection.HTTP_FORBIDDEN);
  }
 }

  private boolean checkLogin(String username, String password) {
   if (username.equals("admin") && password.equals("admin")) {
    return true;
  }
   return false;
  }

  private void sendErrorResponse(Message message, int responseCode) {
   Message outMessage = getOutMessage(message);
   outMessage.put(Message.RESPONSE_CODE, responseCode);
 
   // Set the response headers
   @SuppressWarnings("unchecked")
   Map<String, List<String>> responseHeaders =  (Map<String, List<String>>)    message.get(Message.PROTOCOL_HEADERS);
 
   if (responseHeaders != null) {
     responseHeaders.put("WWW-Authenticate", Arrays.asList(new String[] { "Basic realm=realm" }));
     responseHeaders.put("Content-Length", Arrays.asList(new String[] { "0" }));
  }
  message.getInterceptorChain().abort();
   try {
    getConduit(message).prepare(outMessage);
   close(outMessage);
   } catch (IOException e) {
      e.printStackTrace();
   }
 }

  private Message getOutMessage(Message inMessage) {
   Exchange exchange = inMessage.getExchange();
   Message outMessage = exchange.getOutMessage();
   if (outMessage == null) {
    Endpoint endpoint = exchange.get(Endpoint.class);
    outMessage = endpoint.getBinding().createMessage();
    exchange.setOutMessage(outMessage);
   }
    outMessage.putAll(inMessage);
     return outMessage;
    }

  private Conduit getConduit(Message inMessage) throws IOException {
   Exchange exchange = inMessage.getExchange();
   EndpointReferenceType target = exchange.get(EndpointReferenceType.class);
   Conduit conduit = exchange.getDestination().getBackChannel(inMessage, null, target);
   exchange.setConduit(conduit);
   return conduit;
  }

  private void close(Message outMessage) throws IOException {
   OutputStream os = outMessage.getContent(OutputStream.class);
   os.flush();
   os.close();
  }
 
}

The above interceptor needs to be registered either with a simple annotation at Class level of your Web service:

@InInterceptors(interceptors = "com.sample.ws.BasicAuthAuthorizationInterceptor")

or using Apache CXF configuration file:

<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:cxf="http://cxf.apache.org/core"
 xmlns:jaxws="http://cxf.apache.org/jaxws"
 xsi:schemaLocation="
 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
 http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
 http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

 <import resource="classpath:META-INF/cxf/cxf-all.xml" />

 <cxf:inInterceptors>
   <ref bean="basicAuthAuthorizationInterceptor"/>
 </cxf:inInterceptors>

 </cxf:bus>

 <bean id="basicAuthAuthorizationInterceptor"  class="com.sample.ws.BasicAuthAuthorizationInterceptor">
 </bean>
</beans>

Another option is using the @WebServiceContext to access the Authorization parameter contained in the Header. This is a portable solution although it needs a bit more of work to decode the credentials to Base64 to plain text.

This requires Apache commons-codec libraries. You can download them here:

http://commons.apache.org/codec/

Authentication can be performed using a simple function:

byte[] buf = Base64.decodeBase64(userpass.getBytes());

Here’s the full code:

@WebService
public class SecuredWSImpl implements SecuredWS {

 @Resource
 WebServiceContext wsctx;
 
 
 public void doSomething () {

 doAuthentication();
 
 // Execute WS business logic
 }
 
 private void doAuthentication() {

 

 MessageContext mctx = wsctx.getMessageContext();
 Map http_headers = (Map) mctx.get(MessageContext.HTTP_REQUEST_HEADERS);
 
  ArrayList list = (ArrayList) http_headers.get("Authorization");
  if (list == null || list.size() == 0) {
    throw new RuntimeException("Authentication failed! This WS needs BASIC Authentication!");
 }
 
  String userpass = (String) list.get(0);
  userpass = userpass.substring(5);
  byte[] buf = Base64.decodeBase64(userpass.getBytes());
  String credentials = new String(buf);
 
  String username = null;
  String password = null;
  int p = credentials.indexOf(":");
  if (p > -1) {
   username = credentials.substring(0, p);
   password = credentials.substring(p+1);
  }   
  else {
   throw new RuntimeException("There was an error while decoding the Authentication!");
  }
  // This should be changed to a DB / Ldap authentication check 
  if (username.equals("admin") && password.equals("admin")) { 
  System.out.println("============== Authentication OK =============");
  return;
  }
  else {
   throw new RuntimeException("Authentication failed! Wrong username / password!");
  }
 
 }
 
}
Found the article helpful? if so please follow us on Socials