- Explain the basics of WebScript authentication
- Secure our Safari REST API by writing an authentication factory that maps Spring WebScript authentication roles to the Safari’s company’s user groups.
- Write 3 new secured webscripts: Create Trip, Create Booking & List User’s Bookings that we can use in future when we create the Safari web shop ui.
- Test that the WebScripts are secured
If you are only interested to get a quick glance at WebScript security, and don’t want to bother about all the Safari use case code, consider only reading the following sections: “Spring WebScript Security Basics”, “Securing the Spring WebScript runtime” and possibly “Using HTTP Sessions in Spring WebScripts?”.
If you want to download the sample code you can do that from: Safari – Spring WebScripts REST API sample #2.
If .
Spring WebScript Security Basics
.
Spring WebScripts can be hosted inside a number of different runtime environments:
- Servlet Runtime (HTTP Access)
- JSR-168 Runtime (Portlet Access)
- JSF Runtime (JSF Component Access)
.As you have probaly guessed the Safari REST API is using the Servlet Runtime, something we declared by defining the WebScriptServlet in safari-rest/src/main/webapp/WEB-INF/web.xml in the last blog post. All runtimes support 4 roles or levels of authentication which is declared in the <authentication> element in each webscript’s decriptor file (some-webscript.get.desc.xml):
- “none” – No authentication is required (default)
- “guest” – No authentication is required but the webscript could get access to additional read services
- “user” – Authentication is required
- “admin” – Authentication is required by a user with the admin role
The default authentication level, if the <authentication> element is omitted, is “none”..
The “use case” for the Safari Company
.
- “erik” – A registered user and therefore a member of the “customer” group.
- “roy” – An employee and therefore a member of the “manager” group but sometimes also buys trips and therefore is a member of the “customer” group as well.
<!-- Safari Services --> <bean id="travelService" class="com.safari.core.travel.TravelService"/> <bean id="identityService" class="com.safari.core.travel.IdentityService"/
package com.safari.core.travel;
import com.safari.core.SafariContext;
import java.util.ArrayList;
import java.util.List;
/**
* In memory service for demo and testing purposes
*/
public class IdentityService {
public static final String MANAGER = "manager";
public static final String CUSTOMER = "customer";
public boolean authenticate(String username, String password) {
// Perform simple example authentication
if (username.equals(password)) {
SafariContext.setCurrentUser(username);
return true;
}
return false;
}
public List<String> getUsers() {
List<String> users = new ArrayList<String>();
users.add("erik"); // A registered customer
users.add("roy"); // Sales manager for the company who sometimes buy trips
return users;
}
public List<String> getGroups(String user) {
List<String> groups = new ArrayList<String>();
if (user.equals("erik")) {
groups.add(CUSTOMER);
}
else if (user.equals("roy")) {
groups.add(MANAGER);
groups.add(CUSTOMER);
}
return groups;
}
}
package com.safari.core;
public abstract class SafariContext {
static ThreadLocal<String> authenticatedUserNameThreadLocal = new ThreadLocal<String>();
public static void setCurrentUser(String userName) {
authenticatedUserNameThreadLocal.set(userName);
}
public static String getCurrentUser() {
return authenticatedUserNameThreadLocal.get();
}
}
Securing the Spring WebScript runtime
As mentioned before the Safari REST API was configured to use the WebScript Servlet Runtime. To add authentication to it simply add the “authenticator” init-param to the WebScriptServlet and safari-rest/src/main/webapp/WEB-INF/web.xml like below:
<!-- Spring WebScripts --> <servlet> <servlet-name>WebScriptServlet</servlet-name> <servlet-class>org.springframework.extensions.webscripts.servlet.WebScriptServlet</servlet-class> <init-param> <param-name>authenticator</param-name> <param-value>webscripts.authenticator.safari</param-value> </init-param> </servlet>
This is done by simply extending the org.springframework.extensions.webscripts.AbstractBasicHttpAuthenticatorFactory class (which will do the work of decoding the HTTP headers for us) and respond to its 2 abstract methods: doAuthenticate(String username, String password) and doAuthorize(String username, RequiredAuthentication role).
Name it SafariBasicHttpAuthenticatorFactory, place it in safari-rest/src/main/java/com/safari/rest/auth/ and make it look like below:
package com.safari.rest.auth;
import com.safari.core.travel.IdentityService;
import org.springframework.extensions.webscripts.AbstractBasicHttpAuthenticatorFactory;
import org.springframework.extensions.webscripts.Description;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;
import java.util.ArrayList;
import java.util.List;
public class SafariBasicHttpAuthenticatorFactory extends AbstractBasicHttpAuthenticatorFactory
{
private IdentityService identityService;
/**
* The Safari Identity Service which will perform the authentication.
*
* @param identityService The safari identity service
*/
public void setIdentityService(IdentityService identityService) {
this.identityService = identityService;
}
/**
* Performs an authentication check using the Identity Service.
*
* @param username The username of the user
* @param password THe password the user tries to log in with
* @return true if the username and pass word matched a user in the system
*/
public boolean doAuthenticate(String username, String password) {
return identityService.authenticate(username, password);
}
/**
* Translate the logged in users groups with the WebScript security
* role model and decide if user is allowed to execute the WebScript.
*
* @param username The username of the logged in user
* @param role The authority role that is required for the user to execute the webscript
* @return true if user is authorized to execute the WebScript
*/
public boolean doAuthorize(String username, Description.RequiredAuthentication role)
{
List<String> grantedGroupIds = new ArrayList<String>();
if (role == Description.RequiredAuthentication.user) {
/**
* This method is called after doAuthenticate which means
* the login was successful and the request was done by a user.
*/
grantedGroupIds.add(IdentityService.CUSTOMER);
}
else if (role == Description.RequiredAuthentication.admin) {
// Check if user is member of the admin group.
grantedGroupIds.add(IdentityService.MANAGER);
}
if (grantedGroupIds.size() == 0) {
// No group membership is required for the user.
return true;
}
else {
// Certain group membership is required user.
List<String> userGroups = identityService.getGroups(username);
for (String group : userGroups)
{
for (String grantedGroupId : grantedGroupIds) {
if (group.equals(grantedGroupId)) {
return true;
}
}
}
}
return false;
}
}
<!-- Add authentication and authorization support for webscripts (used by the WebScriptServlet) --> <bean id="webscripts.authenticator.safari" class="com.safari.rest.auth.SafariBasicHttpAuthenticatorFactory"> <property name="identityService" ref="identityService"/> </bean>
<!-- Temporary include the "spring-surf" artifact to get the AbstractBasicHttpAuthenticatorFactory. When updating to RC2 or the final 1.0.0 release it will have moved to "spring-webscripts". --> <dependency> <groupId>org.springframework.extensions.surf</groupId> <artifactId>spring-surf</artifactId> <version>1.0.0-RC1</version> </dependency> <!-- Include the Spring WebScripts runtime --> <dependency> <groupId>org.springframework.extensions.surf</groupId> <artifactId>spring-webscripts</artifactId> <version>1.0.0-RC1</version> </dependency> <!-- Include the Spring WebScript API so we can browse and list our webscripts on the server --> <dependency> <groupId>org.springframework.extensions.surf</groupId> <artifactId>spring-webscripts-api</artifactId> <version>1.0.0-RC1</version> </dependency>
mvn -f safari-root/pom.xml clean install org.codehaus.mojo:tomcat-maven-plugin:1.0-beta-1:redeploy
- Told the WebScriptServlet in web.xml to use our authenticator: “webscripts.authenticator.safari”.
- Defined the authenticator bean in web-application-config.xml.
- Implemented the authenticator by extending AbstractBasicHttpAuthenticatorFactory and respond to doAuthenticate(username, password) and doAuthorize(username, role).
… the rest is up to you and how your app is being implemented, perhaps you want to pass in the username and roles to another environment?
Using HTTP Sessions in Spring WebScripts?
What could be worth pointing out is that the only place where the authentication details are stored, using this approach, is in the request headers and that currently no sessions are used, in other words ideal for scaling. However if you want to use the HttpSession it’s of course possible to access it both in a regular webscript and in your custom made authenticator class. Simply modify you authentication factory with the changes below:
public class SafariBasicHttpAuthenticatorFactoryWithSession extends AbstractBasicHttpAuthenticatorFactory
{
...
// Add a place to store the request and response when the authenticator is created
private WebScriptRequest req;
private WebScriptResponse res;
...
@Override
public Authenticator create(WebScriptServletRequest req, WebScriptServletResponse res)
{
// Override the create method so we get a change to store the request & response
this.req = req;
this.res = res;
return new BasicHttpAuthenticator(req, res);
}
@Override
public boolean doAuthenticate(String username, String password) {
if (identityService.authenticate(username, password)) {
// ...so we can access the HttpSession after a successful login
this.req.getHttpServletRequest().getSession().setAttribute("safari.username", username);
return true;
}
return false;
}
...
}
public class SafariWebScriptWithSessionGet extends SafariWebScript
{
@Override
protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache) {
String bar = (String) ((WebScriptServletRequest)req).getHttpServletRequest().getSession().getAttribute("safari.username");
…
}
}
.
Creating 3 secured WebScripts
The rest of this tutorial will be about creating a couple of secured WebScripts. We will use these WebScripts in future blog posts when we build a Spring Surf web application that will use them to display Safari trips and bookings.
First create a simple Booking pojo in safari-core/src/main/java/com/safari/core/travel/Booking.java that looks like below:
package com.safari.core.travel;
public class Booking {
private int id;
private String username;
private Trip trip;
public Booking () {}
public int getId(){ return id; }
public void setId(int id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public Trip getTrip() { return trip; }
public void setTrip(Trip trip) { this.trip = trip; }
}
package com.safari.core.travel;
import com.safari.core.SafariContext;
import java.util.ArrayList;
import java.util.List;
/**
* In memory service for testing purposes
*/
public class TravelService {
private static int tripIndex = 0;
private static List<Trip> trips = new ArrayList<Trip>();
private static List<Booking> bookings = new ArrayList<Booking>();
public TravelService() {
// Bootstrap data
trips.add(new Trip(++tripIndex, "Masai Mara Adventurer"));
trips.add(new Trip(++tripIndex, "Serengeti Explorer"));
trips.add(new Trip(++tripIndex, "Kruger Wildlife"));
}
/**
* Returns a public list of all available trips
*
* @return a list of trips
*/
public List<Trip> getTrips() {
return trips;
}
/**
* Finds a trip by id
*
* @param tripId The trip id to look for
* @return the trip if found otherwise null
*/
public Trip getTrip(int tripId) {
// Find the trip
for (Trip trip : trips) {
if (trip.getId() == tripId) {
return trip;
}
}
return null;
}
/**
* Administration method to create new trips
*
* @param trip The trip to create
* @return The created trip with a unique id
*/
public Trip createTrip(Trip trip) {
// Give trip a unique id and add it
trip.setId(++tripIndex);
trips.add(trip);
return trip;
}
/**
* Lets the current user create a booking for a certain trip
*
* @param tripId The trip to create a booking for
*/
public Booking createBooking(int tripId) {
// Check that the trip exists
Trip trip = getTrip(tripId);
if (trip == null) {
throw new IllegalArgumentException("Cannot book trip with id '" + tripId + "' since it doesn't exist");
}
// Create booking and return it
Booking booking = new Booking();
booking.setTrip(trip);
booking.setUsername(SafariContext.getCurrentUser());
bookings.add(booking);
return booking;
}
/**
* Returns a list of the current users bookings
*/
public List<Booking> getBookings() {
List<Booking> userBookings = new ArrayList<Booking>();
for (Booking booking : bookings) {
if (booking.getUsername().equals(SafariContext.getCurrentUser())) {
userBookings.add(booking);
}
}
return userBookings;
}
}
<webscript> <shortname>Create Trip</shortname> <description>Creates a trip</description> <url>/travel/trip</url> <format default="json">argument</format> <authentication>admin</authentication> </webscript>
Now let’s continue with the admin WebScript for creating trips and implement the Java controller:
package com.safari.rest.api.travel;
import com.safari.core.travel.Trip;
import com.safari.rest.api.SafariWebScript;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import java.util.HashMap;
import java.util.Map;
public class TripPost extends SafariWebScript
{
protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache) {
try {
JSONObject json = new JSONObject(req.getContent().getContent());
Trip trip = new Trip();
trip.setName(json.getString("name"));
trip = getTravelService().createTrip(trip);
Map<String, Object> model = new HashMap<String, Object>();
model.put("trip", trip);
return model;
}
catch (JSONException e) {
throw new WebScriptException(Status.STATUS_BAD_REQUEST, "The request body contains badly formatted json");
} catch (Exception e) {
throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "Trip could not be created: " + e.getMessage());
}
}
}
<#escape x as jsonUtils.encodeJSONString(x)>
{
"id": ${trip.id},
"name": "${trip.name}"
}
</#escape>
Create safari-rest/src/main/resources/webscripts/com/safari/travel/booking.post.desc.xml and make it look like below and note that the <authentication> element is set to “user”:
<webscript> <shortname>Create Booking</shortname> <description>Creates a booking</description> <url>/travel/booking</url> <format default="json">argument</format> <authentication>user</authentication> </webscript>
package com.safari.rest.api.travel;
import com.safari.core.travel.Booking;
import com.safari.rest.api.SafariWebScript;
import org.json.JSONObject;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import java.util.HashMap;
import java.util.Map;
public class BookingPost extends SafariWebScript
{
@Override
protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache)
{
// Make sure tripId is a number and create the booking
try {
JSONObject json = new JSONObject(req.getContent().getContent());
Booking booking = getTravelService().createBooking(json.getInt("tripId"));
Map<String, Object> model = new HashMap<String, Object>();
model.put("booking", booking);
return model;
}
catch (NumberFormatException nfe) {
throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Parameter tripId is mandatory and must contain an integer value");
} catch (Exception e) {
throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "Trip could not be booked: " + e.getMessage());
}
}
}
<#escape x as jsonUtils.encodeJSONString(x)>
{
"id": ${booking.id},
"username": "${booking.username}",
"trip": {
"id": "${booking.trip.id}",
"name": "${booking.trip.name}"
}
}
</#escape>
<webscript> <shortname>List User's Bookings</shortname> <description>Lists the logged in user's bookings</description> <url>/travel/bookings</url> <format default="json">argument</format> <authentication>user</authentication> </webscript>
package com.safari.rest.api.travel;
import com.safari.rest.api.SafariWebScript;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptRequest;
import java.util.HashMap;
import java.util.Map;
public class BookingsGet extends SafariWebScript
{
@Override
protected Map<String, Object> executeImpl(WebScriptRequest req, Status status, Cache cache)
{
// Create the template model and fill it with trips
Map<String, Object> model = new HashMap<String, Object>();
model.put("bookings", getTravelService().getBookings());
return model;
}
}
<#escape x as jsonUtils.encodeJSONString(x)>
[<#list bookings as booking>
{
"id": ${booking.id},
"username": "${booking.username}",
"trip": {
"id": "${booking.trip.id}",
"name": "${booking.trip.name}"
}
}<#if booking_has_next>,</#if>
</#list>]
</#escape>
<!-- Safari WebScripts / REST API calls (id follows pattern with "webscript." + package + webscript name + method -->
<bean id="webscript.com.safari.travel.trips.get"
class="com.safari.rest.api.travel.TripsGet"
parent="safariWebscript">
</bean>
<bean id="webscript.com.safari.travel.trip.post"
class="com.safari.rest.api.travel.TripPost"
parent="safariWebscript">
</bean>
<bean id="webscript.com.safari.travel.booking.post"
class="com.safari.rest.api.travel.BookingPost"
parent="safariWebscript">
</bean>
<bean id="webscript.com.safari.travel.bookings.get"
class="com.safari.rest.api.travel.BookingsGet"
parent="safariWebscript">
</bean>
Testing you WebScripts
First build the entire project and redploy the rest api using:
mvn -f safari-root/pom.xml clean install org.codehaus.mojo:tomcat-maven-plugin:1.0-beta-1:redeploy
POST http://localhost:8080/safari-rest/service/travel/trip
{
"name": "Spring Safari"
}
{
"id": 4,
"name": "Spring Safari"
}
GET http://localhost:8080/safari-rest/service/travel/trips
POST http://localhost:8080/safari-rest/service/travel/booking
{
"tripId": 4
}
{
"id": 0,
"username": "roy",
"trip": {
"id": 4,
"name": "Spring Safari"
}
}
GET http://localhost:8080/safari-rest/service/travel/bookings
[
{
"id": 0,
"username": "roy",
"trip": {
"id": 4,
"name": "Spring Safari"
}
}
]
Hi Erik
Thanks for posting this article. I’m really interested in using the Web Script framework to host some services in standalone mode (i.e. not part of Alfresco).
Can you please explain how I can use script based controllers (e.g. Rhino JavaScript and Groovy)?
Also, I noticed that Web Scripts when used with Alfresco are available via ‘/service’ (basic auth & ticketed login) and ‘/wcservice’ (Alfresco Explorer auth – e.g. NTLM & Kerberos) endpoints. Does the standalone Web Scripts framework support the same authentication options? i.e. can I create a WebScript and make it available via basic auth, Kerberos, etc?
Many thanks
I guessed at how to use a scripted controller. I created folder ‘WEB-INF/classes/webscripts’ then dropped my Web Script descriptor, script/controller and FTL template there.
I would still be interested in other forms of authentication so if you have any information on Kerberos or LDAP then I’d be much obliged.
Regards
Hi Mark, good to hear you solved it, sorry I hadn’t got back to you earlier.
Regarding your other questions, what if you continue to use BASIC HTTP between your client and the rest services and just modify IdentityService.java above to do an LDAP search instead of going against the test data? Would that be sufficient?
Cheers,
:: Erik
PS. Also note that if you use a javascript controller as you said you had done, you obviously don’t need to define a spring bean in web-application-context.xml