JAX-RS Security using JSON Web Encryption (JWE) with JWS/JWT for Authentication and Authorization

JAX-RS Security using JSON Web Encryption(JWE) with JWK/JWS/JWT for Authentication and Authorization Example

In this tutorial we will discuss how to secure JAX-RS RESTful web services using JSON Web Encryption(JWE), JSON Web Key (JWK), JSON Web Signature(JWS), and JSON Web Tokens(JWT) for Authentication and Authorization. JSON Web Encryption (JWE) encrypted content using Javascript Object Notation (JSON) based structures. In our example implementation, we will be using Symmetric Encryption where the receiver and sender share a common key. In the next tutorial JAX-RS Security using JSON Web Encryption(JWE) with AngularJS, Bootstrap, Grid-UI and MongoDB Example we discuss the User Interface (UI) components and how they integrate with the backend.

What is JSON Web Encryption (JWE)?

JSON Web Encryption, JWE for short, are encrypted using cryptographic algorithms and serialized for tokenization in HTTP authorization headers. In order to ensure the message or token has not been altered in any way the token contains a digital signature (JWS) that is cryptographically encrypted using a strong algorithm such as HMAC SHA-256.

CONTENT MASTER KEY ENCRYPTION

JWE supports three forms of Content Master Key (CMK) encryption:

  • Asymmetric encryption under the recipient’s public key.
  • Symmetric encryption under a key shared between the sender and receiver.
  • Symmetric encryption under a key agreed upon between the sender and receiver.

Structure of JSON Web Encryption Compact Serialization

A JSON Web Encryption compact serialization is structured into five parts: The JWE Protected Header, JWE Encrypted Key, JWE Initialization Vector, JWE Ciphertext, and the JWE Authentication Tag separated by period character (.).

jwe_structure

JWT Token encrypted using JWE and signed with JWS

Below you can see an actual encrypted JSON Web Token using JWE and base64 encoded and signed with JWS to ensure the contents have not been modified in any way.

eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjpudWxsLCJ
jdHkiOiJKV1QifQ..TKJLnTuWFr-c88OpGrwWWw.QMr2usYlLPj4EosyZ-VFHopCkavJE
QucDlPCjNaA2Rk78B1BUwci2sb6m8ZVveXKNPGGu6yl3Yd4tE_4-LyVX_kgQFEdxjAO9Y
kwJ-iANdrBdomjrmarNXyqapgHuJ0Z5aTKKzSc8mbOlFbcohHp9eexDjbI1Rgy7Fxzgez
eOkqkGbIuy8KGI0siCBFp6ttm5rfePCU7bjeBkPjECPk8WvxMyH9VmLJArUu1vZnEO0AZ
Qvcmc8ijmId4ezX1a89KmOkxh-I_h3H8DU9Yx7On7JKonHbm7xFx9jH4nwPVtl0FB2LW7
EdZtD-baH2tgSz8jiDSjEkgaEtp61wGgynG9BG_XlO0mw-Imu_aFjz2j9bMPpZUFdlHrE
ljqBYRcP9d.yrsAzczT88htfobE1B_9lg

Getting Started

In order to run this tutorial yourself, you will need the following:

Required Libraries

Copy all of the following jars to WebContent->WEB-INF->lib folder.

asm-3.1.jar
commons-codec-1.9.jar
commons-logging-1.2.jar
fluent-hc-4.5.jar
httpclient-4.5.jar
httpclient-cache-4.5.jar
httpclient-win-4.5.jar
httpcore-4.4.1.jar
httpmime-4.5.jar
jackson-core-asl-1.9.2.jar
jackson-jaxrs-1.9.2.jar
jackson-mapper-asl-1.9.2.jar
jackson-xc-1.9.2.jar
jersey-client-1.18.jar
jersey-core-1.18.jar
jersey-json-1.18.jar
jersey-server-1.18.jar
jersey-servlet-1.18.jar
jna-4.1.0.jar
jna-platform-4.1.0.jar
jose4j-0.4.4.jar
json-simple-1.1.1.jar
jsr311-api-1.1.1.jar
log4j-1.2.17.jar
mongo-java-driver-3.0.2.jar
persistence-api-1.0.2.jar
slf4j-api-1.7.13.jar

Complete Project Overview

I have added the project overview to give you a full view of the structure and show you all files contained in this sample project.

jax-rs_jwe_security_proj

RESTful Web Service End Points

Restricted URIs will be shown with LOCK icon in the table below.

#URIMethodDescription
1/rest/security/statusGETDisplays the current status of the API being used. Non-restricted REST end-point
2/rest/security/authenticateGETAuthenticates the user by using the username and password that is passed in the header against the User in the User Collection in MongoDB data store.
3/rest/security/getallrolesGETRetrieves all roles for a given token by returning the roles as a JSON array. **Restricted REST end-point
4/rest/security/showallitemsGETRetrieves all items in our MongoDB datastore return the entire collection as a JSON array. **Restricted REST end-point

JSON Web Encryption Authorization and Authentication Communication Flow

jwe_auth_flow

1 – Initial Request for Protected Resource

