REST Jersey2 JSON JWT Authentication Authorization

This tutorial explains how to create a Java REST Web Service with Jersey2, JSON communication, JSON Web Token authentication and role authorization using annotations and request filters. Passwords are hashed with PKDF2 and salted with HMAC SHA1. The provided code is working with two tested databases, OrientDB and SQLite. The data access layer uses the DAO (Data Access Object) pattern, in order to separate business logic from the database layer. This tutorial is a rather long one, since there are a lot of open topics to go into and explain. We hope you got some time.

The code is available at Github.

1. Prerequisites

  1. Maven
  2. Tomcat (we use 8.5 but any Version starting with 7 should be working – Java8)
  3. Curl or equivalent (we use Postman to query the rest service)

2. Project setup

We created a maven-archetype-webapp project in Eclipse which provides the project structure we need. You will get the same result when executing mvn install. This generates a WAR file that can be deployed on a Tomcat Server.

The key dependencies for the REST web service are:

  • Jersey2 (Glassfish)
  • (Jackson) JAX-RS
  • Jose4j JWT implementation
<!-- JAX-RS -->
<dependency>
	<groupId>javax.ws.rs</groupId>
	<artifactId>javax.ws.rs-api</artifactId>
	<version>${jaxrs.version}</version>
</dependency>
<!-- JERSEY 2 -->
<dependency>
	<groupId>org.glassfish.jersey.containers</groupId>
	<artifactId>jersey-container-servlet</artifactId>
	<version>${jersey2.version}</version>
</dependency>
<dependency>
	<groupId>org.glassfish.jersey.containers</groupId>
	<artifactId>jersey-container-servlet-core</artifactId>
	<version>${jersey2.version}</version>
</dependency>
<dependency>
	<groupId>org.glassfish.jersey.media</groupId>
	<artifactId>jersey-media-json-jackson</artifactId>
	<version>${jersey2.version}</version>
</dependency>
<!-- Jackson -->
<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-core</artifactId>
	<version>${jackson.version}</version>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.jaxrs</groupId>
	<artifactId>jackson-jaxrs-json-provider</artifactId>
	<version>${jackson.version}</version>
</dependency>
<!-- JWT -->
<dependency>
	<groupId>org.bitbucket.b_c</groupId>
	<artifactId>jose4j</artifactId>
	<version>${jose4jJWT.version}</version>
</dependency>

The architecture of the web application looks like this:

REST app architecture

We use JSON-based communication between the client and the server. All requests are filtered before they reach the REST API in order to check for authentication (JWT) and authorization (roles like Admin, User etc.).

The Business Logic layer does not exist, we access the Data Access Layer directly from the REST API for simplification.

Finally we provide two implementations as persistent storage. Multi-model store OrientDB and SQLlite. Both databases have in-memory and file persistence, so you do not have to install extra software.

3. Register Authentication Filter

Let us start from top to bottom. Requests from clients are filtered via an Authentication filter. We register a ResourceConfig class in the web.xml to apply our filter later on and specify our REST Web Service class.

<?xml version="1.0" encoding="UTF-8"?>
<web-app 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"
         version="3.1">

	<display-name>rest-jersey2-json-jwt</display-name>

	<welcome-file-list>
    	<welcome-file>index.html</welcome-file>
	</welcome-file-list>

	<servlet>
		<servlet-name>rest-jersey2-json-jwt</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
             <param-name>jersey.config.server.provider.packages</param-name>
			<param-value>com.tutorialacademy.rest.restapi</param-value>
		</init-param>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>com.tutorialacademy.rest.restapi.RestApplicationConfig</param-value>
        </init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>rest-jersey2-json-jwt</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
</web-app>

The ResourceConfig registers our Authentication filter:

public class RestApplicationConfig extends ResourceConfig {
	
	public RestApplicationConfig() {
        packages( "com.tutorialacademy.rest.filter" );
		register( AuthenticationFilter.class );
	}
}

The Authentication filter implementation:

