When building a system, developers usually disregard the security aspects. Security has been always something very important to worry about, but it’s attracting even higher concerns than before. Just this year we had a few cases like the Heartbleed Bug or the CelebrityGate scandal. This has nothing to do with the post, but are just examples that security really matters and we should be aware of it.
With the increasing popularity of REST services it makes sense that these need to be secured in some way. A couple of weeks ago, I had to integrate my client with a REST service behind https. I have never done it before and that’s the reason for this post. I have to confess that I’m no security expert myself, so please correct me if I write anything stupid.
The Setup
For this example I have used the following setup:
I’m not going into many details about SSL and TSL, so please check here for additional content. Note that TLS is the new name for SSL evolution. Sometimes there is confusion between the two and people often say SSL, but use the newest version of TSL. Keep that in mind.
Don’t forget to follow the instructions on the following page to setup SSL for Tomcat: SSL Configuration HOW-TO. This is needed for the server to present the client with a set of credentials, a Certificate, to secure the connection between server and client.
The Code
Service
Let’s create a simple Spring REST Service:
RestService.java
| @Controller @RequestMapping("/") public class RestService { @RequestMapping(method = RequestMethod.GET) @ResponseBody public String get() { return "Called the get Rest Service"; } } |
And we also need some wiring for this to work:
RestConfig.java
| @Configuration @EnableWebMvc @ComponentScan(basePackages = "com.radcortez.rest.ssl") public class RestConfig {} |
web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <?xml version="1.0" encoding="UTF-8"?> <web-app version="3.1" 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/web-app_3_1.xsd"> <servlet> <servlet-name>rest</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextClass</param-name> <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value> </init-param> <init-param> <param-name>contextConfigLocation</param-name> <param-value>com.radcortez.rest.ssl</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>rest</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <security-constraint> <web-resource-collection> <web-resource-name>Rest Application</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <user-data-constraint> <!-- Needed for our application to respond to https requests --> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint> </web-app> |
Please, note the elements security-constraint
, user-data-constraint
and <transport-guarantee>CONFIDENTIAL</transport-guarantee>
. These are needed to specify that the application requires a secure connection. Check Securing Web Applications for Java Applications.
Running the Service
Just deploy the application on the TomEE Server using your favourite IDE environment and access https://localhost:8443/
. You should get the following (you might need to accept the server certificate first):
Note that the browser protocol is https
and the port is 8443
(assuming that you kept the default settings in SSL Configuration HOW-TO.
Client
Now, if you try to call this REST service with a Java client, most likely you are going to get the following message and Exception (or similar):
Message: I/O error on GET request for “https://localhost:8443/”:sun.security.validator.ValidatorException:
Exception: Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
This happens because the running JDK does not have a valid certificate for your server. You can import it, and get rid of the problem, but let’s do something more interesting. We are going to programatically supply a trusted keystore with our server certificate.
This is especially useful if:
- you are running your code into multiple environments
- you don’t have to manually import the certificate into the JDK every time
- if you upgrade the JDK you have to remember about the certificates
- for some odd reason you don’t have access to the JDK itself to import the certificate
Let’s write some code:
RestClientConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | @Configuration @PropertySource("classpath:config.properties") public class RestClientConfig { @Bean public RestOperations restOperations(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception { return new RestTemplate(clientHttpRequestFactory); } @Bean public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) { return new HttpComponentsClientHttpRequestFactory(httpClient); } @Bean public HttpClient httpClient(@Value("${keystore.file}") String file, @Value("${keystore.pass}") String password) throws Exception { KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); FileInputStream instream = new FileInputStream(new File(file)); try { trustStore.load(instream, password.toCharArray()); } finally { instream.close(); } SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()).build(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1.2"}, null, BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); return HttpClients.custom().setSSLSocketFactory(sslsf).build(); } @Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } } |
Here we use Spring RestOperations interface which specified a basic set of RESTful operations. Next we use Apache HTTP Components SSLConnectionSocketFactory which gives us the ability to validate the identity of the server against a list of trusted certificates. The certificate is loaded from the same file used on the server by KeyStore.
RestServiceClientIT.java
| @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = RestClientConfig.class) public class RestServiceClientIT { @Autowired private RestOperations rest; @Test public void testRestRequest() throws Exception { ResponseEntity<String> response = rest.getForEntity("https://localhost:8443/", String.class); System.out.println("response = " + response); System.out.println("response.getBody() = " + response.getBody()); } } |
A simple test class. We also need a properties file with the keystore file location and password:
config.properties
| keystore.file=${user.home}/.keystore keystore.pass=changeit |
This should work fine if you used all the defaults.
Running the Test
If you now run the test which invokes the REST service within a Java client, you should get the following output:
Response: <200 OK,Called the get Rest Service,{Server=[Apache-Coyote/1.1], Cache-Control=[private], Expires=[Thu, 01 Jan 1970 01:00:00 WET], Content-Type=[text/plain;charset=ISO-8859-1], Content-Length=[27], Date=[Tue, 23 Dec 2014 01:29:20 GMT]}>
Body: Called the get Rest Service
Conclusion
That’s it! You can now call your REST service with your client in a secured manner. If you prefer to add the certificate to the JDK keystore, please check this post.
Stay tuned for an equivalent for Java EE JAX-RS equivalent.
Resources
You can clone a full working copy from my github repository: REST SSL.
Did you ever had the need to implement your own custom JAAS Principal and LoginModule for you JEE application? There are a couple of reasons for it. I’ve done it in the following cases:
- Authenticate the user using different strategies.
- Have additional user information on the Principal object.
- Share user information between applications using the Principal object.
Maybe you have your own specific reason, it doesn’t matter. Today’s post will guide you on how to do it for Wildfly. There are a few articles on the topic, but each of them deal with a different aspect of the problem. I got motivated to write this post to aggregate all the steps in a single article, including the Arquillian test.
Wildfly uses PicketBox for Java Application Security and already implements some handy classes to take care of the authentication of the user for you. Have a look into UsersRolesLoginModule
, DatabaseServerLoginModule
, LdapUsersLoginModule
, BaseCertLoginModule
and so on. Let’s start by creating a Maven project with the following dependency:
| <dependency> <groupId>org.picketbox</groupId> <artifactId>picketbox</artifactId> <version>4.0.20.Beta2</version> </dependency> |
Next, just create a CustomPrincipal
and a CustomLoginModule
classes:
| public class CustomPrincipal extends SimplePrincipal { private String description; public CustomPrincipal(String name, String description) { super(name); this.description = description; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } } |
Note that we’re extending the org.jboss.security.SimplePrincipal
present in PicketBox, but you can also implement java.security.Principal
instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class CustomLoginModule extends UsersRolesLoginModule { private CustomPrincipal principal; @Override public boolean login() throws LoginException { boolean login = super.login(); if (login) { principal = new CustomPrincipal(getUsername(), "An user description!"); } return login; } @Override protected Principal getIdentity() { return principal != null ? principal : super.getIdentity(); } } |
Here again we’re extending a PicketBox class, org.jboss.security.auth.spi.UsersRolesLoginModule
. You can code your own login module by implementing javax.security.auth.spi.LoginModule
, but I recommend to extend one of the PicketBox classes, since they already have a lot of the behaviour that you will need. org.jboss.security.auth.spi.UsersRolesLoginModule
is a very simple login module that authenticates an user by matching his login and password to a file. You shouldn’t use it for production applications, but it’s very handy for prototypes.
CustomLoginModule
is also overriding two methods. These are needed to access our CustomPrincipal
in a JEE application. The login()
method as the name says is called when the user is performing the login action, so in here we create our CustomPrincipal
object. On the other hand, the getIdentity()
method is called to return the Principal that corresponds to the user primary identity, so we return our own instance if the login was successful.
Ok, great. How do we test it now? We use Arquillian, JUnit and HttpUnit. Start by adding the needed Maven dependencies (these are all the project dependencies):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | <dependencyManagement> <dependencies> <dependency> <groupId>org.jboss.arquillian</groupId> <artifactId>arquillian-bom</artifactId> <version>${arquillian.version}</version> <scope>import</scope> <type>pom</type> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.picketbox</groupId> <artifactId>picketbox</artifactId> <version>4.0.20.Beta2</version> </dependency> <dependency> <groupId>org.wildfly</groupId> <artifactId>wildfly-arquillian-container-remote</artifactId> <version>${wildfly.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.arquillian.junit</groupId> <artifactId>arquillian-junit-container</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>httpunit</groupId> <artifactId>httpunit</artifactId> <version>1.7</version> <scope>test</scope> </dependency> <dependency> <groupId>net.sourceforge.htmlunit</groupId> <artifactId>htmlunit</artifactId> <version>2.13</version> <scope>test</scope> </dependency> <dependency> <groupId>rhino</groupId> <artifactId>js</artifactId> <version>1.7R1</version> <scope>test</scope> </dependency> </dependencies> |
Note that we also included the javaee-api 7
dependency. Next, we’re creating a simple EJB to access our CustomPrincipal
and a Servlet to perform the authentication:
| @Stateless public class SampleEJB { @Resource private EJBContext ejbContext; @RolesAllowed("user") public String getPrincipalName() { return ejbContext.getCallerPrincipal().getName(); } } |
Now the Servlet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | @WebServlet(urlPatterns = {"/LoginServlet"}) public class LoginServlet extends HttpServlet { @Inject private SampleEJB sampleEJB; @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } private void processRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { try { String username = request.getParameter("username"); String password = request.getParameter("password"); if (username != null && password != null) { request.login(username, password); } CustomPrincipal principal = (CustomPrincipal) request.getUserPrincipal(); response.getWriter().println("principal=" + request.getUserPrincipal().getClass().getSimpleName()); response.getWriter().println("username=" + sampleEJB.getPrincipalName()); response.getWriter().println("description=" + principal.getDescription()); } catch (ServletException e) { response.sendError(HttpServletResponse.SC_FORBIDDEN); } } } |
I think the code is self-explanatory, but we still need to wire everything together. The way we configure our login module to be used on Wildfly and on our application is by using Security Domains. You can add a Security Domain by hand using the standalone/configuration/standalone.xml
and domain/configuration/domain.xml
files on the Wildfly installation folder, but we’re going to do something more interesting.
Using the Command Line Interface (CLI), is really easy to make changes to the server configuration without modifying any XML. This also allows you to setup a test environment and clean up your changes in the end. To achieve that, create the following files:
jboss-add-login-module.cli
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | connect /subsystem=security/security-domain=CustomSecurityDomain:add(cache-type=default) reload /subsystem=security/security-domain=CustomSecurityDomain/authentication=classic: \ add( \ login-modules=[{ \ "code"=>"com.cortez.wildfly.security.CustomLoginModule", \ "flag"=>"required", \ "module-options"=>[ \ ("usersProperties"=>"user.properties"), \ ("rolesProperties"=>"roles.properties")] \ }]) reload |
As you probably have guessed, jboss-add-login-module.cli
contains the CLI commands to add our Security Domain to the Wildfly instance. We first add the Security Domain and then assign the login modules to the domain. Both commands should be able to be executed as a single command, but for some reason I was getting an error so I had to split them apart. The configuration is not available on the server unless you perform a reload
command. For the login module, please note the FQN of our CustomLoginModule
associated with the configuration. That’s how we wire the custom login module to the Security Domain. The configuration also references two other files: user.properties
and roles.properties
that are used to perform the user credential verification and load the user roles. Here are examples for both files:
user.properties
Define all valid usernames and their corresponding passwords.
roles.properties
Define the sets of roles for valid usernames.
We still need the CLI commands to remove the Security Domain at the cleanup phase:
jboss-remove-login-module.cli
| connect /subsystem=security/security-domain=CustomSecurityDomain:remove reload |
Almost done? Not yet! We still need to associate our Security Domain to our JEE application so our custom code runs when we perform authentication or execute Principal related behaviour. We need the following files now:
jboss-web.xml
| <?xml version="1.0" encoding="UTF-8"?> <jboss-web> <security-domain>CustomSecurityDomain</security-domain> </jboss-web> |
The previous file sets the Security Domain into Servlets.
jboss-ejb3.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?xml version="1.0" encoding="UTF-8"?> <jboss:jboss xmlns="http://java.sun.com/xml/ns/javaee" xmlns:jboss="http://www.jboss.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:s="urn:security:1.1" version="3.1" impl-version="2.0"> <assembly-descriptor> <s:security> <!-- Even wildcard * is supported --> <ejb-name>SampleEJB</ejb-name> <!-- Name of the security domain which is configured in the EJB3 subsystem --> <s:security-domain>CustomSecurityDomain</s:security-domain> </s:security> </assembly-descriptor> </jboss:jboss> |
This file sets the Security Domain in EJB’s.
Uff! Now we’re finally ready to see some action! We required a bit of setup to have this example working! Here is the test class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | @RunWith(Arquillian.class) public class CustomLoginModuleTest { @ArquillianResource private URL deployUrl; @AfterClass public static void removeSecurityDomain() { processCliFile(new File("src/test/resources/jboss-remove-login-module.cli")); } @Deployment(testable = false) public static WebArchive createDeployment() { processCliFile(new File("src/test/resources/jboss-add-login-module.cli")); WebArchive war = ShrinkWrap.create(WebArchive.class) .addClass(CustomPrincipal.class) .addClass(CustomLoginModule.class) .addClass(SampleEJB.class) .addClass(LoginServlet.class) .addAsWebInfResource("jboss-web.xml") .addAsWebInfResource("jboss-ejb3.xml") .addAsResource("user.properties") .addAsResource("roles.properties"); System.out.println(war.toString(true)); return war; } @Test public void testLogin() throws IOException, SAXException { WebConversation webConversation = new WebConversation(); GetMethodWebRequest request = new GetMethodWebRequest(deployUrl + "LoginServlet"); request.setParameter("username", "username"); request.setParameter("password", "password"); WebResponse response = webConversation.getResponse(request); assertTrue(response.getText().contains("principal=" + CustomPrincipal.class.getSimpleName())); assertTrue(response.getText().contains("username=username")); assertTrue(response.getText().contains("description=An user description!")); System.out.println(response.getText()); } private static void processCliFile(File file) { CLI cli = CLI.newInstance(); cli.connect("localhost", 9990, null, null); CommandContext commandContext = cli.getCommandContext(); BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(file)); String line = reader.readLine(); while (commandContext.getExitCode() == 0 && !commandContext.isTerminated() && line != null) { commandContext.handleSafe(line.trim()); line = reader.readLine(); } } catch (Throwable e) { throw new IllegalStateException("Failed to process file '" + file.getAbsolutePath() + "'", e); } finally { StreamUtils.safeClose(reader); } } } |
That’s it! Now your servlet login
method will authenticate using your custom login module and methods like getUserPrincipal()
from the servlet request or getCallerPrincipal()
from the EJBContext will return the CustomPrincipal
instance.
Fire up a Wildfly instance and run the test using mvm test
or just use your favourite IDE.
A few problems:
- It should be possible to add the security domain using only one single CLI command, but for some reason I was getting an erro. I need to have a better look into this.
- I couldn’t find a way to run code before and after the Arquillian deployment, so the code to add the Security Domain is inside the deployment method. I’ll try to find a way to do it.
If you want additional information, please check the following references:
If you’re too lazy to write the code on your own or just want a working sample, you can download it here. Enjoy!