RESTful Java with JAX-RS 2.0 (2013)
Part II. JAX-RS Workbook
Chapter 29. Examples for Chapter 15
The chapter goes over some example code that illustrates a few of the concepts and APIs you were introduced to in Chapter 15. In the first example, you’ll write two custom security plug-ins. In the second example, you’ll use JSON Web Encryption to add more security to a chat application.
Example ex15_1: Custom Security
In the first example, we will write two custom security features using JAX-RS filters. The first feature is a custom authentication protocol. The second will be a custom access policy. The example applies these security features to the code we wrote in ex06_1.
One-Time Password Authentication
The first custom security feature we’ll write is one-time password (OTP) authentication. The client will use a credential that changes once per minute. This credential will be a hash that we generate by combining a static password with the current time in minutes. The client will send this generated one-time password in the Authorization header. For example:
GET/customersHTTP/1.1
Authorization:<username> <generated_password>
The header will contain the username of the user followed by the one-time password.
The server code
We will enforce OTP authentication only on JAX-RS methods annotated with the @OTPAuthenticated annotation:
src/main/java/com/restfully/shop/features/OTPAuthenticated.java
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@NameBinding
public@interfaceOTPAuthenticated
{
}
When declared on a JAX-RS method, this annotation will trigger the binding of a ContainerRequestFilter that implements the OTP algorithm using the @NameBinding technique discussed in Name Bindings. To apply a name binding, the OTPAuthenticated annotation interface is annotated with @NameBinding.
With our custom annotation defined, let’s take a look at the filter that implements the OTP algorithm:
src/main/java/com/restfuly/shop/features/OneTimePasswordAuthenticator.java
@OTPAuthenticated
@Priority(Priorities.AUTHENTICATION)
publicclassOneTimePasswordAuthenticatorimplementsContainerRequestFilter
{
The OneTimePasswordAuthenticator class is annotated with @OTPAuthenticated. This completes the @NameBinding we started when we implemented the @OTPAuthenticated annotation interface. The class is also annotated with @Priority. This annotation affects the ordering of filters as they are applied to a JAX-RS method. We’ll discuss specifically why we need this later in the chapter, but you usually want authentication filters to run before any other filter.
protectedMap<String,String>userSecretMap;
publicOneTimePasswordAuthenticator(Map<String,String>userSecretMap)
{
this.userSecretMap=userSecretMap;
}
Our filter will be a singleton object and will be initialized with a map. The key of the map will be a username, while the value will be the secret password used by the user to create a one-time password.
@Override
publicvoidfilter(ContainerRequestContextrequestContext)throwsIOException
{
Stringauthorization=requestContext.getHeaderString(
HttpHeaders.AUTHORIZATION);
if(authorization==null)thrownewNotAuthorizedException("OTP");
String[]split=authorization.split(" ");
finalStringuser=split[0];
Stringotp=split[1];
In the first part of our filter() method, we parse the Authorization header that was sent by the client. The username and encoded password are extracted from the header into the user and otp variables.
Stringsecret=userSecretMap.get(user);
if(secret==null)thrownewNotAuthorizedException("OTP");
Stringregen=OTP.generateToken(secret);
if(!regen.equals(otp))thrownewNotAuthorizedException("OTP");
Next, our filter() method looks up the secret of the user in its map and generates its own one-time password. This token is compared to the value sent in the Authorization header. If they match, then the user is authenticated. If the user does not exist or the one-time password is not validated, then a 401, “Not Authorized,” response is sent back to the client.
finalSecurityContextsecurityContext=
requestContext.getSecurityContext();
requestContext.setSecurityContext(newSecurityContext()
{
@Override
publicPrincipalgetUserPrincipal()
{
returnnewPrincipal()
{
@Override
publicStringgetName()
{
returnuser;
}
};
}
@Override
publicbooleanisUserInRole(Stringrole)
{
returnfalse;
}
@Override
publicbooleanisSecure()
{
returnsecurityContext.isSecure();
}
@Override
publicStringgetAuthenticationScheme()
{
return"OTP";
}
});
After the user is authenticated, the filter() method creates a custom SecurityContext implementation within an inner anonymous class. It then overrides the existing SecurityContext by calling ContainerRequestContext.setSecurityContext(). TheSecurityContext.getUserPrincipal() is implemented to return a Principal initialized with the username sent in the Authorization header. Other JAX-RS code can now inject this custom SecurityContext to find out who the user principal is.
The algorithm for generating a one-time password is pretty simple. Let’s take a look:
src/main/java/com/restfully/shop/features/OTP.java
publicclassOTP
{
publicstaticStringgenerateToken(Stringsecret)
{
longminutes=System.currentTimeMillis()/1000/60;
Stringconcat=secret+minutes;
MessageDigestdigest=null;
try
{
digest=MessageDigest.getInstance("MD5");
}
catch(NoSuchAlgorithmExceptione)
{
thrownewIllegalArgumentException(e);
}
byte[]hash=digest.digest(concat.getBytes(Charset.forName("UTF-8")));
returnBase64.encodeBytes(hash);
}
}
OTP is a simple class. It takes any arbitrary password and combines it with the current time in minutes to generate a new String object. An MD5 hash is done on this String object. The hash bytes are then Base 64–encoded using a RESTEasy-specific library and returned as a String.
The @OTPAuthenticated annotation is then applied to two methods in the CustomerResource class to secure access to them:
src/main/java/com/restfully/shop/services/CustomerResource.java
@GET
@Path("{id}")
@Produces("application/xml")
@OTPAuthenticated
publicCustomergetCustomer(@PathParam("id")intid)
{
...
}
@PUT
@Path("{id}")
@Consumes("application/xml")
@OTPAuthenticated
@AllowedPerDay(1)
publicvoidupdateCustomer(@PathParam("id")intid,Customerupdate)
{
...
}
The getCustomer() and updateCustomer() methods are now required to be OTP authenticated.
Allowed-per-Day Access Policy
The next custom security feature we’ll implement is an allowed-per-day access policy. The idea is that for a certain JAX-RS method, we’ll specify how many times each user is allowed to execute that method per day. We will do this by applying the @AllowedPerDay annotation to a JAX-RS method:
src/main/java/com/restfuly/shop/features/AllowedPerDay.java
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@NameBinding
public@interfaceAllowedPerDay
{
intvalue();
}
As with @OTPAuthenticated, we’ll use a @NameBinding to bind the annotation to a specific ContainerRequestFilter. Let’s take a look at that filter:
src/main/java/com/restfuly/shop/features/PerDayAuthorizer.java
@AllowedPerDay(0)
@Priority(Priorities.AUTHORIZATION)
publicclassPerDayAuthorizerimplementsContainerRequestFilter
{
The PerDayAuthorizer class is annotated with @AllowedPerDay. This completes the @NameBinding we started when we implemented the @AllowedPerDay annotation interface. The class is also annotated with @Priority. This annotation affects the ordering of filters as they are applied to a JAX-RS method. We want this filter to run after any authentication code, but before any application code, as we are figuring out whether or not a user is allowed to invoke the request. If we did not annotate the OneTimePasswordAuthenticator and PerDayAuthorizerclasses with the @Priority annotation, it is possible that the PerDayAuthorizer would be invoked before the OneTimePasswordAuthenticator filter. The PerDayAuthorizer needs to know the authenticated user created in the OneTimePasswordAuthenticator filter; otherwise, it won’t work.
@Context
ResourceInfoinfo;
We inject a ResourceInfo instance into the filter instance using the @Context annotation. We’ll need this variable to know the current JAX-RS method that is being invoked.
publicvoidfilter(ContainerRequestContextrequestContext)throwsIOException
{
SecurityContextsc=requestContext.getSecurityContext();
if(sc==null)thrownewForbiddenException();
Principalprincipal=sc.getUserPrincipal();
if(principal==null)thrownewForbiddenException();
Stringuser=principal.getName();
The filter() method first obtains the SecurityContext from the ContainerRequestContext.getSecurityContext() method. If the context is null or the user principal is null, it returns a 403, “Forbidden,” response to the client by throwing a ForbiddenException.
if(!authorized(user))
{
thrownewForbiddenException();
}
}
The username value is passed to the authorized() method to check the permission. If the method returns false, a 401, “Forbidden,” response is sent back to the client via a ForbiddenException.
protectedstaticclassUserMethodKey
{
Stringusername;
Methodmethod;
publicUserMethodKey(Stringusername,Methodmethod)
{
this.username=username;
this.method=method;
}
@Override
publicbooleanequals(Objecto)
{
if(this==o)returntrue;
if(o==null||getClass()!=o.getClass())returnfalse;
UserMethodKeythat=(UserMethodKey)o;
if(!method.equals(that.method))returnfalse;
if(!username.equals(that.username))returnfalse;
returntrue;
}
@Override
publicinthashCode()
{
intresult=username.hashCode();
result=31*result+method.hashCode();
returnresult;
}
}
protectedMap<UserMethodKey,Integer>count=
newHashMap<UserMethodKey,Integer>();
The filter instance remembers how many times in a day a particular user invoked a particular JAX-RS method. It stores this information in the count variable map. This map is keyed by a custom UserMethodKey class, which contains the username and JAX-RS method that is being tracked.
protectedlongtoday=System.currentTimeMillis();
protectedsynchronizedbooleanauthorized(Stringuser,AllowedPerDayallowed)
{
if(System.currentTimeMillis()>today+(24*60*60*1000))
{
today=System.currentTimeMillis();
count.clear();
}
The authorized() method is synchronized, as this filter may be concurrently accessed and we need to do this policy check atomically. It first checks to see if a day has elapsed. If so, it resets the today variable and clears the count map.
UserMethodKeykey=newUserMethodKey(user,info.getResourceMethod());
Integercounter=count.get(user);
if(counter==null)
{
counter=0;
}
The authorized() method then checks to see if the current user and method are already being tracked and counted.
AllowedPerDayallowed=
info.getResourceMethod().getAnnotation(AllowedPerDay.class);
if(allowed.value()>counter)
{
count.put(user,counter+1);
returntrue;
}
returnfalse;
}
}
The method then extracts the AllowedPerDay annotation from the current JAX-RS method that is being invoked. This annotation will contain the number of times per day that a user is allowed to invoke the current JAX-RS method. If this value is greater than the current count for that user for that method, then we update the counter and return true. Otherwise, the policy check has failed and we return false.
We then apply this functionality to a JAX-RS resource method by using the @AllowedPerDay annotation:
src/main/java/com/restfully/shop/services/CustomerResource.java
@PUT
@Path("{id}")
@Consumes("application/xml")
@OTPAuthenticated
@AllowedPerDay(1)
publicvoidupdateCustomer(@PathParam("id")intid,Customerupdate)
{
...
}
A user will now only be able to invoke the updateCustomer() method once per day.
The last thing we have to do is initialize our deployment. Our Application class needs to change a little bit to enable this:
src/main/java/com/restfully/shop/services/ShoppingApplication/java
@ApplicationPath("/services")
publicclassShoppingApplicationextendsApplication
{
privateSet<Object>singletons=newHashSet<Object>();
publicShoppingApplication()
{
singletons.add(newCustomerResource());
HashMap<String,String>userSecretMap=newHashMap<String,String>();
userSecretMap.put("bburke","geheim");
singletons.add(newOneTimePasswordAuthenticator(userSecretMap));
singletons.add(newPerDayAuthorizer());
}
@Override
publicSet<Object>getSingletons()
{
returnsingletons;
}
}
The ShoppingApplication class populates the user-secret map that must be used to construct the singleton OneTimePasswordAuthenticator instance. The PerDayAuthorizer class is also a singleton and instantiated by this constructor.
The client code
The first thing we do on the client side is to implement a ClientRequestFilter that sets up the Authorization header that will be sent to the server:
src/main/java/com/restfully/shop/features/OneTimePasswordGenerator.java
publicclassOneTimePasswordGeneratorimplementsClientRequestFilter
{
protectedStringuser;
protectedStringsecret;
publicOneTimePasswordGenerator(Stringuser,Stringsecret)
{
this.user=user;
this.secret=secret;
}
@Override
publicvoidfilter(ClientRequestContextrequestContext)throwsIOException
{
Stringotp=OTP.generateToken(secret);
requestContext.getHeaders().putSingle
(HttpHeaders.AUTHORIZATION,user+" "+otp);
}
}
This filter is very simple. It is constructed with the username and password we will use to generate the one-time password. The filter() method generates the one-time password by calling the OTP.generateToken() method we described earlier in this chapter. The filter() method then generates and sets the Authorization header for the HTTP request.
The client test code is the same as ex06_1 except that we set it up to use OTP authentication. Let’s take a look:
src/test/java/com/restfully/shop/test/CustomerResourceTest.java
@Test
publicvoidtestCustomerResource()throwsException
{
System.out.println("*** Create a new Customer ***");
CustomernewCustomer=newCustomer();
newCustomer.setFirstName("Bill");
newCustomer.setLastName("Burke");
newCustomer.setStreet("256 Clarendon Street");
newCustomer.setCity("Boston");
newCustomer.setState("MA");
newCustomer.setZip("02115");
newCustomer.setCountry("USA");
Responseresponse=client.target(
"http://localhost:8080/services/customers")
.request().post(Entity.xml(newCustomer));
if(response.getStatus()!=201)thrownewRuntimeException
("Failed to create");
Stringlocation=response.getLocation().toString();
System.out.println("Location: "+location);
response.close();
The testCustomerResource() method starts off the same way as in ex06_1. It creates a customer and obtains its URI from the response. Creating a customer is not authenticated so we do not need to worry about setting up authorization here.
System.out.println("*** GET Created Customer **");
Customercustomer=null;
WebTargettarget=client.target(location);
try
{
customer=target.request().get(Customer.class);
Assert.fail();// should have thrown an exception
}
catch(NotAuthorizedExceptione)
{
}
This particular code shows what happens when an unauthenticated request is made. It makes a GET request on the new customer’s URI that fails with a NotAuthorizedException because we have not set up our OTP filter yet.
target.register(newOneTimePasswordGenerator("bburke","geheim"));
We register an instance of our OneTimePasswordGenerator filter initialized with our username and static password. We can now make an authenticated GET request without error.
customer=target.request().get(Customer.class);
System.out.println(customer);
To show our allowed-per-day policy in action, the code executes a customer update twice.
customer.setFirstName("William");
response=target.request().put(Entity.xml(customer));
if(response.getStatus()!=204)
thrownewRuntimeException("Failed to update");
++++
<?hard-pagebreak?>
++++
// Show the update
System.out.println("**** After Update ***");
customer=target.request().get(Customer.class);
System.out.println(customer);
// only allowed to update once per day
customer.setFirstName("Bill");
response=target.request().put(Entity.xml(customer));
Assert.assertEquals(Response.Status.FORBIDDEN,response.getStatusInfo());
}
The first invocation succeeds, but the second fails because we are allowed to invoke this method only once per day.
Build and Run the Example Program
Perform the following steps:
1. Open a command prompt or shell terminal and change to the ex15_1 directory of the workbook example code.
2. Make sure your PATH is set up to include both the JDK and Maven, as described in Chapter 17.
3. Perform the build and run the example by typing maven install.
Example ex15_1: JSON Web Encryption
In Chapter 15, you learned a little bit about JSON Web Encryption (JWE) and how it can be used to encrypt HTTP message body or header values. This example augments the customer chat client implemented in Chapter 27. Chat clients will use a shared secret to encrypt and decrypt the messages they send to and receive from the chat server. Chat clients that know the shared secret see the decrypted message, while clients that don’t know it see only the JWE encoding. Let’s take a look at the code:
src/main/java/ChatClient.java
publicclassChatClient
{
publicstaticvoidmain(String[]args)throwsException
{
Stringname=args[0];
finalStringsecret=args[1];
The ChatClient first starts out by storing the name and secret password that the client will use. It obtains these values from the command line.
finalClientclient=newResteasyClientBuilder()
.connectionPoolSize(3)
.build();
WebTargettarget=client.target("http://localhost:8080/services/chat");
target.request().async().get(newInvocationCallback<Response>()
{
@Override
publicvoidcompleted(Responseresponse)
{
Linknext=response.getLink("next");
Stringmessage=response.readEntity(String.class);
try
{
JWEInputencrypted=newJWEInput(message);
message=encrypted.decrypt(secret).readContent(String.class);
}
catch(Exceptionignore)
{
//e.printStackTrace();
}
System.out.println();
System.out.print(message);
System.out.println();
System.out.print("> ");
client.target(next).request().async().get(this);
}
@Override
publicvoidfailed(Throwablethrowable)
{
System.err.println("FAILURE!");
}
});
The code then implements the receive loop we discussed in Chapter 27. The difference is that it uses the RESTEasy org.jboss.resteasy.jose.jwe.JWEInput class to decrypt the received message. A JWEInput instance is initialized with the received text message. TheJWEInput.decrypt() method decrypts the JWE with the shared secret. The readContext() method extracts the decrypted bytes into a String object that we can output to the console. If the message is not a JWE or if the wrong secret is used, then the original received text message is outputted to the console.
Let’s now take a look at how sending a message has changed:
while(true)
{
System.out.print("> ");
BufferedReaderbr=newBufferedReader
(newInputStreamReader(System.in));
Stringmessage=name+": "+br.readLine();
Stringencrypted=newJWEBuilder()
.contentType(MediaType.TEXT_PLAIN_TYPE)
.content(message)
.dir(secret);
target.request().post(Entity.text(encrypted));
}
This while loop is similar to the code discussed in Chapter 27. The difference is that it uses the RESTEasy org.jboss.resteasy.jose.jwe.JWEBuilder class to encrypt the text message we want to post to the server. The JWEBuilder.contentType() method sets the cty header of the JWE. The content() method sets the entity we want to encrypt. The dir() method first takes the entity and marshals it using a MessageBodyReader picked from the content type and the entity’s class. The dir() method then generates the JWE based on this marshalled content and shared secret algorithm. Once we have our JWE-encoded string, we then post it to the chat server.
One thing to notice is that we have not changed the server at all. The server is a dumb intermediary that just forwards messages from one client to others. It doesn’t care about what is being sent across the wire.
Build and Run the Example Program
You’ll need multiple console windows to run this example. In the first console window, perform the following steps:
1. Change to the ex15_2 directory of the workbook example code.
2. Make sure your PATH is set up to include both the JDK and Maven, as described in Chapter 17.
3. Perform the build and run the example by typing maven jetty:run.
This will start the JAX-RS services for the example.
Open another console window and do the following.
1. Change to the ex15_2 directory of the workbook example code.
2. Run the chat client by typing maven exec:java -Dexec.mainClass=ChatClient -Dexec.args="your-name your-secret".
Replace your-name with your first name and your-secret with your shared password. Repeat this process in yet another console window to run a second chat client. You may also want to start different chat clients that use different passwords to see what happens.
All materials on the site are licensed Creative Commons Attribution-Sharealike 3.0 Unported CC BY-SA 3.0 & GNU Free Documentation License (GFDL)
If you are the copyright holder of any material contained on our site and intend to remove it, please contact our site administrator for approval.
© 2016-2026 All site design rights belong to S.Y.A.