@Override
public void filter( ContainerRequestContext requestContext )
{
    Method method = resourceInfo.getResourceMethod();
    // everybody can access (e.g. user/create or user/authenticate)
    if( !method.isAnnotationPresent( PermitAll.class ) )
    {
        // nobody can access
        if( method.isAnnotationPresent( DenyAll.class ) ) 
        {
            requestContext.abortWith( 
            	ResponseBuilder.createResponse( Response.Status.FORBIDDEN, ACCESS_FORBIDDEN )
            );
            return;
        }
          
        // get request headers to extract jwt token
        final MultivaluedMap<String, String> headers = requestContext.getHeaders();
        final List<String> authProperty = headers.get( AUTHORIZATION_PROPERTY );
          
        // block access if no authorization information is provided
        if( authProperty == null || authProperty.isEmpty() )
        {
        	logger.warn("No token provided!");
            requestContext.abortWith( 
                	ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED, ACCESS_DENIED )
            );
            return;
        }
          
        String id = null ;
        String jwt = authProperty.get(0);
        
		// try to decode the jwt - deny access if no valid token provided
		try {
			id = TokenSecurity.validateJwtToken( jwt );
		} catch ( InvalidJwtException e ) {
			logger.warn("Invalid token provided!");
            requestContext.abortWith( 
                	ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED, ACCESS_INVALID_TOKEN )
            );
            return;
		}
        
        // check if token matches an user token (set in user/authenticate)
        UserDAO userDao = UserDAOFactory.getUserDAO();
        User user = null;
        try {
        	user = userDao.getUser( id );
        }
        catch ( UserNotFoundException e ) {
        	logger.warn("Token missmatch!");
            requestContext.abortWith( 
                	ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED, ACCESS_DENIED )
            );
        	return;
        }
        
        UserSecurity userSecurity = userDao.getUserAuthentication( user.getId() );
        
        // token does not match with token stored in database - enforce re authentication
        if( !userSecurity.getToken().equals( jwt ) ) {
        	logger.warn("Token expired!");
            requestContext.abortWith( 
                	ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED, ACCESS_REFRESH )
            );
            return;
        }
        
        // verify user access from provided roles ("admin", "user", "guest")
        if( method.isAnnotationPresent( RolesAllowed.class ) )
        {
        	// get annotated roles
            RolesAllowed rolesAnnotation = method.getAnnotation( RolesAllowed.class );
            Set<String> rolesSet = new HashSet<String>( Arrays.asList( rolesAnnotation.value() ) );
              
            // user valid?
            if( !isUserAllowed( userSecurity.getRole(), rolesSet ) )
            {
            	logger.warn("User does not have the rights to acces this resource!");
                requestContext.abortWith( 
                    	ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED, ACCESS_DENIED )
                );
                return;
            }
        }
        
        // set header param email for user identification in rest service - do not decode jwt twice in rest services
        List<String> idList = new ArrayList<String>();
        idList.add( id );
        headers.put( HEADER_PROPERTY_ID, idList );
    }
}

This method from the Authentication filter is called on every request to our server.

  1. We start of with checking access permissions for the required REST service via annotation (Line 6):
    • @PermitAll – e.g. everybody can access, like register or login / authenticate
    • @DenyAll – no one can use that method
    • @RolesAllowed({“admin”,”user”}) – only users with roles admin or user can call this method
  2. If the method annotation is different from @PermitAll, which means we do not have to check for authentication nor authorization, we continue (Line 6)
  3. Check if the called method has a @DenyAll annotation, which means no one has currently access and return a “forbidden” message (Line 9)
  4. Now we have a user calling an accessible function where we need some authentication. We check the JSON Web Token provided in the header (x-access-token) (Line 22)
  5. If we have a x-access-token  provided, we have to decode it and validate it (not expired, issuer etc correct) (Line 36)
  6. If the token is valid, we check if the provided (user) ID information matches a user in the database (Line 49)
  7. We cross check the token with an already available token in the database (Line 62)
  8. Finally we check if the user fulfills the role requirements (role stored in database, role for method in annotation) (Line 71,78)

We only proceed to the REST Web Service if no early abort via requestContext.abortWith(..) was executed.

4. REST Web Service

If we reach the REST Web Service, authentication and authorization are already processed in the authentication filter. This is where we start the interaction with the Business Layer (or in this example directly with the Data Base Layer):

@DeclareRoles({"admin", "user", "guest"})
@Path("/user")
public class UserRestService extends ResourceConfig {
	