C:\curl>curl -H "Content-Type: application/json" -H "username: apacheuser" 
-H "password: Summer95!" -v -X POST  
http://localhost:8080/JweSecurityExample/rest/security/authenticate
* Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /JweSecurityExample/rest/security/authenticate HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.48.0
> Accept: */*
> Content-Type: application/json
> username: apacheuser
> password: Summer95!

Username/Password in Header

For security reason we recommend that the username and password be included in the HTTP Headers instead of HTTP Parameters via (@QueryParam). This is especially important when using TLS/SSL as it will guarantee that request data is encrypted end to end and prevent man in the middle attacks.

2 – Server Responds with Success Code and Payload (on Success)

At this point, our server responds with the encrypted and digitally signed JWT token for us to use.

< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 12 Mar 2016 16:07:34 GMT
<
{"message":"eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjpudWxsLCJ
jdHkiOiJKV1QifQ..upoa4h2LsxIbPfEv2gMmjQ.e0OCTvkFPOy5LVlcqlstOz3ZlAZXjymHQNd
cYQh7O-hh4jJEeQDPaHXafvysU_OEImB9qPrp1uDfp0ZdDh6ISvZxARIPRtJ-GyFi_X0fuBpacF
7FAhu7uObEBl60QulWS4msJLOKt3NE2g-Xskl1-3_SGhdbku5w0wQ66_TkZGS9DXtpCP5emeEvO
dJKsJ00Bbd0Fw1eAxWvsvSD9dFPG6qJhKR-V4tXN4semV17c7uH6INWzsaVCAkqGzrcrsZNL316
wcCZhq279FWj7Kz2zwGDKA1aMFgQ8Kv5UdH566xW8CXMK785MLs1FNvaKj4Q_04vR-p7UTD5EMX
IVJq6xAE8webTdBrebdOrIlKx4CVXmovyGAJGQicVmuTHIshz._mw0nYAfCIAD-1eqto33jw"
,"status_code":200}
* Connection #0 to host localhost left intact

3 – JSON Web Encryption (JWE)/JSON Web Token in Header

All subsequent calls should contain this encrypted JWT Token as shown below (carriage returns have been added for readability).

curl -H "Content-Type: application/json" -H "token: eyJhbGciOiJkaXIiLCJlbmMiO
iJBMTI4Q0JDLUhTMjU2Iiwia2lkIjpudWxsLCJjdHkiOiJKV1QifQ..upoa4h2LsxIbPfEv2gMmjQ
.e0OCTvkFPOy5LVlcqlstOz3ZlAZXjymHQNdcYQh7O-hh4jJEeQDPaHXafvysU_OEImB9qPrp1uDf
p0ZdDh6ISvZxARIPRtJ-GyFi_X0fuBpacF7FAhu7uObEBl60QulWS4msJLOKt3NE2g-Xskl1-3_SG
hdbku5w0wQ66_TkZGS9DXtpCP5emeEvOdJKsJ00Bbd0Fw1eAxWvsvSD9dFPG6qJhKR-V4tXN4semV
17c7uH6INWzsaVCAkqGzrcrsZNL316wcCZhq279FWj7Kz2zwGDKA1aMFgQ8Kv5UdH566xW8CXMK78
5MLs1FNvaKj4Q_04vR-p7UTD5EMXIVJq6xAE8webTdBrebdOrIlKx4CVXmovyGAJGQicVmuTHIshz
._mw0nYAfCIAD-1eqto33jw" -v -X GET  
http://localhost:8080/JweSecurityExample/rest/security/showallitems

*   Trying ::1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /JweSecurityExample/rest/security/showallitems HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.48.0
> Accept: */*
> Content-Type: application/json
> token: eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjpudWxsLCJjdHki
OiJKV1QifQ..upoa4h2LsxIbPfEv2gMmjQ.e0OCTvkFPOy5LVlcqlstOz3ZlAZXjymHQNdcYQh7O-
hh4jJEeQDPaHXafvysU_OEImB9qPrp1uDfp0ZdDh6ISvZxARIPRtJ-GyFi_X0fuBpacF7FAhu7uOb
EBl60QulWS4msJLOKt3NE2g-Xskl1-3_SGhdbku5w0wQ66_TkZGS9DXtpCP5emeEvOdJKsJ00Bbd0
Fw1eAxWvsvSD9dFPG6qJhKR-V4tXN4semV17c7uH6INWzsaVCAkqGzrcrsZNL316wcCZhq279FWj7
Kz2zwGDKA1aMFgQ8Kv5UdH566xW8CXMK785MLs1FNvaKj4Q_04vR-p7UTD5EMXIVJq6xAE8webTdB
rebdOrIlKx4CVXmovyGAJGQicVmuTHIshz._mw0nYAfCIAD-1eqto33jw
>

4 – Server Responds with Success Code and Payload (on Success)

< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 12 Mar 2016 16:17:34 GMT
<
[{"_id":"10029T1","item-id":"123","item-name":"KitchenAid Artisan 5 qt. 
Stand Mixer","price":314.99,"quantity":13},{"_id":"12349K1","item-id":
"k10001","item-name":"Keurig K10 MINI Plus Brewing System","price":
79.99,"quantity":36},{"_id":"83949PT","item-id":"EPP1029","item-name":
"Electric Power Pressure Cooker XL (8 qt)","price":119.99,"quantity":8},
{"_id":"71829Y","item-id":"IQ50009","item-name":"KitchenIQ 50009 Edge 
Grip 2 Stage Knife Sharpener, Black","price":5.79,"quantity":23},{"_id":
"30814B","item-id":"3081414B","item-name":"La Crosse Technology 
308-1414B Wireless Atomic Digital Color Forecast Station","price":49.99,
"quantity":6},{"_id":"PAN110CFM","item-id":"110CFM","item-name":
"Panasonic FV-11VQ5 WhisperCeiling 110 CFM Ceiling Mounted Fan, White",
"price":113.79,"quantity":7},{"_id":"AS4175","item-id":"AS4175",
"item-name":"American Standard 4175.300.075 Colony Soft Pull-Down 
Kitchen Faucet, Stainless Steel","price":120.0,"quantity":9},{"_id":
"FM3700B","item-id":"FM3700B","item-name":"PUR Advanced Faucet Water 
Filter Chrome FM-3700B","price":23.95,"quantity":27},{"_id":"ARC150SB",
"item-id":"ARC150SB","item-name":"Aroma 20 Cup Cooked (10 cup uncooked) 
Digital Rice Cooker, Slow Cooker","price":36.99,"quantity":13},{"_id":
"CPT180TST","item-id":"CPT180TST","item-name":"Cuisinart Metal Classic 
4-Slice Toaster","price":69.99,"quantity":6},{"_id":"GR4NWPAN","item-id":
"GR4NWPAN","item-name":"Cuisinart Griddler and Waffle Maker with Removable
 Plates","price":99.99,"quantity":13}]

* Connection #0 to host localhost left intact

The Item Model (Item.java)

This will be used to as the object which we store and retrieve in order to test out our application. I added it because I wanted my web service to store and retrieve some Java object.

package com.avaldes.model;

import javax.persistence.Id;

import org.codehaus.jackson.annotate.JsonProperty;

public class Item {
  @Id
  private String _id;
  private String itemId;
  private String itemName;
  private double itemPrice;
  private int itemQuantity;
  
  public Item() {}
  
  public Item(String _id, String itemId, String itemName, 
      double itemPrice, int itemQuantity) {
    
    super();
    this._id = _id;
    this.itemId = itemId;
    this.itemName = itemName;
    this.itemPrice = itemPrice;
    this.itemQuantity = itemQuantity;
  }

  public String get_id() {
    return _id;
  }
  
  public void set_id(String _id) {
    this._id = _id;
  }
  
  @JsonProperty(value = "item-id")
  public String getItemId() {
    return itemId;
  }
  
  public void setItemId(String itemId) {
    this.itemId = itemId;
  }
  
  @JsonProperty(value = "item-name")
  public String getItemName() {
    return itemName;
  }
  
  public void setItemName(String itemName) {
    this.itemName = itemName;
  }
  
  @JsonProperty(value = "price")
  public double getItemPrice() {
    return itemPrice;
  }
  
  public void setItemPrice(double itemPrice) {
    this.itemPrice = itemPrice;
  }
  
  @JsonProperty(value = "quantity")
  public int getItemQuantity() {
    return itemQuantity;
  }
  
  public void setItemQuantity(int itemQuantity) {
    this.itemQuantity = itemQuantity;
  }

  @Override
  public String toString() {
    return "Item [_id=" + _id + ", itemId=" + itemId + ", itemName="
        + itemName + ", itemPrice=" + itemPrice + ", itemQuantity="
        + itemQuantity + "]";
  }
}

The User Model (User.java)

This will be used to as the object which we store and retrieve in order to test out our application. I added it because I wanted my web service to store and retrieve some Java object.

package com.avaldes.model;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.Id;

import org.codehaus.jackson.annotate.JsonProperty;

public class User {
  @Id
  private String _id;
  private String username;
  private String password;
  private String firm;
  private List<String> rolesList = new ArrayList<String>();
  
  public User() {}

  public User(String _id, String username, 
								String password, List<String> rolesList) {
    super();
    this._id = _id;
    this.username = username;
    this.password = password;
    this.rolesList = rolesList;
  }

  @JsonProperty(value = "_id")
  public String get_id() {
    return _id;
  }
  
  public void set_id(String _id) {
    this._id = _id;
  }

  @JsonProperty(value = "username")
  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  @JsonProperty(value = "password")
  public String getPassword() {
    return password;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  @JsonProperty(value = "firm")
  public String getFirm() {
    return firm;
  }

  public void setFirm(String firm) {
    this.firm = firm;
  }

  @JsonProperty(value = "roles")
  public List<String> getRolesList() {
    return rolesList;
  }

  public void setRolesList(List<String> rolesList) {
    this.rolesList = rolesList;
  }

  @Override
  public String toString() {
    return "User [_id=" + _id + ", username=" + username 
			+ ", password=" + password + ", rolesList=" 
			+ rolesList + "]";
  }
}

The Singleton Class for Mongo Database (MongoDBSingleton.java)

package com.avaldes.util;

import java.io.IOException;
import java.util.Properties;

import org.apache.log4j.Logger;

import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.client.MongoDatabase;

public class MongoDBSingleton {
  static Logger logger = Logger.getLogger(MongoDBSingleton.class);
  private static final String properties_filename 
																= "mongodb.properties";
  
  private static MongoClient mongo            = null;
  private static MongoDatabase mongoDatabase  = null;
  private static String hostname              = null;
  private static int port                     = 0;
  private static String username              = null;
  private static String password              = null;
  private static String database              = null;
  
  private static class Holder {
    private static final MongoDBSingleton instance 
			= new MongoDBSingleton();
  }
  
  private MongoDBSingleton() {
    logger.info("Inside MongoDBSingleton...");
    ClassLoader classLoader 
			= Thread.currentThread().getContextClassLoader();
    Properties properties = new Properties();
    try {
      logger.info("Reading mongo.properties...");
      properties.load(
				classLoader.getResourceAsStream(properties_filename));
      hostname = properties.getProperty("mongodb.hostname");
      logger.info("mongodb.hostname....: " + hostname);
      String portStr = properties.getProperty("mongodb.port");
      port = Integer.parseInt(portStr);
      logger.info("mongodb.port........: " + port);
      username = properties.getProperty("mongodb.username");
      logger.info("mongodb.username....: " + username);
      password = properties.getProperty("mongodb.password");
      logger.info("mongodb.password....: " + password);
      database = properties.getProperty("mongodb.database");
      logger.info("mongodb.database....: " + database);
      
    } catch (IOException e) {
      e.printStackTrace();
    }
  };
  
  public static MongoDBSingleton getInstance() {
    return Holder.instance;
  }
  
  public MongoClient getMongoClient() {
    String URI = String.format("mongodb://%s:%s@%s:%d/?authSource=%s",
				username, password, hostname, port, database); 
    MongoClientURI mongoClientURI = new MongoClientURI(URI);
    mongo = new MongoClient(mongoClientURI);
    return mongo;
  }
  
  public MongoDatabase getDatabase() {
    if (mongoDatabase == null) {
      mongo = getMongoClient();
    }
    return mongo.getDatabase(database);
  }
}

Complete Program (JWESecurityExample.java)

package com.avaldes.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.apache.log4j.Logger;
import org.bson.Document;
import org.bson.json.JsonParseException;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.jose4j.jwe.ContentEncryptionAlgorithmIdentifiers;
import org.jose4j.jwe.JsonWebEncryption;
import org.jose4j.jwe.KeyManagementAlgorithmIdentifiers;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKey.Factory;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.lang.JoseException;

import com.avaldes.model.Item;
import com.avaldes.model.StatusMessage;
import com.avaldes.model.User;
import com.avaldes.util.MongoDBSingleton;
import com.mongodb.client.MongoDatabase;

@Path("/security")
public class JWESecurityExample {
  static Logger logger = Logger.getLogger(JWESecurityExample.class);
  static JsonWebKey jwKey = null;

  static {
    logger.info("Inside static initializer...");
    // Setting up Direct Symmetric Encryption and Decryption
    String jwkJson = "{\"kty\":\"oct\",
             \"k\":\"9d6722d6-b45c-4dcb-bd73-2e057c44eb93-928390\"}";
    try {
      new JsonWebKey.Factory();
      jwKey = Factory.newJwk(jwkJson);
    } catch (JoseException e) {
      e.printStackTrace();
    }
  }

  @Path("/status")
  @GET
  @Produces(MediaType.TEXT_HTML)
  public String returnVersion() {
    return "JweSecurityExample Status is OK...";
  }

  @Path("/authenticate")
  @POST
  @Produces(MediaType.APPLICATION_JSON)
  public Response authenticateCredentials(
      @HeaderParam("username") String username,
      @HeaderParam("password") String password)
      throws JsonGenerationException, JsonMappingException,
      IOException {

    logger.info("Authenticating User Credentials...");

    if (username == null) {
      StatusMessage statusMessage = new StatusMessage();
      statusMessage
          .setStatus(Status.PRECONDITION_FAILED.getStatusCode());
      statusMessage.setMessage("Username value is missing!!!");
      return Response
          .status(Status.PRECONDITION_FAILED.getStatusCode())
          .entity(statusMessage).build();
    }

    if (password == null) {
      StatusMessage statusMessage = new StatusMessage();
      statusMessage
          .setStatus(Status.PRECONDITION_FAILED.getStatusCode());
      statusMessage.setMessage("Password value is missing!!!");
      return Response
          .status(Status.PRECONDITION_FAILED.getStatusCode())
          .entity(statusMessage).build();
    }

    User user = validateUser(username, password);
    logger.info("User after validateUser => " + user);

    if (user == null) {
      StatusMessage statusMessage = new StatusMessage();
      statusMessage.setStatus(Status.FORBIDDEN.getStatusCode());
      statusMessage
          .setMessage("Access Denied for this functionality !!!");
      logger.info("statusMessage ==> " + statusMessage);
      return Response.status(Status.FORBIDDEN.getStatusCode())
          .entity(statusMessage).build();
    }

    logger.info("User Information => " + user);

    // Create the Claims, which will be the content of the JWT
    JwtClaims claims = new JwtClaims();
    claims.setIssuer("avaldes.com");
    claims.setExpirationTimeMinutesInTheFuture(10);
    claims.setGeneratedJwtId();
    claims.setIssuedAtToNow();
    claims.setNotBeforeMinutesInThePast(2);
    claims.setSubject(user.getUsername());
    claims.setStringListClaim("roles", user.getRolesList());

    JsonWebSignature jws = new JsonWebSignature();

    logger.info("Claims => " + claims.toJson());
    // The payload of the JWS is JSON content of the JWT Claims
    jws.setPayload(claims.toJson());
    jws.setKeyIdHeaderValue(jwKey.getKeyId());
    jws.setKey(jwKey.getKey());

    jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);

    String jwt = null;
    try {
      jwt = jws.getCompactSerialization();
    } catch (JoseException e) {
      e.printStackTrace();
    }

    JsonWebEncryption jwe = new JsonWebEncryption();
    jwe.setAlgorithmHeaderValue(
        KeyManagementAlgorithmIdentifiers.DIRECT);
    jwe.setEncryptionMethodHeaderParameter(
        ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);
    jwe.setKey(jwKey.getKey());
    jwe.setKeyIdHeaderValue(jwKey.getKeyId());
    jwe.setContentTypeHeaderValue("JWT");
    jwe.setPayload(jwt);

    String jweSerialization = null;
    try {
      jweSerialization = jwe.getCompactSerialization();
    } catch (JoseException e) {
      e.printStackTrace();
    }

    StatusMessage statusMessage = new StatusMessage();
    statusMessage.setStatus(Status.OK.getStatusCode());
    statusMessage.setMessage(jweSerialization);
    logger.info("statusMessage ==> " + statusMessage);
    return Response.status(Status.OK.getStatusCode())
        .entity(statusMessage).build();
  }

  // --- Protected resource using JWT/JWE Token ---
  @Path("/getallroles")
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public Response getAllRoles(@HeaderParam("token") String token)
      throws JsonGenerationException, JsonMappingException,
      IOException {

    logger.info("Inside getAllRoles...");

    List<String> allRoles = null;

    if (token == null) {
      StatusMessage statusMessage = new StatusMessage();
      statusMessage.setStatus(Status.FORBIDDEN.getStatusCode());
      statusMessage
          .setMessage("Access Denied for this functionality !!!");
      return Response.status(Status.FORBIDDEN.getStatusCode())
          .entity(statusMessage).build();
    }

    logger.info("JWK (1) ===> " + jwKey.toJson());

    // Validate Token's authenticity and check claims
    JwtConsumer jwtConsumer = new JwtConsumerBuilder()
        .setRequireExpirationTime()
        .setAllowedClockSkewInSeconds(30)
        .setRequireSubject() 
        .setExpectedIssuer("avaldes.com")
        .setDecryptionKey(jwKey.getKey())
        .setVerificationKey(jwKey.getKey()).build(); 

    try {
      // Validate the JWT and process it to the Claims
      JwtClaims jwtClaims = jwtConsumer.processToClaims(token);
      logger.info("JWT validation succeeded! " + jwtClaims);
      try {
        allRoles = jwtClaims.getStringListClaimValue("roles");
      } catch (MalformedClaimException e) {
        e.printStackTrace();
      }
    } catch (InvalidJwtException e) {
      logger.error("JWT is Invalid: " + e);
      StatusMessage statusMessage = new StatusMessage();
      statusMessage.setStatus(Status.FORBIDDEN.getStatusCode());
      statusMessage
          .setMessage("Access Denied for this functionality !!!");
      return Response.status(Status.FORBIDDEN.getStatusCode())
          .entity(statusMessage).build();
    }

    return Response.status(200).entity(allRoles).build();
  }

  // --- Protected resource using JWT/JWE Token ---
  @Path("/showallitems")
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public Response showAllItems(@HeaderParam("token") String token)
      throws JsonGenerationException, JsonMappingException,
      IOException {

    Item item = null;

    logger.info("Inside showAllItems...");

    if (token == null) {
      StatusMessage statusMessage = new StatusMessage();
      statusMessage.setStatus(Status.FORBIDDEN.getStatusCode());
      statusMessage
          .setMessage("Access Denied for this functionality !!!");
      return Response.status(Status.FORBIDDEN.getStatusCode())
          .entity(statusMessage).build();
    }

    logger.info("JWK (1) ===> " + jwKey.toJson());

    // Validate Token's authenticity and check claims
    JwtConsumer jwtConsumer = new JwtConsumerBuilder()
        .setRequireExpirationTime()
        .setAllowedClockSkewInSeconds(30)
        .setRequireSubject()
        .setExpectedIssuer("avaldes.com")
        .setDecryptionKey(jwKey.getKey())
        .setVerificationKey(jwKey.getKey()).build();

    try {
      // Validate the JWT and process it to the Claims
      JwtClaims jwtClaims = jwtConsumer.processToClaims(token);
      logger.info("JWT validation succeeded! " + jwtClaims);
    } catch (InvalidJwtException e) {
      logger.error("JWT is Invalid: " + e);
      StatusMessage statusMessage = new StatusMessage();
      statusMessage.setStatus(Status.FORBIDDEN.getStatusCode());
      statusMessage
          .setMessage("Access Denied for this functionality !!!");
      return Response.status(Status.FORBIDDEN.getStatusCode())
          .entity(statusMessage).build();
    }

    MongoDBSingleton mongoDB = MongoDBSingleton.getInstance();
    MongoDatabase db = mongoDB.getDatabase();

    List<Document> results = db.getCollection("items").find()
        .into(new ArrayList<Document>());
    int size = results.size();

    if (size == 0) {
      StatusMessage statusMessage = new StatusMessage();
      statusMessage
          .setStatus(Status.PRECONDITION_FAILED.getStatusCode());
      statusMessage.setMessage("There are no Items to display !!!");
      return Response
          .status(Status.PRECONDITION_FAILED.getStatusCode())
          .entity(statusMessage).build();
    }

    List<Item> allItems = new ArrayList<Item>();
    for (Document current : results) {
      ObjectMapper mapper = new ObjectMapper();
      try {
        logger.info(current.toJson());
        item = mapper.readValue(current.toJson(), Item.class);
        allItems.add(item);
      } catch (JsonParseException e) {
        e.printStackTrace();
      } catch (JsonMappingException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

    return Response.status(200).entity(allItems).build();
  }

  private User validateUser(String username, String password) {
    MongoDBSingleton mongoDB = MongoDBSingleton.getInstance();
    MongoDatabase db = mongoDB.getDatabase();
    List<Document> results = null;

    logger.info("Inside of validateUser...");
    results = db.getCollection("users")
        .find(new Document("username", username)).limit(1)
        .into(new ArrayList<Document>());

    int size = results.size();
    logger.info("size of results==> " + size);

    if (size > 0) {
      for (Document current : results) {
        ObjectMapper mapper = new ObjectMapper();
        User user = null;
        try {
          logger.info(current.toJson());
          user = mapper.readValue(current.toJson(), User.class);
        } catch (JsonParseException e) {
          e.printStackTrace();
        } catch (JsonMappingException e) {
          e.printStackTrace();
        } catch (IOException e) {
          e.printStackTrace();
        }
        if (user != null && username.equals(user.getUsername())
            && password.equals(user.getPassword())) {
          return user;
        } else {
          return null;
        }
      }
      return null;
    } else {
      return null;
    }
  }
}

LOG4J Configuration File (log4j.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "
    -//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">

	<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">

  <!-- Appenders -->
  <appender name="console" class="org.apache.log4j.ConsoleAppender">
  <param name="Target" value="System.out" />
    <layout class="org.apache.log4j.PatternLayout">
    <param name="ConversionPattern" value="%-5p: %c - %m%n" />
    </layout>
  </appender>
  
  <!-- Application Loggers -->
  <logger name="com.avaldes">
    <level value="info" />
  </logger>

  <!-- Root Logger -->
  <root>
    <priority value="warn" />
    <appender-ref ref="console" />
  </root>
</log4j:configuration>

Web Deployment Descriptor (web.xml)

This is a pretty straight forward deployment descriptor file – only thing you need to add is the location of you java package in the Jersey ServletContainer entry as init-param. Please ensure you add it to the web.xml file as shown below.

<?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">

  <display-name>JAX-RS JSON Web Token Application</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>
  
  <servlet>
    <servlet-name>Jersey REST Service</servlet-name>
     <servlet-class>
       com.sun.jersey.spi.container.servlet.ServletContainer
     </servlet-class>
    <init-param>
      <param-name>
        com.sun.jersey.config.property.packages
      </param-name>
      <param-value>com.avaldes</param-value>
    </init-param>
    <init-param>
      <param-name>
        com.sun.jersey.api.json.POJOMappingFeature
      </param-name>
      <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>Jersey REST Service</servlet-name>
    <url-pattern>/rest/*</url-pattern>
  </servlet-mapping>
</web-app>

MongoDB Items Collections

{
    "_id" : "10029T1",
    "item-id" : "123",
    "item-name" : "KitchenAid Artisan 5 qt. Stand Mixer",
    "price" : 314.99,
    "quantity" : 13
}
{
    "_id" : "12349K1",
    "item-id" : "k10001",
    "item-name" : "Keurig K10 MINI Plus Brewing System",
    "price" : 79.99,
    "quantity" : 36
}
{
    "_id" : "83949PT",
    "item-id" : "EPP1029",
    "item-name" : "Electric Power Pressure Cooker XL (8 qt)",
    "price" : 119.99,
    "quantity" : 8
}
{
    "_id" : "71829Y",
    "item-id" : "IQ50009",
    "item-name" : "KitchenIQ 50009 Edge Grip 2 Stage Knife Sharpener, 
                     Black",
    "price" : 5.79,
    "quantity" : 23
}
{
    "_id" : "30814B",
    "item-id" : "3081414B",
    "item-name" : "La Crosse Technology 308-1414B Wireless 
                      Atomic Digital Color Forecast Station",
    "price" : 49.99,
    "quantity" : 6
}
{
    "_id" : "PAN110CFM",
    "item-id" : "110CFM",
    "item-name" : "Panasonic FV-11VQ5 WhisperCeiling 110 CFM Ceiling 
                      Mounted Fan, White",
    "price" : 113.79,
    "quantity" : 7
}
{
    "_id" : "AS4175",
    "item-id" : "AS4175",
    "item-name" : "American Standard 4175.300.075 Colony Soft 
                      Pull-Down Kitchen Faucet, Stainless Steel",
    "price" : 120,
    "quantity" : 9
}
{
    "_id" : "FM3700B",
    "item-id" : "FM3700B",
    "item-name" : "PUR Advanced Faucet Water Filter Chrome FM-3700B",
    "price" : 23.95,
    "quantity" : 27
}
{
    "_id" : "ARC150SB",
    "item-id" : "ARC150SB",
    "item-name" : "Aroma 20 Cup Cooked (10 cup uncooked) Digital 
		     Rice Cooker, Slow Cooker",
    "price" : 36.99,
    "quantity" : 13
}
{
    "_id" : "CPT180TST",
    "item-id" : "CPT180TST",
    "item-name" : "Cuisinart Metal Classic 4-Slice Toaster",
    "price" : 69.99,
    "quantity" : 6
}
{
    "_id" : "GR4NWPAN",
    "item-id" : "GR4NWPAN",
    "item-name" : "Cuisinart Griddler® and Waffle Maker with 
	      Removable Plates",
    "price" : 99.99,
    "quantity" : 13
}

MongoDB Users Collections

{
    "_id" : "1",
    "username" : "apacheuser",
    "password" : "Summer95!",
    "firm"     : "Apache",
    "roles"    : ["client", "admin"]
}
{
    "_id" : "2",
    "username" : "springuser",
    "password" : "Spring99!",
    "firm"     : "SpringSource",
    "roles"    : ["client"]
}
{
    "_id" : "3",
    "username" : "user3",
    "password" : "Autumn03!",
    "firm"     : "RedHat",
    "roles"    : ["client"]
}

MongoDB Property File

We will be storing all of the MongoDB Database credentials in a property file that will only be accessible by the application running on the server.

#----MongoDB Database Details-----
mongodb.hostname=localhost
mongodb.port=27017
mongodb.username=webuser
mongodb.password=W3b$ervic3s!
mongodb.database=jwtDB

Testing out the Web Services

In addition to using our AngularJS/Bootstrap/Grid-UI web application to test out our restful services I used both CURL and Postman which is a Google Chrome Application. Using this tool I validated each of the REST API calls. Please review the screen shots below:

Using CURL to Test out the JWE/JWT/JWS Authorization and Authentication

C:\curl>curl -H "Content-Type: application/json" -H "username: apacheuser" 
-H "password: Summer95!" -v -X POST  
http://localhost:8080/JweSecurityExample/rest/security/authenticate
* Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /JweSecurityExample/rest/security/authenticate HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.48.0
> Accept: */*
> Content-Type: application/json
> username: apacheuser
> password: Summer95!
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 12 Mar 2016 16:07:34 GMT
<
{"message":"eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjpudWxsLCJ
jdHkiOiJKV1QifQ..upoa4h2LsxIbPfEv2gMmjQ.e0OCTvkFPOy5LVlcqlstOz3ZlAZXjymHQNd
cYQh7O-hh4jJEeQDPaHXafvysU_OEImB9qPrp1uDfp0ZdDh6ISvZxARIPRtJ-GyFi_X0fuBpacF
7FAhu7uObEBl60QulWS4msJLOKt3NE2g-Xskl1-3_SGhdbku5w0wQ66_TkZGS9DXtpCP5emeEvO
dJKsJ00Bbd0Fw1eAxWvsvSD9dFPG6qJhKR-V4tXN4semV17c7uH6INWzsaVCAkqGzrcrsZNL316
wcCZhq279FWj7Kz2zwGDKA1aMFgQ8Kv5UdH566xW8CXMK785MLs1FNvaKj4Q_04vR-p7UTD5EMX
IVJq6xAE8webTdBrebdOrIlKx4CVXmovyGAJGQicVmuTHIshz._mw0nYAfCIAD-1eqto33jw"
,"status_code":200}
* Connection #0 to host localhost left intact
------------------------------------------------------------

curl -H "Content-Type: application/json" -H "token: eyJhbGciOiJkaXIiLCJlbmMiO
iJBMTI4Q0JDLUhTMjU2Iiwia2lkIjpudWxsLCJjdHkiOiJKV1QifQ..upoa4h2LsxIbPfEv2gMmjQ
.e0OCTvkFPOy5LVlcqlstOz3ZlAZXjymHQNdcYQh7O-hh4jJEeQDPaHXafvysU_OEImB9qPrp1uDf
p0ZdDh6ISvZxARIPRtJ-GyFi_X0fuBpacF7FAhu7uObEBl60QulWS4msJLOKt3NE2g-Xskl1-3_SG
hdbku5w0wQ66_TkZGS9DXtpCP5emeEvOdJKsJ00Bbd0Fw1eAxWvsvSD9dFPG6qJhKR-V4tXN4semV
17c7uH6INWzsaVCAkqGzrcrsZNL316wcCZhq279FWj7Kz2zwGDKA1aMFgQ8Kv5UdH566xW8CXMK78
5MLs1FNvaKj4Q_04vR-p7UTD5EMXIVJq6xAE8webTdBrebdOrIlKx4CVXmovyGAJGQicVmuTHIshz
._mw0nYAfCIAD-1eqto33jw" -v -X GET  
http://localhost:8080/JweSecurityExample/rest/security/showallitems

*   Trying ::1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /JweSecurityExample/rest/security/showallitems HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.48.0
> Accept: */*
> Content-Type: application/json
> token: eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjpudWxsLCJjdHki
OiJKV1QifQ..upoa4h2LsxIbPfEv2gMmjQ.e0OCTvkFPOy5LVlcqlstOz3ZlAZXjymHQNdcYQh7O-
hh4jJEeQDPaHXafvysU_OEImB9qPrp1uDfp0ZdDh6ISvZxARIPRtJ-GyFi_X0fuBpacF7FAhu7uOb
EBl60QulWS4msJLOKt3NE2g-Xskl1-3_SGhdbku5w0wQ66_TkZGS9DXtpCP5emeEvOdJKsJ00Bbd0
Fw1eAxWvsvSD9dFPG6qJhKR-V4tXN4semV17c7uH6INWzsaVCAkqGzrcrsZNL316wcCZhq279FWj7
Kz2zwGDKA1aMFgQ8Kv5UdH566xW8CXMK785MLs1FNvaKj4Q_04vR-p7UTD5EMXIVJq6xAE8webTdB
rebdOrIlKx4CVXmovyGAJGQicVmuTHIshz._mw0nYAfCIAD-1eqto33jw
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 12 Mar 2016 16:17:34 GMT
<
[{"_id":"10029T1","item-id":"123","item-name":"KitchenAid Artisan 5 qt. 
Stand Mixer","price":314.99,"quantity":13},{"_id":"12349K1","item-id":
"k10001","item-name":"Keurig K10 MINI Plus Brewing System","price":
79.99,"quantity":36},{"_id":"83949PT","item-id":"EPP1029","item-name":
"Electric Power Pressure Cooker XL (8 qt)","price":119.99,"quantity":8},
{"_id":"71829Y","item-id":"IQ50009","item-name":"KitchenIQ 50009 Edge 
Grip 2 Stage Knife Sharpener, Black","price":5.79,"quantity":23},{"_id":
"30814B","item-id":"3081414B","item-name":"La Crosse Technology 
308-1414B Wireless Atomic Digital Color Forecast Station","price":49.99,
"quantity":6},{"_id":"PAN110CFM","item-id":"110CFM","item-name":
"Panasonic FV-11VQ5 WhisperCeiling 110 CFM Ceiling Mounted Fan, White",
"price":113.79,"quantity":7},{"_id":"AS4175","item-id":"AS4175",
"item-name":"American Standard 4175.300.075 Colony Soft Pull-Down 
Kitchen Faucet, Stainless Steel","price":120.0,"quantity":9},{"_id":
"FM3700B","item-id":"FM3700B","item-name":"PUR Advanced Faucet Water 
Filter Chrome FM-3700B","price":23.95,"quantity":27},{"_id":"ARC150SB",
"item-id":"ARC150SB","item-name":"Aroma 20 Cup Cooked (10 cup uncooked) 
Digital Rice Cooker, Slow Cooker","price":36.99,"quantity":13},{"_id":
"CPT180TST","item-id":"CPT180TST","item-name":"Cuisinart Metal Classic 
4-Slice Toaster","price":69.99,"quantity":6},{"_id":"GR4NWPAN","item-id":
"GR4NWPAN","item-name":"Cuisinart Griddler and Waffle Maker with Removable
 Plates","price":99.99,"quantity":13}]
* Connection #0 to host localhost left intact
------------------------------------------------------------

C:\curl>curl -H "Content-Type: application/json" -H "token: eyJhbGciO
iJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjpudWxsLCJjdHkiOiJKV1Qif
Q..h-o8owUQfhL22Q414bk3SQ.Rb-5FvjjZ6hD80MH4t2sMGTqWSoAyYFHBmsW_5YG5dA
V7ZgVEHMhXZHdQaSX_ijCEQYfjp_uKCubnATH48BmP3FauWg0_1u-Nbmbedyy2Cefx1UE
7SebOK9P77HNu54NVJtLNu_WqCMCmnEroa1yJ34KSD3i2wFUyASP3n6nU8v7YJp6ySf8A
utrB6_vJJP3goVeHWIir-pqaE1VmQ_ub7cWXJaM_8dJDk9C6qHP0Qf2ZqqhzuN6xY-NO_
DKRA6WDcCNfXkD5uwDJjQjt2y9lwCObb7YXkeWVlSP0UqOSsSPZ8KegBh1odYhyIj0cA
oEdEgKsLR_ZNsk1sGne6Bj51kHfw82nGfXUpdoSR0U-Tg7GiCUDT6iQ7AAMDUX-wj_.5H
THvY-sxMUZgj254jJ1Kw" -v -X GET  
http://localhost:8080/JweSecurityExample/rest/security/getallroles

*   Trying ::1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /JweSecurityExample/rest/security/getallroles HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.48.0
> Accept: */*
> Content-Type: application/json
> token: eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjpudWx
sLCJjdHkiOiJKV1QifQ..h-o8owUQfhL22Q414bk3SQ.Rb-5FvjjZ6hD80MH4t2sMGTq
WSoAyYFHBmsW_5YG5dAV7ZgVEHMhXZHdQaSX_ijCEQYfjp_uKCubnATH48BmP3FauWg0
_1u-Nbmbedyy2Cefx1UE7SebOK9P77HNu54NVJtLNu_WqCMCmnEroa1yJ34KSD3i2wFU
yASP3n6nU8v7YJp6ySf8AutrB6_vJJP3goVeHWIir-pqaE1VmQ_ub7cWXJaM_8dJDk9C
6qHP0Qf2ZqqhzuN6xY-NO_DKRA6WDcCNfXkD5uwDJjQjt2y9lwCObb7YXkeWVlSP0UqO
SsSPZ8KegBh1odYhyIj0cAoEdEgKsLR_ZNsk1sGne6Bj51kHfw82nGfXUpdoSR0U-Tg7
GiCUDT6iQ7AAMDUX-wj_.5HTHvY-sxMUZgj254jJ1Kw
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Date: Sat, 12 Mar 2016 16:41:53 GMT
<
["client","admin"]
* Connection #0 to host localhost left intact

Testing Application and POSTMAN Chrome Extension

Download the Complete Source Code

That’s It!

I hope you enjoyed this tutorial. It was certainly a lot of fun putting it together and testing it out. Please continue to share the love and like us so that we can continue bringing you quality tutorials. Happy Coding!!!

java_jaxrs_jwe_security

Please Share Us on Social Media

Facebooktwitterredditpinterestlinkedinmail

Leave a Reply

Your email address will not be published. Required fields are marked *