Accessing Tomcat JAAS-secured (Form-based) JAX-RS endpoint, from within Java EE client

Unlike my previous post here, sometimes we do need to access JAX-RS resource which is secured by the “traditional” approach of using FORM-based authentication (i.e. jsecuritycheck).

I thought it would be good to share how I do it from within Java client.

Please note that this post is based on (Bien 2013).

Firstly, the thing about ‘jsecurity’-check in Tomcat…

After much reading, I found some help from the following entry at a href=”www.stackoverflow.com” target=”_blank”>stackoverflow.com:

that to get some resource on the tomcat server with j_security_check auth it is necessary to implement three steps:

  1. Send GET request to the needed private resource, in response you get a cookie (Header “Set cookie”)
  2. Send request with cookie (from step 1) to the j_security_check. On response you should get code 302 – “Moved Temporarily”
  3. Now you can repeat request to the private resource with same cookie, on responce you get needed resource data.

(Alexander 2011)

Let’s look at some code

I have put comments in the code to explain what is going on in relation to the three points above. But basically what is happening is that we intercept (Kops 2013) the request in order to first make another request authenticating against jsecuritycheck.

– Register the ‘Authenticator’ when making the call

final Client client = ClientBuilder.newClient().register(new Authenticator("http://myapplication.com/myweb", "username", "password"));
final WebTarget target = client.target("http://myapplication.com/myweb/resources/person");
final JsonObject response = target.request(MediaType.APPLICATION_JSON_TYPE).get(JsonObject.class);
// do something with the response

– Our ‘Authenticator’, which is a ‘javax.ws.rs.client.ClientRequestFilter’

package id.co.lucyana.hr.util;

// imports omitted

@Provider
public final class Authenticator implements ClientRequestFilter {
    private static final String COOKIE = "Cookie";

    // security params
    private static final String J_SECURITY_CHECK = "j_security_check";
    private static final String J_USERNAME = "j_username";
    private static final String J_PASSWORD = "j_password";

    private final String username;
    private final String password;
    private final String baseUri;
    
    // requires by @Provider
    public Authenticator() {
        this.username = null;
        this.password = null;
        this.baseUri = null;
    }

    public Authenticator(final String baseUri, final String username, final String password) {
        this.username = username;
        this.password = password;
        this.baseUri = baseUri;
    }

    @Override
    public void filter(final ClientRequestContext requestContext) throws IOException {
        final List<Object> cookies = new ArrayList<>();

        /*
         * This is hitting the URL as requested by the ClientBuilder.
         * (Refer to the line: "1. Send GET request to the needed private resource,
         * in response you get a cookie (Header “Set cookie”)"
         */
        final Client initialClient =  ClientBuilder.newClient();
        final Response responseInitial = initialClient
                .target(requestContext.getUri())
                .request(requestContext.getAcceptableMediaTypes().get(0))
                .method(requestContext.getMethod());
        initialClient.close();

        /*
         * This section is getting the cookie above and use it to make the call against jsecuritycheck
         * (Refer to the line: 2. Send request with cookie (from step 1) to the j_security_check.
         * On response you should get code 302 – “Moved Temporarily”)
         */
        responseInitial.getCookies().values().stream().forEach((cookie) -> {
            cookies.add(cookie.toCookie());
        });
        final Client loginClient = ClientBuilder.newClient();
        final Form form = new Form();
        form.param(J_USERNAME, this.username);
        form.param(J_PASSWORD, this.password);
        final Response loginResponse = loginClient.target(this.baseUri).path(J_SECURITY_CHECK)
                .request(MediaType.APPLICATION_FORM_URLENCODED)
                .header(COOKIE, cookies)
                .post(Entity.form(form));
        loginClient.close();
        loginResponse.getCookies().values().stream().forEach((cookie) -> {
            cookies.add(cookie.toCookie());
        });

        /*
         * This is right before making the actual call. So we add the cookie (which the server will be
         * able to tell that this call has been authenticated).
         * (Refer to the line: 3. Now you can repeat request to the private resource with same cookie,
         * on responce you get needed resource data.)
         */
        requestContext.getHeaders().put(COOKIE, cookies);
    }
}

References

Alexander, 2011, ‘Request to j_security_check return 408 error only with right paramters’, stackoverflow.com, accessed on 28 November 2016

Bien, A, 2013, ‘Client-side HTTP Basic Authentication With JAX-RS 2.0’, Adam Bien’s weblog, accessed on 28 November 2016