	@POST
	@Path("/create")
	@PermitAll
	@Consumes(MediaType.APPLICATION_JSON)
	@Produces(MediaType.APPLICATION_JSON)
	public Response createUser( UserSecurity userSecurity ) {
		UserDAO userDao = UserDAOFactory.getUserDAO();
		
		try {
			try {
				// check if user no registered already
				userDao.getUserIdByEmail( userSecurity.getEmail() );
				throw new UserExistingException( userSecurity.getEmail() );
			}
			catch( UserNotFoundException e ) {
				// standard user role
				userSecurity.setRole("user");
				// store plain password for authentication
				String plainPassword = userSecurity.getPassword();
				// generate password
				userSecurity.setPassword( PasswordSecurity.generateHash( userSecurity.getPassword() ) );
				// create user
				userDao.createUser( userSecurity );
				// authenticate user
				return authenticate( new Credentials( userSecurity.getEmail(), plainPassword ) );
			}
		} 
		catch ( UserExistingException e ) {
			return ResponseBuilder.createResponse( Response.Status.CONFLICT, e.getMessage() );
		}
		catch ( Exception e ) {
			return ResponseBuilder.createResponse( Response.Status.INTERNAL_SERVER_ERROR );
		}
	}
	
	@POST
	@Path("/authenticate")
	@PermitAll
	@Produces("application/json")
	@Consumes("application/json")
	public Response authenticate( Credentials credentials ) {
		UserDAO userDao = UserDAOFactory.getUserDAO();
		
		try {
			String id = userDao.getUserIdByEmail( credentials.getEmail() );
			UserSecurity userSecurity = userDao.getUserAuthentication( id );
			
			if( PasswordSecurity.validatePassword( credentials.getPassword(), userSecurity.getPassword() ) == false ) {
				return ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED );
			}

			// generate a token for the user
			String token = TokenSecurity.generateJwtToken( id );
			
			// write the token to the database
			UserSecurity sec = new UserSecurity( null, token );
			sec.setId( id );
			userDao.setUserAuthentication( sec );
			
			Map<String,Object> map = new HashMap<String,Object>();
			map.put( AuthenticationFilter.AUTHORIZATION_PROPERTY, token );
			
			// Return the token on the response
			return ResponseBuilder.createResponse( Response.Status.OK, map );
		}
		catch( UserNotFoundException e ) {
			return ResponseBuilder.createResponse( Response.Status.NOT_FOUND, e.getMessage() );
		}
		catch( Exception e ) {
			return ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED );
		}
		
	}
	
	@GET
	@Path("/get")
	@RolesAllowed({"admin","user"})
	@Produces("application/json")
	public Response get( @Context HttpHeaders headers ) {
		UserDAO userDao = UserDAOFactory.getUserDAO();
		
		try {
			String id = getId( headers );
			
			// use decoded email from jwt in header
			User user = userDao.getUser( id );
			
			// Return the object
			return ResponseBuilder.createResponse( Response.Status.OK, user );
		}
		catch( UserNotFoundException e ) {
			return ResponseBuilder.createResponse( Response.Status.NOT_FOUND, e.getMessage() );
		}
		catch ( Exception e ) {
			return ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED );
		}
	}
	
	@GET
	@Path("/getAll")
	@RolesAllowed({"admin"}) // only an admin user should be allowed to request all users
	@Produces("application/json")
	public Response getAll( @Context HttpHeaders headers ) {
		UserDAO userDao = UserDAOFactory.getUserDAO();
		
		try {
			List<JsonSerializable> usersJson = new ArrayList<JsonSerializable>();
			usersJson.addAll( (Collection<? extends JsonSerializable>) userDao.getAllUsers() );
			
			// Return the objects
			return ResponseBuilder.createResponse( Response.Status.OK, usersJson );
		}
		catch( UserNotFoundException e ) {
			return ResponseBuilder.createResponse( Response.Status.NOT_FOUND, e.getMessage() );
		}
		catch ( Exception e ) {
			return ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED );
		}
		
	}
	
	@POST
	@Path("/update")
	@RolesAllowed({"admin","user"}) 
	@Produces("application/json")
	public Response update( @Context HttpHeaders headers, User user ) {
		UserDAO userDao = UserDAOFactory.getUserDAO();
		
		try {
			String id = getId( headers );
			
			user.setId( id );
			userDao.updateUser( user );
			
			// Return the response
			return ResponseBuilder.createResponse( Response.Status.OK, "User updated" );
		}
		catch( UserNotFoundException e ) {
			return ResponseBuilder.createResponse( Response.Status.NOT_FOUND, e.getMessage() );
		}
		catch ( Exception e ) {
			return ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED );
		}
		
	}
	
	@DELETE
	@Path("/delete")
	@RolesAllowed({"admin","user"}) 
	@Produces("application/json")
	public Response delete( @Context HttpHeaders headers ) {
		UserDAO userDao = UserDAOFactory.getUserDAO();
		
		try {
			String id = getId( headers );
			
			userDao.deleteUser( id );
			
			// Return the response
			return ResponseBuilder.createResponse( Response.Status.OK, "User deleted" );
		}
		catch( UserNotFoundException e ) {
			return ResponseBuilder.createResponse( Response.Status.NOT_FOUND, e.getMessage() );
		}
		catch ( Exception e ) {
			return ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED );
		}
		
	}
	
	private String getId( HttpHeaders headers) {
		// get the email we set in AuthenticationFilter
		List<String> id = headers.getRequestHeader( AuthenticationFilter.HEADER_PROPERTY_ID );
		
		if( id == null || id.size() != 1 )
			throw new NotAuthorizedException("Unauthorized!");
		
		return id.get(0);
	}

}

