Posts Tagged ‘Security

06
Mar
16

JAX-RS authentication and authorization

This page describes how to configure a (JAX-RS application) using (custom login modules) and (authorization based annotation) on JBoss 7.

  1. JAX-RS is a language API that provides support in creating web services according to the REST architectural pattern in Java.
  2. Custom login module will act like an adapter to allow you to integrate with your authentication and authorization API’s. This will allow your applications to use existing roles defined in your organization.
  3. Authorization based annotations like (@RolesAllowed, @DenyAll, @PermitAll, and @RunAs) make it easy to specify permissions at resource level. These permissions allows developers to specify who is allowed to interact with the REST resource. This can be done at the individual method level so you can restrict HTTP methods like @GET, @PUT, @DELETE etc… to only users allowed to perform those operations.
Full downloadable source for this page is available here. Corrections and enhancements are welcome, fork, change and push back to GitHub.

Software Stack

  • Java 8
  • Jboss EAP 6.4 – should work other Jboss servers as well
  • JAVA EE 6 / JAX-RS 1.1

Procedure

Most of the configuration will be done inside the WAR file however there is ONE THING that needs to be configured on the server. A new security domain needs to be defined for the custom login module. There is no way around this. This must be done.

[JBOSS BASE DIR]/standalone/configuration/standalone.xml

find the security-domains section and add the following domain.

<security-domain name="custom-auth" cache-type="default">
	<authentication>
    <login-module code="com.test.MySimpleUsernamePasswordLoginModule" flag="required"/>
    </authentication>
</security-domain>

Login Module

The following is the custom login module. This is just one example on how to extend UsernamePasswordLoginModule. Other base classes are available to extend (AnonLoginModule, DatabaseServerLoginModule, LdapExtLoginModule, LdapLoginModule, SimpleServerLoginModule, UsersLoginModule, UsersRolesLoginModule, XMLLoginModule). See the JBoss documentation.

src/main/java/com/test/MySimpleUsernamePasswordLoginModule.java

package com.test;

import java.security.acl.Group;
import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;

import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.jboss.security.auth.spi.UsernamePasswordLoginModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The simples username and password based login module possible,
 * extending JBoss' {@link UsernamePasswordLoginModule}.
 */
public class MySimpleUsernamePasswordLoginModule extends UsernamePasswordLoginModule {
	private static final Logger logger = LoggerFactory.getLogger(MySimpleUsernamePasswordLoginModule.class);

    @SuppressWarnings("rawtypes")
    public void initialize(Subject subject, CallbackHandler callbackHandler,
            Map sharedState,
            Map options) {
        super.initialize(subject, callbackHandler, sharedState, options);
    }

    /**
     * (required) The UsernamePasswordLoginModule modules compares the result of this
     * method with the actual password.
     */
    @Override
    protected String getUsersPassword() throws LoginException {
        System.out.format("MyLoginModule: authenticating user '%s'\n", getUsername());
        // Lets pretend we got the password from somewhere and that it's, by a chance, same as the username
        String password = super.getUsername();
        // Let's also pretend that we haven't got it in plain text but encrypted
        // (the encryption being very simple, namely capitalization)
        password = password.toUpperCase();
        return password;
    }

    /**
     * (optional) Override if you want to change how the password are compared or
     * if you need to perform some conversion on them.
     */
    @Override
    protected boolean validatePassword(String inputPassword, String expectedPassword) {
        // Let's encrypt the password typed by the user in the same way as the stored password
        // so that they can be compared for equality.
        String encryptedInputPassword = (inputPassword == null)? null : inputPassword.toUpperCase();
        System.out.format("Validating that (encrypted) input psw '%s' equals to (encrypted) '%s'\n"
                , encryptedInputPassword, expectedPassword);
        return super.validatePassword(encryptedInputPassword, expectedPassword);
    }

    /**
     * (required) The groups of the user, there must be at least one group called
     * "Roles" (though it likely can be empty) containing the roles the user has.
     */
    @Override
    protected Group[] getRoleSets() throws LoginException {
    	String user = getIdentity().getName();
    	logger.info("getting roles for user: " + user);
        SimpleGroup group = new SimpleGroup("Roles");
        try {
            group.addMember(new SimplePrincipal("user_role"));
            if("admin".equalsIgnoreCase(user)) 
            	group.addMember(new SimplePrincipal("admin_role"));
        } catch (Exception e) {
            throw new LoginException("Failed to create group member for " + group);
        }
        return new Group[] { group };
    }

}

REST Resource

Each method is annotated with the roles that are allowed to execute it.

src/main/java/com/test/CompanyResource.java

package com.test;

import java.util.List;

import javax.annotation.PostConstruct;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Path("companies")
@Produces("application/json")
public class CompanyResource {
	private static final Logger logger = LoggerFactory.getLogger(CompanyResource.class);

	@PostConstruct
	public void postConstruct() {
	}
	
	@GET
	@Path("{id}")
	@RolesAllowed({"user_role", "admin_role"})
	public Object getCompany(@PathParam("id")  Long id) {
		logger.info("getCompany called");
		return null;
	}

	@POST
	@RolesAllowed({"admin_role"})
	public Object create() {
		logger.info("create called");
		return null;
	}
	