Kops, M, 2013, ‘JAX-RS 2.0 REST Client Features by Example’, hasCode.com, accessed on 28 November 2016

JSON.stringify(result) prints empty collection

Just recently when I did a simple test on my JQuery-mobile page to print the result return from JAX-RS end point, I received an empty collection.

It says

localhost:8080 says:

{"adjustmentInfo":[{}]}

At the same time, I am sure my end point returned a collection containing one AdjustmentInfo object.

Here is my AdjustmentInfo class.

// imports omitted

@XmlRootElement // don't forget, required by JAX-RS
public final class AdjustmentInfo implements Serializable {
    
    private long adjustmentNo;
    private String reason;
    private BigDecimal amount;
    private boolean negating;

    /**
     * Required by JAX-RS
     */
    private AdjustmentInfo() {
        this.adjustmentNo = 0;
        this.reason = null;
        this.amount = null;
        this.negating = false;
    }

    AdjustmentInfo(final Adjustment adjustment) {
        this.adjustmentNo = adjustment.getAdjustmentNo();
        this.reason = adjustment.getReason();
        this.amount = adjustment.getAmount();
        this.negating = adjustment.isNegating();
    }

    public long getAdjustmentNo() { ... }
    public String getReason() { ... }
    public BigDecimal getAmount() { ... }
    public boolean isNegating() { ... }
}

After trial-and-error, I found that apparently I had to implement all mutator method (I had not implemented it before because I didn’t have any need of them).

// imports omitted

@XmlRootElement // don't forget, required by JAX-RS
public final class AdjustmentInfo implements Serializable {
    
    private long adjustmentNo;
    private String reason;
    private BigDecimal amount;
    private boolean negating;

    /**
     * Required by JAX-RS
     */
    private AdjustmentInfo() {
        this.adjustmentNo = 0;
        this.reason = null;
        this.amount = null;
        this.negating = false;
    }

    AdjustmentInfo(final Adjustment adjustment) {
        this.adjustmentNo = adjustment.getAdjustmentNo();
        this.reason = adjustment.getReason();
        this.amount = adjustment.getAmount();
        this.negating = adjustment.isNegating();
    }

    public long getAdjustmentNo() { ... }
    public String getReason() { ... }
    public BigDecimal getAmount() { ... }
    public boolean isNegating() { ... }

    // IMPLEMENT THESE MUTATORS

    public void setAdjustmentNo(long adjustmentNo) { ... }
    public void setReason(String reason) { ... }
    public void setAmount(BigDecimal amount) { ... }
    public void setNegating(boolean negating) { ... }
}

Is it in the spec? I wonder…

Java EE integration testing with Arquillian (incl. JAX-RS, JPA, and JSR-352 Batch, Wildfly) using Chameleon, Shrinkwrap, Drone/Graphene

Continuing from my previous post here, I think I have learnt a few new (and exciting) things about Java EE and also Arquillian, and testing, and I would like to share it with you.

But before we start, I would like to first draw your attention to the following points (These are purely my opinion):

  • In anticipation with Oracle announcement stopping the commercial support of Commercial, I guess I decided to take a look at another leading Java EE application server, Wildfly, and I was so glad I did.
  • In the previous post I used embedded glassfish for testing. Whilst embedded Wildfly is also available, I personally find that testing using remote application server is so much “real”, or getting us closer to simulate the real production environment. Just as an example, if I want to test my application which is running on Wildfly 10 (which uses Java Mail, JMS), currently point to database 'xyz', all I need to do is to unzip the same version of application server used in real environment (simply rename it with -test) and point to another copy of 'xyz' database.

Anyway, let’s start.

In this post, I am going to cover the following:

  • The testing scenario (covering JAX-RS and JSR-352 Batch)
  • Very brief setup of Wildfly
  • Programming JAX-RS and JSR-352 Batch
  • Setting up Arquillian with Chameleon (incl. ShrinkWrap)
  • Setting up of Drone/Graphene

The scenario

The test is to invoke a RESTFUL web service running on remote Wildfly (which in turn will invoke a JSR-352 Batch). But, instead of testing it manually, i.e. opening a browser or using curl, we are going to code it as an Arquillian test.

So, that is quite straight-forward.

Installing Wildfly