This Web Service offers standard CRUD operations as well as an authentication method (send email and password to retrieve a token). The getAll method is only accessible from an admin (compare role annotations). We do not need any JSON annotation properties. Jackson provides a JSON deserializer and automatically matches attributes into the (User) POJO object. In order to do so the created POJOs need getter and setter methods for all attributes as well as an empty constructor.

The class itself has two annotations to define the allowed roles and determine the Web Service path:

@DeclareRoles({"admin", "user", "guest"})
@Path("/user")

Followed by the first Web Service method with several annotations

@POST
@Path("/create")
@PermitAll
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createUser( UserSecurity userSecurity ) {
...
}

With these annotations we define a POST method which is accessible at …/user/create. Everybody may access this service (e.g. no authentication or authorization required). This methods consumes a JSON object like:

{
	"firstname":"Mark",
	"lastname":"Twain",
	"email" : "mark@twain.com",
	"password" : "secret"
}

The method produces a JSON object as response.

Now let us have a look at a method that is only accessible by an admin user:

@GET
@Path("/getAll")
@RolesAllowed({"admin"}) // only an admin user should be allowed to request all users
@Produces("application/json")
public Response getAll( @Context HttpHeaders headers ) {
...
}

This method is a GET Method. We require a valid JWT which is checked in the Authentication filter and carries the user Id (data base reference) in the JWT body. Only admin users are allowed to access this method (checked in Authentication filter as well) and it produces JSON objects.

All the methods use exception propagation which is required to send only the necessary information back to the client (or possible attackers):

@GET
@Path("/get")
@RolesAllowed({"admin","user"})
@Produces("application/json")
public Response get( @Context HttpHeaders headers ) {
	UserDAO userDao = UserDAOFactory.getUserDAO();
	
	try {
		String id = getId( headers );
		
		// use decoded email from jwt in header
		User user = userDao.getUser( id );
		
		// Return the token on the response
		return ResponseBuilder.createResponse( Response.Status.OK, user );
	}
	catch( UserNotFoundException e ) {
		return ResponseBuilder.createResponse( Response.Status.NOT_FOUND, e.getMessage() );
	}
	catch ( Exception e ) {
		return ResponseBuilder.createResponse( Response.Status.UNAUTHORIZED );
	}
}

Here we catch a UserNotFoundException which is thrown whenever a user is not found in the database. Additionally we catch any other Exception and return status Unauthorized (Internal Server Error would be an option, but it looks worse 🙂 ). Therefore we avoid that critical Exceptions or even stack traces reach the client.

We decode the user ID (database ID) in the JWT body. Whenever we receive a valid JWT, we have an identification for the user sending that request.

In order to send only information from POJOs that we want the client to receive, we implemented a JsonSerializable interface with one method toJson(). Consequently, when returning a user object to the client, we do not want to return a password:

@Override
public JSONObject toJson() throws JSONException {
	JSONObject jsonObject = new JSONObject();
	jsonObject.put( "email", this.email );
	jsonObject.put( "firstname", this.firstname );
	jsonObject.put( "lastname", this.lastname );
	return jsonObject;
}

5. Data Access Layer

Now finished with the Web Service, we move on to the Data Access Layer. We use Data Access Objects (DAO) in order to abstract the Business Layer from the Data Access Layer. The idea behind DAO is to have an interface and a factory that provides the implementation for the database.

public interface UserDAO {
	public boolean createUser( UserSecurity user ) throws UserExistingException;
	
	public String getUserIdByEmail( String email ) throws UserNotFoundException;
	public User getUser( String id ) throws UserNotFoundException;
	
	public List<User> getAllUsers();
	
	public UserSecurity getUserAuthentication( String id ) throws UserNotFoundException;
	public boolean setUserAuthentication( UserSecurity user ) throws UserNotFoundException;
	
