RESTful Web Service Endpoint Scripting
See Also: Publishing
RESTful Web Services Overview, RESTful Services,
Server Admin Web Service
Administration, Publishing
Web Service Documentation
A RESTful Web Service contains a collection of endpoints. Endpoint script(s) are added to each endpoint and are executed when a request for the endpoint is processed. Note that all RESTful web service scripts should be created in JavaScript, as the FPL language does not have any way to address the web service request or response data.
Any script
statements that operate on web pages or controls are not supported in an
Endpoint Script – as a RESTful Web Service has no pages.
An Endpoint Script
can be created in the tree: New >
Script.
All of the RESTful Web Service specific functions are can be accessed using the JavaScript variable form.rest.
The table below shows the methods that can be used to access request data:
Function |
Returns |
Description |
form.rest.getMethod() |
String |
Returns the HTTP
method used to call the endpoint. e.g GET, POST |
form.rest.getEndpointPath() |
String |
Returns the endpoint
path invoked that is received by the system e.g /customers/1 |
form.rest.getPathParameter(name) |
String |
Returns the
incoming path parameter
value by name or returns null if the variable does not exist. |
form.rest.getPathParameters() |
Map<String,
String> |
Returns a Map of
key, value String pairs for all the incoming path parameters. |
form.rest.getRequestBody() |
String |
Returns the request
body or null if no body is set on the request. |
form.rest.getRequestContentType() |
String |
Returns the
Content-Type HTTP header from the incoming request. e.g application/json.
Returns null if the Content-Type header is not set. |
form.rest.getRequestHeader(name) |
String |
Returns the HTTP
header value by name or returns null if the header does not exist. |
form.rest.getRequestHeaders() |
Map<String,
String> |
Returns a Map of key,
value String pairs for all the incoming HTTP headers. |
form.rest.getRequestParameter(name) |
String |
Returns the
incoming query or post parameter value by name or returns null if the
variable does not exist. |
form.rest.getRequestParameters() |
Map<String,
String> |
Returns a Map of
key, value String pairs for all the incoming query/post parameters. |
The table below shows the methods that can be used to set information on the response:
Function |
Parameter |
Description |
form.rest.setResponseBody(body) |
body - String |
Sets the response
body. |
form.rest.setResponseContentType(type) |
type - String |
Sets the
Content-Type HTTP header for the response. E.g application/json |
form.rest.setResponseHeader(name, value) |
name – String value - String |
Adds a response
HTTP header value by name. |
form.rest.setResponseStatus(status) |
status - Number |
Set the response
status code. The default is 200 - HTTP OK. |
The default HTTP status code for the response is 200 (Status OK). Error handling should be handled within the script and the appropriate HTTP status should be set on the response before exiting the script. Unhandled errors are returned as status code 500 (server error) and the detail error message is returned in the response body.
An example script with error handling might be as follows:
importPackage(com.ebasetech.xi.api);
importPackage(com.ebasetech.xi.services);
//get customer id
from the request URI
fields.customer_id.value
= form.rest.getPathParameter("customerId");
//fetch customer
resources.customers.fetch();
if(system.variables.$FETCH_COUNT
> 0)
{
var customer = {};
customer.id = fields.customer_id.value;
customer.firstName = fields.first_name.value;
customer.lastName =
fields.last_name.value;
customer.dob = fields.dob.value;
customer.sex = fields.sex.value;;
customer.hasChildren =
fields.children.value;
form.rest.setResponseContentType("application/json");
form.rest.setResponseBody(JSON.stringify(customer));
}
else
{
//return 404 if customer not found
form.rest.setResponseStatus(404);
form.rest.setResponseBody("Customer
not found with customer id: " + fields.customer_id.value);
}
See Internal Errors for more information regarding generic error response status codes.
If an error is uncaught when executing an endpoint event, then the ‘On Error’ event is invoked, if configured.
An on error
event may be as follows:
importPackage(com.ebasetech.xi.api);
importPackage(com.ebasetech.xi.services);
//get customer id from the request URI
fields.customer_id.value =
form.rest.getPathParameter("customerId");
//fetch customer
resources.customers.fetch();
if(system.variables.$FETCH_COUNT > 0)
{
var
customer = {};
customer.id
= fields.customer_id.value;
customer.firstName
= fields.first_name.value;
customer.lastName
= fields.last_name.value;
customer.dob
= fields.dob.value;
customer.sex
= fields.sex.value;;
customer.hasChildren
= fields.children.value;
form.rest.setResponseContentType("application/json");
form.rest.setResponseBody(JSON.stringify(customer));
}
else
{
//return
404 if customer not found
form.rest.setResponseStatus(404);
form.rest.setResponseBody("Customer
not found with customer id: " + fields.customer_id.value);
}
A RESTful
Web Service does not support authentication as default. Security is typically
required on each endpoint implementation requirements. This normally depends on
whether the data is sensitive or not. Usually public information does not
require any security where as sensitive information, for example, customer
details should have some form of authentication implementation.
Other
considerations might be that the system supports user roles and permissions.
These also have specific requirements based on the endpoint implementation.
When
implementing security the server should return the correct HTTP status:
HTTP Status |
HTTP Status
Description |
Description |
200 |
OK |
User is validated and the request has been successfully executed. |
401 |
UNAUTHORIZED |
The request requires authentication. The response MUST include the response header – WWW-Authenticate containing a challenge applicable to the requested resource, for example - WWW-Authenticate: Basic realm=”myrealm” If the request contains the required Authorization credentials then a 401 indicates that the authorization has failed for those credentials, for example, incorrect username or password. |
403 |
FORBIDDEN |
The server has understood the request but is refusing to fulfill it, for example, a user does not have sufficient permissions to access a resource. This is a permanent failure and does not contain a challenge. |
The JavaScript
API contains a method to help with Base64 encoding that is used in HTTP
Basic authentication:
Method Name |
Description |
form.rest.readBasicAuthenticationCredentials() |
Extracts the username and password from the request Authorization HTTP header and returns a UserCredential object that contains the username and password in clear text. Example: var credentials =
form.rest.readBasicAuthenticationCredentials(); if(credentials) { //authenticate user } |
When implementing
any custom authentication, it is not recommended to add username and
passwords to the URI parameters as they are always visible on the request.
Username and passwords should be added to the request by using POST parameters
or as HTTP headers.
It does not
matter which authentication method is used, always use HTTPS to secure the client and server communication.
Basic
authentication is the simplest type of authentication. The username and
password are concatenated together and then base64 encoded and added to the
request header:
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l
The
following example reads the Basic
Authorization request header and tries to authenticate the user against a
database. If the request does not contain the required Basic authentication
credentials then return a 401 (UNAUTHORIZED). If the authorization fails then
return a HTTP response of 403 (FORBIDDEN). If the credentials are authorized,
process the request. The Basic authentication is authorized in the endpoint
script before returning the response:
//read authentication
var cred =
form.rest.readBasicAuthenticationCredentials();
fields.customer_id.value =
form.rest.getPathParameter("customerId");
fields.firstName.value =
form.rest.getRequestParameter("firstName");
fields.lastName.value =
form.rest.getRequestParameter("lastName");
var authenticated = false;
if(cred)
{
//authenticate
user
if(validateUser(cred.getUsername(),
cred.getPassword()))
{
//update
the customer
resources.customers.update();
}
else
{
//just
reject the user
form.rest.setResponseStatus(403);
}
}
else
{
log("No
credentials");
form.rest.setResponseHeader("WWW-Authenticate",
"Basic realm=\"User Realm\"");
form.rest.setResponseStatus(401);
}
function validateUser(username, password)
{
var auth = false;
var stmt = "select password from users
where name = '" + username + "'";
services.database.executeSelectStatement("SAMPLES",
stmt,
function
(columnData)
{
auth =
password == columnData.PASSWORD;
return
true; // continue
});
return auth;
}
Custom
Authentication allows you to implement your own authentication. This could be
username and password based authentication or token based authentication. Any
requests containing security are recommended to be transferred SSL (https). If
the request is not transferred using SSL it is recommended that the security
credentials are encrypted.
The
following example shows a token based authentication. Token based
authentication is a two phased authentication procedure. See below for the
authentication flow:
Create an
authentication endpoint:
importPackage(com.ebasetech.xi.api);
importPackage(com.ebasetech.xi.services);
var username = form.rest.getRequestParameter("username");
var password =
form.rest.getRequestParameter("password");
// Authenticate the
user using the credentials provided
if(authenticate(username,
password))
{
// Issue a token for the user
var tokenObj = issueToken(username);
//persist token
persistToken(tokenObj);
// Return the token on the response
form.rest.setResponseBody(JSON.stringify(tokenObj));
}
else
{
form.rest.setResponseStatus(403);
}
/**
* Authenticate against a database, LDAP, file or
whatever
* @return false if the credentials fail
otherwise true
*/
function
authenticate(username, password) {
if(username == "demouser"
&& password == "demopwd")
return
true;
else
return
false;
}
/**
* Issue
a token (can be a random String persisted to a database or a JWT token)
* The
issued token must be associated to a user
*
@return the issued token
*/
function
issueToken(username) {
var d = new Date();
d.setDate(d.getDate() + 1); //add a day
var random = new
java.security.SecureRandom();
var token = new java.math.BigInteger(130,
random).toString(32);
var tokenObj = {};
tokenObj.token = token;
tokenObj.expires = d.getTime();
return tokenObj;
}
/**
* Add the issued token to the database
*/
function
persistToken(tokenObj)
{
fields.token.value = tokenObj.token;
fields.expires.value = tokenObj.expires;
fields.username.value = username;
resources.login.update();
}
importPackage(com.ebasetech.xi.api);
importPackage(com.ebasetech.xi.services);
try
{
var accessToken =
form.rest.getRequestHeader("Authentication");
//validate token
if(validateToken(accessToken))
{
//get customer id from the
request URI
fields.customer_id.value =
form.rest.getPathParameter("customerId");
//fetch customer
resources.customers.fetch();
if(system.variables.$FETCH_COUNT
> 0)
{
var customer = {};
customer.id =
fields.customer_id.value;
customer.firstName =
fields.first_name.value;
customer.lastName
= fields.last_name.value;
customer.dob =
fields.dob.value;
customer.sex =
fields.sex.value;
customer.hasChildren =
fields.children.value;
form.rest.setResponseContentType("application/json");
form.rest.setResponseBody(JSON.stringify(customer));
}
else
{
//return 404 if customer
not found
form.rest.setResponseStatus(404);
form.rest.setResponseBody("Customer
not found with customer id: " + fields.customer_id.value);
}
}
else
{
//return not authorized
log("Authorization
failure");
form.rest.setResponseHeader("WWW-Authenticate",
"Bearer realm=\"User Realm\"");
form.rest.setResponseStatus(401);
}
}
catch(e)
{
//return generic error
form.rest.setResponseStatus(500);
form.rest.setResponseBody("Error finding
customer details for customer: " + e);
}
function
validateToken(accessToken)
{
if(accessToken ||
accessToken.indexOf("Bearer ") != 0)
return false;
var token =
accessToken.substring("Bearer".length).trim();
var stmt = "select expiry from
auth_tokens where access_token = '" + token + "'";
services.database.executeSelectStatement("SAMPLES",
stmt,
function (columnData)
{
var
verifyDate = new Date();
auth
= verifyDate.getTime() <= columnData.expiry;
return
true; // continue
});
return auth;
}