At the time of writing, I am using Wildfly 10 CR4.

  • Just download it in wildfly.org, and unzip it.
  • First, you need to add user. so open a terminal and navigate to <unzip location>/bin/ and run ./bin/add-user.sh. Just follow the prompt.
  • set a JAVA_HOME
  • First, you need to add user. so open a terminal and navigate to <unzip location>/bin/ and run ./bin/add-user.sh. Just follow the prompt.
  • To run wildfly simply run ./bin/standalone.sh. However, please note by default Wildfly start with Java EE 7 Web Profile (JBoss 2014). I might want to use feature such as JMS, so to start Java EE 7 Full Profile, do ./bin/standalone.sh --server-config=standalone-full.xml

Develop a JSR-352 Batch

A JSR-352 Batch (referred as Batch in this article) programming model is quite extensive, but very straight-forward to understand and to use. But basically you can either following the read-process-write model, or roll-your-own batchlet model (Gupta 2013). But for more details, you can refer to the Oracle tutorial here. (Kannan 2013).

In this example, I am going to walk you through a very simple read-process-write Batch.

– Write a job XML

  • Firstly, create an empty beans.xml to enable CDI.
  • Secondly, you need to create a folder batch-jobs under the META-INF. If you use maven, then this goes under resources.
  • Under the newly created forder, add a Job XML. Please note that by convention, the name of the batch job is nothing but the job JSL XML file name, minus the .xml extension (Kannan 2013). So, let’s say we call it testJob.xml.
  • The testJob.xml Comments interleaved.

    <?xml version="1.0" encoding="UTF-8"?>
    <job id="testJob" 
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/jobXML_1_0.xsd"
         version="1.0">
        <step id="testStep">
            
            <!--
                Basically the way it works is this.
                
                A reader is executed to read an item, and then passed to the
                processor.
                
                Depending on the checkpoint-policy, a collection of processed
                items are then passed to the writer, to be written. And then
                transaction commit.
                
                Please note in this case since the checkpoint-policy is set to
                'item' and item-count="1", it means that each item processed is
                to be written and transactioni commit.
            -->
            <chunk checkpoint-policy="item" item-count="1">
                <reader ref="testBatchReader" />
                <processor ref="testBatchProcessor" />
                <writer ref="testBatchWriter" />
            </chunk>
        </step>
    </job>
    

– Write the Batch Reader, Processor and Writer

  • The TestBatchReader.java. Batch will stop processing when reader returns null. This is a very simple example that simply return an Integer up to 100. But it can be much sophisticated than this, such as reading a file, etc.

    package id.co.lucyana.hr.batch;
    
    import javax.batch.api.chunk.AbstractItemReader;
    import javax.inject.Named;
    
    @Named
    public final class TestBatchReader extends AbstractItemReader {
    
        private int counter = 0;
        private static final int MAX_COUNTER = 100;
    
        @Override
        public Object readItem() throws Exception {
            while (this.counter < MAX_COUNTER) {
                ++this.counter;
                return this.counter;
            }
            return null;
        }
    }
    
  • The TestBatchProcessor.java. Do nothing

    package id.co.lucyana.hr.batch;
    
    import javax.batch.api.chunk.ItemProcessor;
    import javax.inject.Named;
    
    @Named
    public final class TestBatchProcessor implements ItemProcessor {
    
        @Override
        public Object processItem(Object item) throws Exception {
            return item; // do nothing here
        }
    }
    
  • The TestBatchWriter.java. Again, this one can be much sophisticated, such as writing to the underlying datastore, etc.

    package id.co.lucyana.hr.batch;
    
    import java.util.List;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import javax.batch.api.chunk.AbstractItemWriter;
    import javax.inject.Named;
    
    @Named
    public final class TestBatchWriter extends AbstractItemWriter {
    
        @Override
        public void writeItems(List items) throws Exception {
            Logger.getLogger(TestBatchWriter.class.getName()).log(Level.INFO, items.toString());
        }
    }
    

That’s is for your Batch programming. Now let’s start with JAX-RS to trigger Batch to run.

Develop an Restful web service

JAX-RS has been awhile, so I shall not bore you with the details. But basically all we want to achieve is to be able to invoke a Restful webservice, with a job name as a parameter to trigger a batch process.

This is the ApplicationPath

package id.co.lucyana.hr.util;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath(value = "resources")
public class ApplicationConfig extends Application {
}

And, this is the JAX-RS end point, receiving a job name as a parameter

package id.co.lucyana.hr.batch;