	@GET
	@RolesAllowed({"user_role","admin_role"})
	public List<Object> getAll(@Context SecurityContext sec) {
		logger.info("getAll called");		
		// the following is just a demo of a programmatic security check.
		logger.info("Id: " + sec.getUserPrincipal().getName());
		String role = "user_role";
		logger.info("User is in role "+role+" : " + sec.isUserInRole(role));
		role = "admin_role";
		logger.info("User is in role "+role+" : " + sec.isUserInRole(role));
		return null;
	}

	@PUT
	@RolesAllowed({"admin_role"})
	public Object update(Object selectedCompany) {
		logger.info("update called");
		return null;
	}

	@DELETE
	@Path("{id}")
	@RolesAllowed({"admin_role"})
	public void delete(@PathParam("id")  Long id) {
		logger.info("delete called");
	}

}

REST Application Config

src/main/java/com/test/WarehouseApplication.java

package com.test;

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

@ApplicationPath("/services")
public class WarehouseApplication extends Application {}

Configuration files

src/main/resources/log4j.xml

<!DOCTYPE log4j:configuration PUBLIC
  "-//APACHE//DTD LOG4J 1.2//EN" "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
   
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    
	<appender name="file" class="org.apache.log4j.FileAppender">
	   <param name="maxFileSize" value="1024KB" />
	   <param name="file" value="${user.home}/logs/jboss-custom-login.log" />
	   <layout class="org.apache.log4j.PatternLayout">
		<param name="ConversionPattern" 
			value="%d %-5p %c:%L - %m%n" />
	   </layout>
	</appender>
	    
    <logger name="com.test">
        <level value="DEBUG"/>
    </logger>
    <logger name="com.springframework">
        <level value="INFO"/>
    </logger>
    <logger name="org.hibernate.SQL">
        <level value="DEBUG"/>
    </logger>
    <root>
        <level value="ERROR"/>
        <appender-ref ref="file"/>
    </root> 
</log4j:configuration>

src/main/webapp/WEB-INF/jboss-web.xml

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
   <security-domain>custom-auth</security-domain>
   <disable-audit>true</disable-audit>
</jboss-web>

src/main/webapp/WEB-INF/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"
	id="WebApp_ID" version="3.0">

	<!-- The following is required to get JBOSS JAX-RS to detect and use @RolesAllowed -->	
	<context-param>
	    <param-name>resteasy.role.based.security</param-name>
	    <param-value>true</param-value>
	</context-param>	

	<security-constraint> <!-- TODO: try to specify all this via annotations.  -->
		<web-resource-collection>
			<web-resource-name>JBoss Application</web-resource-name>
			<url-pattern>/*</url-pattern>
		</web-resource-collection>
		<auth-constraint> <!-- list of roles allowed to use web application -->
			<role-name>user_role</role-name>
			<role-name>admin_role</role-name>
		</auth-constraint>
	</security-constraint>

	<!-- Define the Login Configuration for this Application -->
	<login-config>
		<auth-method>BASIC</auth-method>
		<realm-name>JBoss Application</realm-name>
	</login-config>

	<!-- Security roles referenced by this web application -->
	<security-role>
		<description>The role a user needs to be allowed to log in to the application</description>
		<role-name>user_role</role-name>
	</security-role>
	<security-role>
		<description>Admin Role</description>
		<role-name>admin_role</role-name>
	</security-role>
</web-app>

pom.xml

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.test</groupId>
    <artifactId>jboss-custom-login</artifactId>
    <packaging>war</packaging>
    <version>20160306</version>
    <name>jboss-custom-login Maven Webapp</name>
    <url>http://maven.apache.org</url>
	<pluginRepositories>
		<pluginRepository> <!-- Ignore this repository. Its only used for document publication. -->
			<id>numberformat-releases</id>
			<url>https://raw.githubusercontent.com/numberformat/wordpress/master/20130213/repo/</url>
		</pluginRepository>
	</pluginRepositories>    
    <dependencies>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.1</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>1.7.1</version>
		</dependency>
    
        <dependency>
            <groupId>jboss</groupId>
            <artifactId>jbosssx</artifactId>
            <version>3.2.3</version>
            <scope>provided</scope>
        </dependency>

		<dependency>
			<groupId>javax</groupId>
			<artifactId>javaee-web-api</artifactId>
			<version>6.0</version>
			<scope>provided</scope>
		</dependency>
        
    </dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<plugin> <!-- Ignore this plugin. Its only used for document publication. -->
				<groupId>github.numberformat</groupId>
				<artifactId>blog-plugin</artifactId>
				<version>1.0-SNAPSHOT</version>
				<configuration>
					<gitUrl>https://github.com/numberformat/wordpress/tree/master/${project.version}/${project.artifactId}</gitUrl>
				</configuration>
				<executions>
					<execution>
						<id>1</id>
						<phase>site</phase>
						<goals>
							<goal>generate</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>    
    
</project>

Test the application

Install the application into JBoss and navigate to the application using a browser testing tool such as Postman:

url: http://localhost:8080/jboss-custom-login/services/companies

The application uses BASIC auth so you may specify the following ids to login:

user/user – can only use read-only methods like [getAll, getCompany]
admin/admin – can use any method

Check the log4j.xml config file to see where the logs are being written.




Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Join 78 other followers

July 2017
S M T W T F S
« Mar    
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

Blog Stats

  • 822,357 hits