	public boolean updateUser( User user ) throws UserNotFoundException;
	public boolean deleteUser( String id ) throws UserNotFoundException;
}

The UserDAO interface offers all methods we use in the REST Web Service. We have a config file that specifies the required database:

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

<Configuration>
	<dbType>SQLite</dbType>
	<dbName>testdb</dbName>
	<dbPath>jdbc:sqlite:testdb</dbPath>
</Configuration>


<!-- <Configuration>
	<dbType>OrientDB</dbType>
	<dbName>testdb</dbName>
	<dbPath>remote:localhost/testdb</dbPath>
	<dbHost></dbHost>
	<dbPort></dbPort>
	<dbUser>admin</dbUser>
	<dbPassword>admin</dbPassword>
	<dbPool>10</dbPool>
</Configuration> -->

The UserDAOFactory produces an implementation of the UserDAO interface, depending on the database selection (SQLlite, OrientDB):

public class UserDAOFactory {
	
	public static UserDAO getUserDAO() {
		// get connection
		Connection connection = ConnectionFactory.getConnection();
		
		// use driver specified according to database
		switch( DbConfig.getDbType() ) {
			case ORIENTDB:
				return new GremlinUserDAO( connection );
			case SQLITE:
				return new SqlUserDAO( connection );
			default:
				// should not happen: we test for correct input in DbConfig.java
				return null;
		}
	}
}

You can compare the usage in the REST Web Service. We only access the DAO object via the factory and do not care about the underlying database implementation. The DAO objects require a connection to a database, which is returned by the ConnectionFactory:

public class ConnectionFactory {
	
	private static Connection connection = null;
	
	public static Connection getConnection() {
		if( connection != null ) return connection;
		
		switch( DbConfig.getDbType() ) {
			case ORIENTDB:
				connection = new OrientDbConnection();
			case SQLITE:
				connection = new SQLiteConnection();
			default:
				break;
		}

		// open connection
		connection.open();
		
		return connection;
	}
}

This factory returns an Connection Interface:

public interface Connection {
	public Object get();
	public boolean open();
	public boolean close();
}

This interface is used to abstract the connection. E.g. we could use a PostgreSQL instead of SQLite connection that works with the SqlUserDAO (if the SQL syntax is equal).

6. Test the REST Web Service

Build the project and deploy it to a Tomcat server. If you have the project up and running it is time for some tests. We use Postman to send GET/POST requests to the server ( e.g. http://localhost:8080/rest-jersey2-json-jwt/user/create):

The Github repository includes a postman_collection.json file that offers some test cases.

If you do not use Postman:

  1. Start of with user creation ( firstname, lastname, email, password )
  2. If successful, the server will automatically log you in and return a x-access-token
  3. In the request header you need a field x-access-token which contains the token provided in step 2 (if no @PermitAll annotation available)
  4. Whenever you send a request with a JSON Body, make sure to include a header field Content-Type with value application/json

7. Improvements

I did not perform a lot of tests, the basic functionality works, but i would not consider it safe yet. I hope to do some more tests and updates when i have time. Some improvement ideas:

  • Check the incoming JSON input. There are no checks for the incoming JSON objects. We have no abstraction and directly load the JSON objects into our POJO files. This might be a security risk.
  • Introduce a Business Layer. Right now we work directly with the Data Access Layer in the Web Service. It would be better to add another abstraction layer to abstract the Web Service from the Data Layer.
  • JWT and Password encryption abstraction: Right now we use “hard”-coded implementations for JWT and encryption. It would be better maintainable if we abstract the algorithms via interfaces and factories as well. Some algorithms are considered safe now but become obsolete in a few years. The ability to change the underlying algorithm as fast as possible may come in handy (requires another field for versioning and dealing with old data).
  • Introduce a refresh token in the database.
  • Create an IP logger for authentication requests to avoid brute-force attacks. Basically you store which IP tried to login and deny the access after 3 failed tries in a row for some time.
  • Logout method. Create a method to invalidate the JWT which equals a logout.

Finally, to use the JWT authentication safely in production, a SSL connection is absolutely required (the JWT is not encrypted, only signed – Man in the middle attack).

This was a long tutorial, i hope it was still clear and understandable. I left out many parts of the code for simplification, i suggest you download the code in the beginning, have a look and come back here for some understanding.

If you have exceptions, questions or problems, feel free to ask and comment.

Facebooktwitterredditpinterestlinkedinmail

Related posts

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.