import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.batch.operations.JobSecurityException;
import javax.batch.operations.JobStartException;
import javax.batch.runtime.BatchRuntime;
import javax.ejb.LocalBean;
import javax.ejb.Singleton;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

@Singleton
@LocalBean
@Path(value = "batch")
public class BatchManager {

    @Path(value = "start")
    @GET
    @Produces(value = "application/json")
    public long start(@QueryParam("job") String job) {
        Logger.getLogger(BatchManager.class.getName()).log(Level.INFO, 
                BatchRuntime.getJobOperator().getJobNames().toString());
        try {
            return BatchRuntime.getJobOperator().start(job, new Properties());
        } catch (JobStartException | JobSecurityException e) {
            Logger.getLogger(BatchManager.class.getName()).log(
                    Level.SEVERE, e.getMessage(), e);
            return -1l;
        }
    }
}

Okay, now take a deep breath… and LET’S TEST!!!

Set up Arquillian, Chameleon, Graphene, Drone, Selenium

Now after all of those development, it is time to test what we have written, and we are going to test it against the real running application server.

The first thing we need to do is to set up Arquillian/Chameleon

– Adding Arquillian/Chameleon

If you look at my previous post, referring to the pom.xml there, you may notice that there are a lot of information regarding to the container used scattered there, i.e. org.jboss.arquillian.container, etc. Chameleon hides this information and makes it so easy to moves between containers (Knutsen 2015).

So the changes on my pom.xml would be as follows.

<?xml version="1.0" encoding="UTF-8"?>
<!-- omitted -->

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.arquillian</groupId>
                <artifactId>arquillian-bom</artifactId>
                <version>1.1.8.Final</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.jboss.arquillian</groupId>
            <artifactId>arquillian-bom</artifactId>
            <version>1.1.9.Final</version>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.junit</groupId>
            <artifactId>arquillian-junit-container</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.arquillian.container</groupId>
            <artifactId>arquillian-container-chameleon</artifactId>
            <version>1.0.0.Alpha5</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

<!-- omitted -->

And configure your arquillian.xml (which is located under your test/resources if you are using Maven) as follows:

<?xml version="1.0" encoding="UTF-8"?>
<arquillian xmlns="http://jboss.org/schema/arquillian"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd">
    <defaultProtocol type="Servlet 3.0" />

    <container qualifier="wildfly" default="true">
        <configuration>
            <property name="chameleonTarget">wildfly:10.0.0.CR4:remote</property>
            <property name="username"><!-- your username goes here --></property>
            <property name="password"><!-- your password goes here --></property>
        </configuration>
    </container>
</arquillian>

– Adding Graphene/Drone/Selenium

Now, remember what we are trying to emulate is as if the user enter a URL to invoke JAX-RS web service, passing a job name to start a Batch job. BUT, we want to code it as a JUnit test. In order to do that, we are going to use the combination of Graphene, Drone and Selenium. You can read more details about them here. But basically these technologies are part of the Arquillian test platform catered for web UI testing.

First we need to modify our pom.xml and add the following:

<!-- omitted -->
    <dependencyManagement>
        <dependencies>
            <!-- omitted -->
            <dependency>
                <groupId>org.jboss.arquillian.selenium</groupId>
                <artifactId>selenium-bom</artifactId>
                <version>2.43.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>       
        <!-- Omitted -->
        <dependency>
            <groupId>org.jboss.arquillian.graphene</groupId>
            <artifactId>graphene-webdriver</artifactId>
            <version>2.0.3.Final</version>
            <type>pom</type>
            <scope>test</scope>
        </dependency>
<!-- omitted -->

And add snippet in our arquillian.xml.

<?xml version="1.0" encoding="UTF-8"?>
    <!-- omitted -->
    <extension qualifier="webdriver">
       <!--<property name="browser">firefox</property>-->
       <property name="remoteReusable">false</property>
   </extension>
</arquillian>

N.B. I could not get ‘firefox’ to work. So by default it would be the ‘htmlUnit’.

All good, the last part is to write the actual Arquillian test.

Write an Arquillian test case

First things first I need to share. And that is to package the application as an EAR. I have tried to deploy it as a (EJB) JAR, JAX-RS does not work, and then simply as WAR, Batch does not work. So, for the purpose of this testing, I deployed as EAR, and all seems to be happy (Nozaki 2015).

So, let’s code our Arquillian test. Comments interleaved.

package id.co.lucyana.hr.batch;

