In the first part of this series, we looked at building a very minimal Restlet application using Spring configuration. In this article, we’re going to add security. Specifically, Amazon Web Services style security. As before, I’m trying to hit on some points that I didn’t find to be well-covered elsewhere, especially using AWS security from a Restlet-based client to talk to AWS. There are also a lot of examples using simple username / password security for Restlet server apps. But you can also use AWS security in your own server applications, and that’s what I’m describing here.
Even though it took a bit of digging to figure out how to make it work, in the end it’s really very easy requiring minimal changes to the server code. However note that to test this we’ll need to write a client application as well. Trying to use a browser-based REST client will be way too difficult.
As before, the sample code is available at GitHub under the tag restlet-server-with-aws-security
First, we need an API key associated with the user making the call. To keep things simple, we’re just going to add an apiKey field to the User object, but it will only be set for a new “admin” user we’ll add with an ID of 0. In a real application this key would likely be generated and saved in the database.
package com.eddiesheffield.restlet.server.data; public class User { private String name; private int age; private String favoriteFood; private String apiKey; private int id; public User(int id, String name, int age, String favoriteFood) { this(id, name, age, favoriteFood, null); } public User(int id, String name, int age, String favoriteFood, String apiKey) { this.id = id; this.name = name; this.age = age; this.favoriteFood = favoriteFood; this.apiKey = apiKey; } ... public String getApiKey() { return apiKey; } }
The UserRepository functionality is unchanged. But in the constructor we now add the new admin user along with a general user just to give us some data to fetch. As we still have no error handling, requesting a non-existant resource causes an exception in the server.
public class UserRepository { private Map<Integer, User> users; private int maxId = 0; public UserRepository() { this.users = new HashMap<Integer, User>(); // Add an admin user String apiKey = "0123456789ABCDEF0123456789ABCDEF01234567"; User user = new User(0, "admin", 0, "", apiKey); users.put(0, user); // Add an initial user addUser("Eddie", 29, "Cheez Whizz"); } public User addUser(String name, int age, String favoriteFood) { maxId++; User user = new User(maxId, name, age, favoriteFood); users.put(user.getId(), user); return user; } ... }
The last server-side Java change is the addition of the AwsApiKeyVerifier class. To understand this class, a little discussion of Restlet and AWS security is in order.
Restlet security works using “guards” which protect resources. You insert a guard in the request chain before the resource(s) to be protected. In this case we’ll be using the org.restlet.ext.crypto.AwsAuthenticator which is part of the crypto extension package. With AWS security, the client generates a signature for the request based on the request message and the API key. The request, including the signature but NOT the API key, is then sent to the server. The server takes the request, looks up the API key for the user issuing the request, and calculates the signature. If the calculated and received signatures match, the request is processed normally. If they don’t match, the request is denied and a 401 Unauthorized status is returned. To lookup the API key, the AwsAuthenticator uses an implementation of org.restlet.security.LocalVerifier. That’s where AwsApiKeyVerifier comes into play. Given the requestor’s identifier, it looks up the user and returns his API key.
As you can see, this is very straightforward.
package com.eddiesheffield.restlet.server.security; import org.restlet.security.LocalVerifier; import com.eddiesheffield.restlet.server.data.User; import com.eddiesheffield.restlet.server.data.UserRepository; public class AwsApiKeyVerifier extends LocalVerifier { private UserRepository repository; public void setRepository(UserRepository repository) { this.repository = repository; } @Override public char[] getLocalSecret(String identifier) { User user = repository.getUser(Integer.valueOf(identifier)); String apiKey = user.getApiKey(); return apiKey.toCharArray(); } }
The rest of the server changes are in the Spring configuration files. The vast majority of those changes are in the applicationContext-resources.xml file.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="userRepository" class="com.eddiesheffield.restlet.server.data.UserRepository"> </bean> <bean id="awsVerifier" class="com.eddiesheffield.restlet.server.security.AwsApiKeyVerifier" scope="prototype"> <property name="repository" ref="userRepository" /> </bean> <bean id="restletContext" class="org.restlet.Context" factory-method="getCurrent" /> <bean id="guard" class="org.restlet.ext.crypto.AwsAuthenticator" scope="prototype"> <constructor-arg ref="restletContext" /> <constructor-arg value="false" /> <constructor-arg value="eddiesheffield.com" /> <property name="wrappedVerifier" ref="awsVerifier" /> <property name="next" ref="root" /> </bean> <bean id="usersResource" class="com.eddiesheffield.restlet.server.resources.UsersResource" scope="prototype" autowire="constructor"> </bean> <bean id="userResource" class="com.eddiesheffield.restlet.server.resources.UserResource" scope="prototype" autowire="constructor"> </bean> </beans>
The awsVerifer bean has a simple definition, taking only the userRepository reference. It is also defined using scope=”prototype” as described last time, though this may not be necessary in this case.
The “guard” bean is the AwsAuthenticator instance. There are several items of interest in this definition. We’ll skip the “restletContext” parameter for the moment. The second constructor param with the value “false” indicates that this guard is not optional and authentication must succeed. The third param is the realm. For this example the value doesn’t really matter – we just set it to something unique – in this case “eddiesheffield.com”. There are two other properties that must be set. “wrappedVerifier” takes the reference to the LocalVerifier instance to be used by the authenticator – the awsVerifier. Be careful here – authenticator classes have a “verifier” property, but don’t use that for the AwsAuthenticator. It has its own internal Verifier implementation which you almost certainly don’t want to override. The last property “next” defines the next handler in the chain where the guard should send requests once authentication succeeds.
Coming back to the first parameter – the “restletContext”. The tricky part is that the constructor requires a org.restlet.Context instance to be passed in. This Context is created and managed by the Restlet framework and thus can’t be injected directly. However it can be accessed by the static Context.getCurrent() method. So we define the restletContext bean using the Spring “factory-method” attribute. Whenever an AwsAuthenticator is instantiated, the factory-method (in this case “getCurrent”) is called to fetch the current Context instance and injects it into the new AwsAuthenticator.
The last update to the server is a minor change to the applicationContext-server.xml file.
... <bean id="top" class="org.restlet.ext.spring.SpringComponent"> <property name="server"> <bean class="org.restlet.ext.spring.SpringServer"> <constructor-arg value="http" /> <constructor-arg value="3000" /> </bean> </property> <property name="defaultTarget" ref="guard" /> </bean> ...
The only change here is to the “defaultTarget” property. Rather than pointing to “root” it now points to “guard”. This causes requests to be first routed through the guard class before going to root.
That’s it for the server. Unlike last time where I used the Chrome extension Advanced REST Client, this time I have included a separate client application. Trying to handle the security manually through a browser client would be virtually impossible.
package com.eddiesheffield.restlet.client; import java.io.IOException; import org.restlet.Client; import org.restlet.Request; import org.restlet.Response; import org.restlet.data.ChallengeResponse; import org.restlet.data.ChallengeScheme; import org.restlet.data.Form; import org.restlet.data.Method; import org.restlet.data.Protocol; import org.restlet.representation.Representation; public class ClientApp { public static void main(String[] args) throws IOException { ChallengeResponse authentication = createChallengeResponse(0, "0123456789ABCDEF0123456789ABCDEF01234567"); Request request = createGetUserRequest(authentication, 1); executeRequest(request); request = createNewUserRequest(authentication, "Frank", 38, "Vegemite"); executeRequest(request); } private static Request createGetUserRequest( ChallengeResponse authentication, int i) { // Prepare the request Request request = new Request(Method.GET, "http://localhost:3000/users/" + i); // Set the AWS ChallengeResponse on the request request.setChallengeResponse(authentication); return request; } private static void executeRequest(Request request) throws IOException { Client client = new Client(Protocol.HTTP); Response response = client.handle(request); // Write the response entity on the console Representation output = response.getEntity(); System.out.println("Status: " + response.getStatus()); output.write(System.out); } private static Request createNewUserRequest(ChallengeResponse authentication, String name, int age, String favoriteFood) { // Prepare the request Request request = new Request(Method.POST, "http://localhost:3000/users"); // Set the AWS ChallengeResponse on the request request.setChallengeResponse(authentication); // Create the data Representation and add to request Representation representation = getNewUserRepresentation(name, age, favoriteFood); request.setEntity(representation); return request; } private static ChallengeResponse createChallengeResponse(int callerId, String apiKey) { // Create the AWS ChallengeResponse ChallengeScheme scheme = ChallengeScheme.HTTP_AWS_S3; ChallengeResponse authentication = new ChallengeResponse(scheme, String.valueOf(callerId), apiKey); return authentication; } private static Representation getNewUserRepresentation(String name, int age, String favoriteFood) { Form form = new Form(); form.add("name", name); form.add("age", String.valueOf(age)); form.add("favoriteFood", favoriteFood); return form.getWebRepresentation(); } }
For the most part, this is standard Restlet client code. It simply looks up the user resource with ID 1, and then adds a new user, each time printing the results. The part we’re most interested in here is the security related code. Restlet clients pass security credentials by setting a ChallengeResponse object on the request before submitting it. The ChallengeResponse is created in the createChallengeResponse method. For AWS security, the ChallengeResponse is created with a scheme of ChallengeScheme.HTTP_AWS_S3, the identifier of the user making the call (the “admin” user we hard-coded in the server), and the secret which in this case is the API key.
As before, build the project with mvn clean install and start the server with java -jar target\RestletSpringServer-0.0.1-SNAPSHOT.jar.
This also builds the client in the same jar file, but you’ll have to be more explicit starting it. Once the server has started, run the client with java -cp .\target\RestletSpringServer-0.0.1-SNAPSHOT.jar com.eddiesheffield.restlet.client.ClientApp.