Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
MartinRaepple
Active Participant


At the end of part 1 of this blog, the decision was taken to use the Open Authorization Framework (OAuth) for protecting the API exposed by the application on HCP. By obtaining an access token from the OAuth 2.0 Authorization Server (AS) on HCP, my sensors (thermostats) connected to the openHAB instance running on my single board computer get authorized to consume the API and store their measured temperature values in the Cloud. So let's get started!

Authorizing the thermostats with OAuth


In general, obtaining an OAuth access token from HCP's AS follows the OAuth 2.0 specification's authorization code grant flow:

  1. The account administrator in the HCP account must register an OAuth client (the API consumer, in our scenario the openHAB instance acting on behalf of the sensors connected to it) in the HCP Cloud Cockpit. This requires to specify a "Redirect URI" for the client which is used in step 4 of this process.

  2. The OAuth client directs the user's agent (i.e. the web browser) to the AS authorization endpoint, which requires the user to authenticate with their HCP account's trusted identity provider (SAP ID Service, the customer's SAP Cloud Identity tenant, or their own identity provider).

  3. Upon successful login with the identity provider (IdP), the user is asked by the AS to confirm that the OAuth client requesting a token will be allowed to act on his or her behalf, optionally under a specific scope (e.g. "send sensor data" in this IoT scenario)

  4. If the user confirmed the previous step, the AS uses the URI registered in step 1 to redirect the user's web browser back to the OAuth client. The HTTP redirect URL includes a one-time authorization code generated by the AS.

  5. The OAuth client request an OAuth access token from the AS token endpoint with the authorization code obtained in the previous step.

  6. The AS validates the authorization code, issues a new access token, and sends it back to OAuth client. The AS associates the token with the user who confirmed the OAuth client's authorization request in step 3.

  7. By sending the access token with the API call in the HTTP Authorization header according to the OAuth specification, the OAuth client can make an authorized call to the API protected with OAuth on HCP.


OAuth 2.0 improves the overall security of the scenario by avoiding to pass the user's confidential username and password to the OAuth client. Using OAuth, the user only shares their credentials with the identity provider (IdP). Although the OAuth client still has to take care for secure storage of the issued access token in step 6, the potential damage of a stolen token compared to a stolen password is considerably lower. The access token only authorizes an OAuth client to call a single API endpoint on behalf of the user. A username and password has a much broader scope - it may allow access to a large number of web sites and services.

Using the OAuth 2.0 Authorization Code Grant Flow in the IoT scenario


Looking at steps 2, 3 and 4 above shows that the authorization code grant flow relies on the user's web browser to obtain the authorization code from the AS and redirect back to the OAuth Client. Starting off this process from the central control unit won't work - it is a headless device with no web browser installed on it. Therefore we make use of special feature in HCP's OAuth AS, which facilitates the authorization of an OAuth Client on a mobile device using a QR code.

 

Here is how we will change the above process to authorize the sensors in our IoT scenario:

 

Step 1: OAuth Client Registration


 

We'll register OAuth clients in the Cloud Cockpit for each sensor because we want to authorize them individually. So in our case, we'll have three OAuth clients: "sensor1" for the thermostat in the living room, "sensor2" for the kitchen's thermostat, and "sensor3" for the bath's thermostat. All of them are configured with the same redirect URI, which will only play a minor role in the following steps. Please note that the OAuth clients must be assigned to a subscription in the account, which is the UI5 application used later to display the temperature values. So this step assumes that the application is already deployed in the account.



 

Optionally one can register application-specific OAuth scopes under the application's submenu folder in the Cockpit:

 



 

Step 2: Obtaining the Authorization Code with a QR Code from the mobile device


 

Instead of starting the Authorization Code Grant Flow from the OAuth Client launching a web browser which wouldn't be possible from my openHAB instance running on the headless single board computer, we stay in the Cloud Cockpit and open another browser window to access HCP's OAuth AS End User UI.

 

This requires to login to the OAuth AS, which is done in my case using an SCI tenant:




With a click on "Code" in the OAuth AS UI you can generate a new authorization code for a selected OAuth client. To simplify the transfer of the authorization code to the OAuth Client which needs it to request the access token, a QR code for the new authorization code will be generated as well.



Having a QR code scanner installed on my mobile device from where I also want to remotely control my thermostats, I can easily scan the generated code and store it in my smart phone's clipboard.

Now comes the tricky part: How should the scanned code be passed to openHAB so that it can request the access token from HCP's OAuth AS in order to persist the thermostat's data in the Cloud? All I would need is an input field on the thermostat's element in openHAB's mobile app UI which allows me to paste the authorization code. openHAB would have to take it from there and use it to send the access token request and store it at a secure place on the local (my Cubietruck's) file system. Unfortunately, input fields are not in openHAB's supported list of standard UI elements for a sitemap. Fortunately, there is an WebView element which allows you to refer to a URL of your choice and display the content in a frame within the mobile UI. If this URL will point to a simple Java servlet running on openHAB generating a label and input field to paste the code with a button to submit it, we are almost done!

Step 3: Extending openHAB to capture the authorization code and request the access token from HCP


So this leaves us with extending openHAB with a new binding implementing the servlet to capture the scanned authorization code from the clipboard and requestthe access token with it. A good starting point for developing a new openHAB binding are these instructions on the OpenHAB wiki. With the OSGi bundle skeleton generated by Maven for my new "AuthzCode" binding, I first add a Servlet class to it:
package org.openhab.binding.authzcode;
...
public class WebViewServlet extends HttpServlet {
private static final String SERVLET_NAME = "/webview";
....
/**
* Activates the webview servlet.
*/
protected void activate() {
try {
logger.debug("Starting up authzcode webview servlet at " + SERVLET_NAME);
Hashtable<String, String> props = new Hashtable<String, String>();
httpService.registerServlet(SERVLET_NAME, this, props, createHttpContext());
bundleContext = FrameworkUtil.getBundle(this.getClass()).getBundleContext();
if (bundleContext != null) {
ServiceReference<?> serviceReference = bundleContext.getServiceReference(AuthzCodeBindingProvider.class.getName());
if (serviceReference != null) {
bindingprovider = (AuthzCodeBindingProvider) bundleContext.getService(serviceReference);
} else
logger.error("BindungProvider is null");
// get all items for this binding
for (String itemName : bindingprovider.getItemNames()) {
Item sapHcpItem = itemUIRegistry.getItem(itemName);
logger.debug("Found item: " + sapHcpItem.getName());
}
} else
logger.error("bundleContext is null");
} catch (Exception ex) {
logger.error("Error during saphcp-webview servlet startup", ex);
}
}
/**
* Deactivates the webview servlet.
*/
protected void deactivate() {
httpService.unregister(SERVLET_NAME);
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
logger.debug("Received WebView Request");
String action = request.getParameter("action");
String request_clientid = request.getParameter("clientid");
if (StringUtils.isNotBlank(request_clientid)) {
this.clientid = request_clientid;
}
logger.debug("clientid set to: " + clientid);
if (StringUtils.isNotBlank(action) && action.equalsIgnoreCase("sendcode")) {
logger.debug("Executing action: " + action);
String code = request.getParameter("code");
// create command (string)
String commandString = "authorize " + clientid + " " + code;
StringType command = new StringType(commandString);
if (bindingprovider.getItemNames() != null) {
// send command synchronously
String firstItemName = bindingprovider.getItemNames().iterator().next();
eventPublisher.sendCommand(firstItemName, command);
} else {
logger.warn("No item with authzcode binding found");
}
}
response.getWriter().println(buildFormString());
}
private String buildFormString() {
String result = "<html>";
result += "<head><style>p {font-family:verdana;}</style></head>";
result += "<body><form action='./webview' method='get'>";
result += "<p>Code <input type='text' name='code'>";
result += "<input type='hidden' name='action' value='sendcode'>";
result += "<input type='submit' value='Submit'></p></form>";
// update state
// check if file with token exists
String accessToken;
accessToken = AuthzCodeCommons.loadToken(this.clientid);
if (StringUtils.isBlank(accessToken)) {
result += buildAccessTokenFailed();
} else {
result += buildAuthorizedString();
}
result += "</body></html>";
return result;
}
private String buildAuthorizedString() {
return "<p><span style=\"color:green\">Sensor is authorized!</span></p>";
}
private String buildAccessTokenFailed() {
return "<p><span style=\"color:red\">Sensor is not authorized!</span></p>";
}
}




















If no request parameter is sent, the doGet method generates a simple HTML form which shows an input field to paste the authorization code in. The form also informs the user if there is already an access token stored for the current item (sensor) or not. By pressing the Submit button, a request is sent to the servlet with two request parameters: The action to send the received code to HCP, and the id of the OAuth Client (thermostat) for which the access token should be requestd. In this case, the WebViewServlet sends a command "authorize" via its EventPublisher instance to the openHAB bus and passes the client id and authorization code with it (lines 60-67).


 

To launch the servlet when the bundle get loaded and activated, a new component description (webviewservlet.xml) for the servlet is added in the OSGI-INF folder of the bundle:
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="activate" deactivate="deactivate" name="org.openhab.authzcode">
<implementation class="org.openhab.binding.authzcode.WebViewServlet"/>
<reference bind="setHttpService" cardinality="1..1" interface="org.osgi.service.http.HttpService" name="HttpService" policy="dynamic" unbind="unsetHttpService"/>
<reference bind="setItemUIRegistry" cardinality="1..1" interface="org.openhab.ui.items.ItemUIRegistry" name="ItemUIRegistry" policy="dynamic" unbind="unsetItemUIRegistry"/>
<reference bind="setEventPublisher" cardinality="1..1"
interface="org.openhab.core.events.EventPublisher" name="EventPublisher"
policy="dynamic" unbind="unsetEventPublisher" />
</scr:component>





















This component description must be referenced in the bundle's MANIFEST.MF file via the Service-Component property:
Service-Component: OSGI-INF/binding.xml, OSGI-INF/genericbindingprovider.xml, OSGI-INF/webviewservlet.xml





















Now it is up to the AuthzCode binding's main component, the Maven-generated AuthzCodeBinding class, to execute the "authorize" command and request the access token with the passed code and client (sensor) id from HCP. This is done by implementing the internalReceiveCommand method, which uses an Apache Commons HTTPClient to create a POST request to the HCP OAuth AS token endpoint according to the OAuth 2.0 specification. In case of a successful access token request, the response is a JSON formattet string containing the access token which is finally stored on the openHAB instance.
public class AuthzCodeBinding extends AbstractBinding<AuthzCodeBindingProvider> implements ManagedService {
...
@Override
protected void internalReceiveCommand(String itemName, Command command) {
// the code being executed when a command was sent on the openHAB
// event bus goes here. This method is only called if one of the
// BindingProviders provide a binding for the given 'itemName'.
// process command
if (command instanceof StringType) {
StringType t = (StringType) command;
logger.debug("internalReceiveCommand() is called with command \"" + t + "\"");
String[] commandParts = StringUtils.split(t.toString());
if (commandParts[0].equals("authorize")) {
String clientId = commandParts[1];
String authzCode = commandParts[2];
this.authorize(clientId, authzCode);
}
}
}

private void authorize(String clientId, String authzCode) {
logger.debug("authorize() method is called with value " + authzCode);
HttpClient httpClient = new HttpClient();
if (this.proxyHost != null) {
httpClient.getHostConfiguration().setProxy(this.proxyHost, new Integer(this.proxyPort).intValue());
}
PostMethod httpPost = new PostMethod(this.tokenendpoint);
httpPost.addRequestHeader("content-type", "application/x-www-form-urlencoded");
httpPost.addParameter("client_id", clientId);
httpPost.addParameter("grant_type", "authorization_code");
httpPost.addParameter("code", authzCode);
httpPost.addParameter("redirect_uri", this.redirectUri);

BufferedReader br = null;
try{
int returnCode = httpClient.executeMethod(httpPost);
if(returnCode != HttpStatus.SC_OK) {
// still consume the response body
String errorResponseBody = httpPost.getResponseBodyAsString();
logger.error("Failed to request access Token" + errorResponseBody);
} else {
// Read the response body.
byte[] responseBody = httpPost.getResponseBody();
// parse response for token
Object accessTokenResponse = JSONValue.parse(new String(responseBody));
JSONObject array=(JSONObject)accessTokenResponse;
// store access token
AuthzCodeCommons.saveToken(clientId, (String)array.get("access_token"));
}
} catch (Exception e) {
logger.error("Failed to store access token for client " + clientId + ": " + e.getMessage());
} finally {
httpPost.releaseConnection();
if(br != null) try { br.close(); } catch (Exception fe) {}
}
}
...


















Static configuration parameters of the AuthzCode binding such as the HCP OAuth AS token URL or optional HTTP proxy settings if openHAB is operated behind a firewall can be set in openHAB's central configuration (openhab.cfg), which is read when the system is started and the bindings are initialized.

Adding the new items for authorizing the three thermostats starts with defining them in openHAB's items configuration file:
...
String Authz_Thermostat_Kitchen {authzcode}
String Authz_Thermostat_Livingroom {authzcode}
String Authz_Thermostat_Bath {authzcode}








To expose them as a WebView in openHAB's mobile app, the new items are referenced in the sitemap configuration file from part 1 of this blog as follows:
sitemap homezone label="Homezone"
{
Frame {
Text item=Thermostat_Livingroom_Actual {
Frame {
Setpoint item=Thermostat_Livingroom_Target step=0.5 maxValue=28
Webview item=Authz_Thermostat_Livingroom url="http://<openHAB host>:<port>/webview?clientid=sensor1" height=2
}
}
}
...







From the url parameter of the WebView element which points to the binding's servlet, the sensor's OAuth client ID is passed as a request parameter (clientid) to the binding.

Step 4: Using the access token to persist sensor data on HCP


With the access token in place to authorize a sensor to persist its data on HCP, we need a component on openHAB to actually call our API whenever there is new temperature value captured by a thermostat and sent on the event bus. Like with our new binding, there is no such component in openHAB's Add-On package which can do this out-of-the box. Since this sounds like a reusable component which may be useful for any kind of sensor data (not just my thermostat's temperature values) to persist on HCP, I created a new openHAB action (bundle) for it, which I called "sapHCP".

A skeleton bundle for a new openHAB action is built almost the same way as the binding before using a Maven archetype. The action's static method sendRequest will be called by openHAB  via a rule which is triggered by an sensor value update event on the bus. More on this in a minute! One can also pass parameters when invoking an action's static method. In case of the sapHCP action, those are the actual sensor (temperature) value to store on HCP, the unit of the value (e.g. Celsius), the sensor type (e.g. Thermostat), an optional description and the sensor's OAuth Client ID.
package org.openhab.action.saphcp.internal;
...
public class SapHCP {
...
public static boolean sendRequest(String sensorValue, String unit, String type, String description, String clientId) {
...
// check for file saphcp.token in etc directory
String accessToken = AuthzCodeCommons.loadToken(clientId);
if (StringUtils.isNotEmpty(accessToken)) {
// found the token
logger.debug("Found token for client with id " + clientId);
// post new temperature value to all HCP items
HttpClient httpClient = new HttpClient();
if (SapHCP.defaultProxyHost != null && SapHCP.defaultProxyPort != null) {
httpClient.getHostConfiguration().setProxy(SapHCP.defaultProxyHost, new Integer(SapHCP.defaultProxyPort).intValue());
}

PostMethod httpPost = new PostMethod(SapHCP.defaultAPIUrl);
// add access token as authorization header
httpPost.addRequestHeader("Authorization", "Bearer " + accessToken);
JSONObject newSensorValue=new JSONObject();
newSensorValue.put("value", sensorValue);
newSensorValue.put("sensorId", clientId);
newSensorValue.put("unit", unit);
newSensorValue.put("type", type);
newSensorValue.put("description", description);
try {
StringRequestEntity requestBody = new StringRequestEntity(newSensorValue.toJSONString(), "application/json", "UTF-8");
httpPost.setRequestEntity(requestBody);
BufferedReader br = null;
try
{
int returnCode = httpClient.executeMethod(httpPost);
// read response
if(returnCode != HttpStatus.SC_OK) {
// still consume the response body
String errorResponseBody = httpPost.getResponseBodyAsString();
logger.error("Failed to send new temperature value: " + errorResponseBody);

} else {
// Read the response body.
byte[] responseBody = httpPost.getResponseBody();
// parse response
logger.debug("New temperature value stored. ID: " + new String(responseBody));
}
} catch (Exception e) {
logger.error("Unknown Host Exception: " + e.getMessage());
} finally {
httpPost.releaseConnection();
if(br != null) try { br.close(); } catch (Exception fe) {}
}
} catch (UnsupportedEncodingException uee) {
logger.error("JSON encoding error: " + uee.getMessage());
}
} else {
logger.warn("No Access token found");
}
return true;
}
}















Using the OAuth client ID parameter as an search index, the sapHCP action can retrieve the sensor's OAuth access token which has been requested in the previous step with the AuthzCode binding and stored in a local token store. If an access token was found for the sensor (client), it is passed with the HTTP authorization header (line 24) in the API call to HCP. The API on HCP expects a flat JSON structure with the values passed as parameters to the sapHCP action.

Step 5: Triggering the sapHCP action from an openHAB rule


Besides the already mentioned items and sitemap configuration files in part 1 of this blog, actions triggered by events on openHAB's software bus can be defined in a rules file to automate processes. The rules for my home automation scenario are quite simple: Whenever a thermostat sends an update on the actual temperature (item), an sapHCP action should be triggered to send the new sensor value to HCP.
import org.openhab.core.library.types.*
import org.openhab.model.script.actions.*
import org.openhab.action.saphcp.*
rule "Send Living Room updates to HCP"
when
Item Thermostat_Livingroom_Actual received update
then
var value = Thermostat_Livingroom_Actual.state.toString
logInfo("sapHCP","Sensor 1: " + value)
sendRequest(value, "C", "Thermostat", "Living Room", "sensor1")
end
...













The above excerpt from the rules file show the descriptive rule definition language in openHAB: In case the item representing the actual temperature in the living room receives a temperature update, its current value is stored in a local variable and send to HCP using the sapHCP action's sendRequest method.

Step 6: Protecting the API on HCP with OAuth


The last remaining piece of the puzzle is the UI5 application running on HCP which exposes the API consumed by the openHAB sapHCP action. Following the declarative approach for protecting the API using OAuth results in the following filter configuration in the application's web.xml deployment descriptor:
...
<filter>
<display-name>Sensor OAuth Protection</display-name>
<filter-name>PersistDataScopeFilter</filter-name>
<filter-class>com.sap.cloud.security.oauth2.OAuthAuthorizationFilter</filter-class>
<init-param>
<param-name>scope</param-name>
<param-value>persist-data</param-value>
</init-param>
<init-param>
<param-name>http-method</param-name>
<param-value>post</param-value>
</init-param>
<init-param>
<param-name>no-session</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>PersistDataScopeFilter</filter-name>
<url-pattern>/api/v1</url-pattern>
</filter-mapping>
...













The API itself is implemented using the the JAX-RS framework Jersey to annotate my MeasurementResource class. Jersey's application servlet is mapped to the path "/api/v1".
@Path("measurement")
@Produces({ MediaType.APPLICATION_JSON })
public class MeasurementResource {
private static Logger logger = LoggerFactory.getLogger(MeasurementResource.class);
@GET
public Response getAllMeasurements()
{
logger.debug("getTemperatures() called");
MeasurementDAO measurementDAO = new MeasurementDAO();
List<Measurement> measurements = measurementDAO.getAllMeasurements();
return Response.ok().entity(measurements).build();
}
@GET
@Path("sensors")
public Response getSensors()
{
logger.debug("getSensors() called");
Collection<Measurement> resultList = new ArrayList<Measurement>();
MeasurementDAO measurementDAO = new MeasurementDAO();
List<String> sensorIDs = measurementDAO.getSensorIDs();
for (String sensorID : sensorIDs) {
Measurement lastMeasurementForSensor = measurementDAO.getLastMeasurementForSensor(sensorID);
resultList.add(lastMeasurementForSensor);
}
return Response.ok().entity(resultList).build();
}
@GET
@Path("sensor/{sensorId}")
public Response getMeasurementsForSensor(@PathParam("sensorId") String sensorId) {
MeasurementDAO measurementDAO = new MeasurementDAO();
List<Measurement> measurementsForSensor = measurementDAO.getSensorMeasurements(sensorId);
return Response.ok().entity(measurementsForSensor).build();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addMeasurement(Measurement newMeasurement)
{
logger.debug("addTemperature() called");
MeasurementDAO measurementDAO = new MeasurementDAO();
long measurementId = measurementDAO.addMeasurement(newMeasurement);
return Response.ok().entity(measurementId).build();
}
}












All methods make use of a central Data Access Object (DAO) to read and store the sensor measurements from the HANA DB on HCP. Internally, the DAO consumes the HCP Persistence Service and uses JPA to manage all sensor data in one single table. Only the addMeasurement operation of the API annotated with the @POST resource method is consumed from the sapHCP action in openHAB. The other read-only operations such as getMeasurementsForSensor are actually consumed from the SAP UI5 JSON models in the application's user interface. In order to protect them as well from unauthorized access, the following security constraint to require FORM-based authentication with the account's (SAML) Identity Provider for the API's path "/api/v1/*" is added to the application's web.xml:
  <login-config>
<auth-method>FORM</auth-method>
</login-config>
<security-constraint>
<web-resource-collection>
<web-resource-name>Protected Web UI Area</web-resource-name>
<url-pattern>/dashboard.html</url-pattern>
<url-pattern>/api/v1/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>Everyone</role-name>
</auth-constraint>
</security-constraint>











To also allow in parallel the consumption of the API's POST-method from openHAB which cannot login with SAML but provides an OAuth access token, the login stack for FORM must be configured in the Cloud Cockpit to include the OAuth 2.0 Login Module:



That's it! The following video shows the end-to-end flow with all components developed and used in this blog series:

  • AuthzCode binding and sapHCP action on openHAB

  • the openHAB mobile application on my smart phone

  • the SAP UI5 application and the OAuth-protected REST API deployed in my HCP trial account

  • my SCI tenant for user authentication at the UI5 application and the HCP OAuth 2.0 AS




9 Comments