import id.co.lucyana.hr.util.ApplicationConfig;
import java.net.URL;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchivePaths;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.EnterpriseArchive;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;

@RunWith(Arquillian.class)
public class BatchManagerTest {

    @Drone
    private WebDriver driver;

    @Deployment
    public static Archive<?> createTestArchive() {
        // just add classes required in your test
        final JavaArchive ejbJar = ShrinkWrap.create(JavaArchive.class, "ejb-jar.jar")
                .addClass(ApplicationConfig.class)
                .addClass(BatchManager.class)
                .addClass(TestBatchReader.class)
                .addClass(TestBatchProcessor.class)
                .addClass(TestBatchWriter.class)
                .addAsManifestResource("test-persistence.xml",
                        ArchivePaths.create("persistence.xml"))
                .addAsManifestResource("META-INF/beans.xml", 
                        ArchivePaths.create("beans.xml"))
                .addAsManifestResource("batch-jobs/testJob.xml")
                .addAsResource("ValidationMessages.properties");

        /*
         * Embedding war package which contains the test class is needed
         * So that Arquillian can invoke test class through its servlet
         * test runner
         */
        final WebArchive testWar = ShrinkWrap.create(
                WebArchive.class, "test.war").addClass(BatchManagerTest.class);
        
        final EnterpriseArchive ear = ShrinkWrap.create(EnterpriseArchive.class)
                .setApplicationXML("test-application.xml")
                .addAsModule(ejbJar)
                .addAsModule(testWar);
        return ear;
    }

    @Before
    public void beforeEachTest() {
        this.driver.manage().deleteAllCookies();
    }
    
    /*
     * So that we do not hard-code the URL, simply use @ArquillianResource
     */
    @Test
    @RunAsClient
    public void testInvoke(@ArquillianResource URL url) {
        this.driver.get(url.toString() + "resources/batch/start?job=testJob");
        String pageSource = this.driver.getPageSource();
        System.out.println(pageSource);
        Assert.assertTrue(true);
    }
}

Please note the use of @ArquillianResource which allows us not to hard-code the application URL (Knutsen 2012).

Result

Yey!!! So, that’s it. Make sure you have your Wildfly up and running, you can now run your test. Shrinkwrap will package your EAR, then Arquillian/Chameleon will deploy it remotely to the running Wildfly server, then as client, Drone/Graphene/Selenium will invoke the URL to run a Batch called testJob.

Here is the snippet of the rest result:

18:07:00,585 INFO  [org.jboss.as.server] (management-handler-thread - 2) WFLYSRV0010: Deployed "a3ee3dad-d71c-41b0-9f57-9ae6e7ffe859.ear" (runtime-name : "a3ee3dad-d71c-41b0-9f57-9ae6e7ffe859.ear")
18:07:06,137 INFO  [id.co.lucyana.hr.batch.BatchManager] (default task-1) []
18:07:06,332 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [1]
18:07:06,333 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [2]
18:07:06,333 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [3]
18:07:06,334 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [4]
18:07:06,334 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [5]
18:07:06,334 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [6]
18:07:06,334 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [7]
18:07:06,335 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [8]
18:07:06,335 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [9]
18:07:06,335 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [10]
18:07:06,335 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [11]
18:07:06,336 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [12]
18:07:06,336 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [13]
18:07:06,336 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [14]
18:07:06,336 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [15]
18:07:06,337 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [16]
18:07:06,337 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [17]
18:07:06,337 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [18]
18:07:06,337 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [19]
18:07:06,338 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [20]
18:07:06,338 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [21]
18:07:06,338 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [22]
18:07:06,338 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [23]
18:07:06,339 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [24]
18:07:06,339 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [25]
18:07:06,339 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [26]
18:07:06,339 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [27]
18:07:06,340 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [28]
18:07:06,340 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [29]
18:07:06,340 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [30]
18:07:06,340 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [31]
18:07:06,341 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [32]
18:07:06,341 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [33]
18:07:06,341 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [34]
18:07:06,341 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [35]
18:07:06,341 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [36]
18:07:06,342 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [37]
18:07:06,342 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [38]
18:07:06,342 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [39]
18:07:06,343 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [40]
18:07:06,343 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [41]
18:07:06,343 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [42]
18:07:06,344 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [43]
18:07:06,344 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [44]
18:07:06,344 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [45]
18:07:06,345 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [46]
18:07:06,345 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [47]
18:07:06,345 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [48]
18:07:06,345 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [49]
18:07:06,346 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [50]
18:07:06,346 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [51]
18:07:06,346 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [52]
18:07:06,347 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [53]
18:07:06,347 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [54]
18:07:06,347 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [55]
18:07:06,347 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [56]
18:07:06,348 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [57]
18:07:06,348 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [58]
18:07:06,348 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [59]
18:07:06,348 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [60]
18:07:06,349 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [61]
18:07:06,349 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [62]
18:07:06,349 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [63]
18:07:06,349 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [64]
18:07:06,349 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [65]
18:07:06,350 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [66]
18:07:06,350 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [67]
18:07:06,350 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [68]
18:07:06,350 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [69]
18:07:06,350 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [70]
18:07:06,351 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [71]
18:07:06,351 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [72]
18:07:06,351 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [73]
18:07:06,351 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [74]
18:07:06,352 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [75]
18:07:06,352 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [76]
18:07:06,352 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [77]
18:07:06,352 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [78]
18:07:06,352 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [79]
18:07:06,352 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [80]
18:07:06,353 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [81]
18:07:06,353 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [82]
18:07:06,353 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [83]
18:07:06,353 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [84]
18:07:06,353 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [85]
18:07:06,354 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [86]
18:07:06,354 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [87]
18:07:06,354 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [88]
18:07:06,354 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [89]
18:07:06,354 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [90]
18:07:06,355 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [91]
18:07:06,355 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [92]
18:07:06,355 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [93]
18:07:06,355 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [94]
18:07:06,356 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [95]
18:07:06,356 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [96]
18:07:06,356 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [97]
18:07:06,357 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [98]
18:07:06,357 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [99]
18:07:06,357 INFO  [id.co.lucyana.hr.batch.TestBatchWriter] (Batch Thread - 1) [100]
18:07:06,885 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 72) WFLYUT0022: Unregistered web context: /test

References

Allen et al., 2014, ‘Functional Testing using Drone and Graphene’, Red Hat Inc., accessed on 23 November 2015

Gupta, A, 2013, ‘Batch Applications in Java EE 7 – Undertanding JSR 352 Concepts: TOTD #192’, Oracle.com, accessed on 10 November 2015

JBoss, 2014, ‘Getting Started Guide – Wildfly 8’, JBoss.org, accessed on 10 November 2015

Kannan, M, 2013, ‘An Overview of Batch Processing in Java EE 7.0’, Oracle.com, accessed on 10 November 2015

Knutsen, A, 2012, ‘@ArquillianResource java.net.URL when test is run on the server’, JBoss Developer, accessed on 23 November 2015

Knutsen, A, 2015, ‘Arquillian Blog: Arquillian Container Chameleon 1.0.0.Alpha6 Released’, Red Hat Inc., accessed on 23 November 2015

Nozaki, K, 2015, ‘Arquillian EJB-JAR/EAR testing examples’, Kohei Nozaki’s blog, accessed on 23 November 2015

Oracle, 2013, ‘Java EE and GlassFish Server Roadmap Update’, Oracle.com, accessed on 10 November 2015

JAAS-secured JAX-RS end point

With the advent of RESTFUL (JAX-RS) as the “preferred” way to create web service end points, for a long time I have always wondered how people implement security mechanism around it.

At the end of the day, I presume the underlying implementation of JAX-RS is servlet, and therefore its security might also be around what is already provided by the container, i.e. JAAS.

This post will cover my findings on how to step-by-step implement FORM-based security using JDBC realm, JAX-RS and how to test it using cURL, on Glassfish 3.

Setting up JDBC-realm

Firstly, since we are using JDBC-realm, let’s assume that we have created a JDBC connection to the underlying database under the JNDI jdbc/test.

The next step is to create a new realm. You can do this by going to server-config > Security > Realms and add a new realm. Select the realm type com.sun.enterprise.security.auth.realm.jdbc.JDBCRealm, and then populate the mandatory fields.

  • Start by giving your new realm a name.
  • For JAAS Context, put jdbcRealm
  • Populate JNDI name, preferably starts with "jndi/"

Next, do pay attention to the rest of the fields. It seems that Glassfish expects to see two tables. The first table is to contain a list of users, with usernames as their unique identifier. The second table is to list the groups that each user belongs to. Username is to be the foreign-key link between the two tables. (The next section should give you better idea on how the tables should look, they are after all very simple).

Once these tables are created, we can then populate the mandatory fields accordingly.

Populate database for testing purpose

The next step is to populate the table for testing purpose. Let’s just assume that we will be testing using username hpotter and password test. For the password however, note that Glassfish digest by default is SHA-256, as illustrated by the following screen shot.

sha-256

Hence, you need to encode the password test before insert. You can use encoder by technipixel, which will give you the String 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.

The next step is to write some INSERT statements:

INSERT INTO person (id, password, username) VALUES (1, '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', 'hpotter');
COMMIT;
INSERT INTO person_role (username, user_group) VALUES ('hpotter', 'User');
INSERT INTO person_role (username, user_group) VALUES ('hpotter', 'Admin');
COMMIT;

Let’s move on to the next step.

Securing web application using JAAS

Quite a number of tutorials out there on JAAS with FORM authentication method. However, I thought I put it here again, with the hope that some might find it simpler.

web.xml

Make the following modification to your web.xml

    <welcome-file-list>
        <welcome-file>/index.jsp</welcome-file><!-- 1 -->
    </welcome-file-list>
    <security-constraint><!-- 2 -->
        <display-name>TestConstraint</display-name>
        <web-resource-collection>
            <web-resource-name>TestResource</web-resource-name>
            <description/>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <description/>
            <role-name>User</role-name>
            <role-name>Admin</role-name>
        </auth-constraint>
    </security-constraint>
    <login-config><!-- 3 -->
        <auth-method>FORM</auth-method>
        <realm-name>testRealm</realm-name>
        <form-login-config>
            <form-login-page>/login.html</form-login-page>
            <form-error-page>/error.html</form-error-page>
        </form-login-config>
    </login-config>
    <security-role><!-- 4 -->
        <description/>
        <role-name>User</role-name>
    </security-role>
    <security-role><!-- 5 -->
        <description/>
        <role-name>Admin</role-name>
    </security-role>

Let’s go through them one-by-one.

  1. This is the file to be displayed on successful login. You can also use this file as redirection. For example, suppose you have a file called index.xhtml (a JSF page), you can use response.sendRedirect("index.jsf");
  2. This is the actual constraint, i.e. how you secure the application. This section is basically there to secure all access to the application, denoted by /* url pattern, and only allow access of user with the role of User and Admin.
  3. This part denotes that what we are using is FORM authentication method (which I’ll explain in more detail in the next section). Important part is to ensure the correct name of the security realm used, which in this case, testRealm, the same realm name we have given when setting it up via Glassfish admin page. The other part is to set the page containing j_security_check that the application will automatically redirect to in the event of request access has not been authenticated yet.
  4. The known role
  5. Same as previous section.

glassfish-web.xml

We also need to configure glassfish-web.xml so that the container knows about the mapping between groups from the database and the role recognised by the application.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
    <security-role-mapping>
        <role-name>Admin</role-name>
        <group-name>Admin</group-name>
    </security-role-mapping>
    <security-role-mapping>
        <role-name>User</role-name>
        <group-name>User</group-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>
</glassfish-web-app>

N.B. If you use Netbeans, this file might be generated for you.

Login page: login.html

If we refer again to web.xml above, notice that the login page is pointing to login.html. For FORM authentication method, as per specification, we need to have a form with j_security_check, with j_username and j_password (Oracle 2013).

<!DOCTYPE html>
<html>
    <body>

        <form action="j_security_check" method="post">
            <p>
                <strong>Username</strong>
                <input type="text" name="j_username" size="25" />
            </p>
            <p>
                <strong>Password</strong>
                <input type="password" size="15" name="j_password" />
            </p>
            <p>
                <input type="submit" value="Submit" />
                <input type="reset" value="Reset" />
            </p>
        </form>
    </body>
</html>

With all of these completed, we can start up Glassfish, deploy our application and test it using any browser. Upon accessing the application, users should be directed to login.html to login. Remember to use hpotter as username and test as password. On successful login, the user should be redirected to index.jsp (which in turn redirects user to index.jsf, or anything the index.jsp is redirecting to, according to your requirement).

Create a RESTFUL end point

The next step of course is to create a RESTFUL end point, which is very simple. One of the posts I have written here might also be useful.

To start, assuming we have the following application path.

package com.dwuysan;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

/**
 * @author denywuy
 */
@ApplicationPath(value = "resources")
public class ApplicationConfig extends Application {
}

Let us assume that we have the following simple RESTFUL service.

package com.dwuysan;

import com.dwuysan.entity.Outlet;
import com.dwuysan.service.OutletService;
import javax.annotation.ManagedBean;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;

@Path(value = "generic")
@RolesAllowed(value = "User")
@ManagedBean
public class GenericResource {

    @Inject
    private OutletService outletService;

    @GET
    @Path("{id}")
    public Outlet get(@PathParam(value = "id") final long id) {
        return this.outletService.getOutlet(id);
    }
}

Note that we have secured this service with javax.annotation.security.RolesAllowed annotation.

Testing secured RESTFUL service using curl

Given the RESTFUL service we created above, we should be able to test it using CURL with the following command:

curl -X GET -H "Accept:application/json" -H "Content-Type:application/json" http://localhost:8080/testApp/resources/generic/101

The above command translate to the following: hit the URL above using GET, with the headers Accept:application/json and Content-Type:application/json (cURL 2013)

Since we have secured our application, invocation as above will not work. The user will be redirected to login.html. Hence, our aim now is to first login. With cURL, we can submit parameters to login, i.e. username and password, and then obtain the cookie. To do that, we can use this command:

curl -b cookies.txt -c cookies.txt -d "j_username=hpotter&j_password=test" http://localhost:8080/testApp/j_security_check

This command is submitting username and password, to the j_security_check (remember our login.html that we have created previously), and storing the cookies obtain under the file cookies.txt.

If you open your cookies.txt, you may see the following:

# Netscape HTTP Cookie File
# http://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost     FALSE   /testApp     FALSE   0       JSESSIONID      245a317ab91fbb28244403346770

N.B. You might receive Document Moved response. This means login has been successful. Otherwise, you would get the raw html of the error.html again.

Once we have been authenticated successfully, we can use the cookies we have obtained from the login to invoke the RESTFUL service.

curl -X GET -H "Accept:application/json" -H "Content-Type:application/json" -b cookies.txt -c cookies.txt http://localhost:8080/testApp/resources/generic/101

References:

BalusC, 2012, ‘Does JSF support form based security’, accessed 12 February 2013.

Oracle, 2013, ‘Securing Web Application’, accessed 12 February 2013.

Wolff, N, 2005, ‘How do you handle authentication via cookie with CURL?’, accessed 12 February 2013.

Injecting EJB into a JAX-RS

Following my previous post here, I tried to inject an EJB 3.1 stateless session bean into this resource, only to find the infamous java.lang.NullPointerException.

Supposed you have an EJB as follows:

MainService

package com.wordpress.dwuysan.service;
import javax.ejb.Stateless;
import javax.ejb.LocalBean;

@Stateless
@LocalBean
public class MainService {

    public String sayHello(final String name) {
        return "Hello" + name;
    }
}

And injecting it in a JAX-RS bean:

GenericResource

package com.wordpress.dwuysan;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path(value = "generic")
public class GenericResource {

    @Inject
    private com.wordpress.dwuysan.service.MainService mainService;

    @GET
    public String get() {
        return this.mainService.sayHello("test");
    }
}

This will result in java.lang.NullPointerException.

To fix this, the JAX-RS bean must also be “managed”, so simply add the annotation javax.annotation.ManagedBean to your JAX-RS bean (Nelson 2011)

References:

Nelson, L, 2011, ‘How do I inject a local stateless session bean into a JAX-RS resource using CDI?’, accessed 06 December 2012.

Starting with JAX-RS: Configuring javax.ws.rs.ApplicationPath

I recently started with a very simple Hello-World-like JAX-RS. So, to start with, I created a class like this:

GenericResource

package com.wordpress.dwuysan;

import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path(value = "generic")
public class GenericResource {

    @GET
    public String get() {
        return "Hello World";
    }
}

Once the application server is running, when I run curl http://<web application address>/generic, it failed.

After a little bit of Google-ing around, I found that you need to set up the resource path, either by installing a correct servlet on web.xml, or if you are fortunate enough and are using Java EE 6, you can use @javax.ws.rs.ApplicationPath and extends javax.ws.rs.core.Application (Kuchtiak 2009).

So, once I add this class:

ApplicationConfig

package com.wordpress.dwuysan;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath(value = "resources")
public class ApplicationConfig extends Application {
}

I can now invoke curl http://<web application address>/resources/generic, I got correct response.

References:

Kuchtiak, M, 2009, 'How to Combine REST Services with EJB 3.1', accessed 06 December 2012.