EDUCAÇÃO E TECNOLOGIA

Part 3: Use SAP Graph securely with real data – authentication

Hello!

This is the third part of our developer tutorial on SAP Graph, the API for SAP’s Integrated Intelligent Suite.  Please refer to part 1 of this tutorial for an introduction to SAP Graph, to part 2 for an introduction to the programming interface of SAP Graph, and to the information map for an introduction to the entire tutorial series.

Here, in part 3 of this tutorial, we are going to build the rudimentary basics of a classical enterprise extension web app: a list-details-navigate application.

SAP Graph tenants

In part 2, we accessed data from a preconfigured SAP Graph tenant that is used to access sandbox data via SAP API Business Hub. We even embedded this server URL into our code. Anybody with an API key can access the sandbox data, without any further authentication or authorization.

Of course, this is not a secure way of accessing confidential business data. Therefore, in this part of the tutorial, we will access SAP Graph securely, via the oAuth protocol. OAuth doesn’t share password data but instead uses authorization tokens to prove an identity between clients and services like SAP Graph, and supports Single Sign On. With oAuth, you effectively approve your browser to interact with SAP Graph on your behalf without giving away your password.

SAP Graph is a multitenant service. Customer administrators use the SAP Business Technology Platform (BTP) to subscribe to SAP Graph, by configuring one or more SAP Graph tenants. We will discuss this topic in more detail in a later part of this tutorial, but at this time it is important to understand that this SAP Graph tenant is the key to a specific landscape of customer systems. Most customers will configure multiple landscapes, for instance for development, staging and productive usage.

Each SAP Graph tenant is unique with unique credentials, such as a tenant-specific URL, and various secrets/tokens.

credentials.js

To programmatically access the data from an SAP Graph tenant, an application requires these credentials. How do you get them? A file containing them, credentials.json, is created during the process of setting up and configuring the SAP Graph tenant. You receive this file from the BTP administrator who configured the specific SAP Graph tenant you want to access.

Save the file in the src folder of your project (you can use the existing src folder in which we developed our very first hello Graph server-side application, or create a new source folder in your project, up to you).

Note: If you are interested in executing this tutorial, and don’t have access to your own SAP-managed data sources, contact the SAP Graph team at sap.graph@sap.com. Upon request, and for a limited time, we can provide you with a credentials.json file to access a dataset using the preconfigured SAP Graph tenant.

auth.js

In the same src folder, create a file called auth.js, paste in the following boilerplate (standard) code, and save:

const credentials = require("./credentials.json");
const fetch = require("node-fetch");
const Cookies = require("universal-cookie");
const CALLBACK_URI = "/myCallbackURI";
const CookieName = "SAPGraphHelloQuotesCookie"; class Auth { constructor() { this.clientId = credentials.uaa.clientid; this.clientSecret = credentials.uaa.clientsecret; this.authUrl = credentials.uaa.url; } getToken(req) { const cookies = new Cookies(req.headers.cookie); return cookies.get(CookieName); } async fetchToken(code, redirectUri) { const params = new URLSearchParams(); params.set('client_id', this.clientId); params.set('client_secret', this.clientSecret); params.set('code', code); params.set('redirect_uri', redirectUri); params.set('grant_type', 'authorization_code'); const response = await fetch(`${this.authUrl}/oauth/token`, { method: 'post', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: params }); const json = await response.json(); return json.access_token; } getMiddleware() { return async (req, res, next) => { const redirectUri = `${req.protocol}://${req.get("host")}${CALLBACK_URI}`; if (req.url.startsWith(CALLBACK_URI)) { const code = req.query.code; if (code) { const token = await this.fetchToken(code, redirectUri); res.cookie(CookieName, token, { maxAge: 1000 * 60 * 120, httpOnly: true, path: "/", }); } res.redirect("/"); } else if (!this.getToken(req)) { res.redirect(`${this.authUrl}/oauth/authorize?client_id=${encodeURIComponent(this.clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code`); } else { next(); } }; }
} module.exports = Auth;

As you can see, this boilerplate authentication code refers to several pieces of information which are extracted from the credentials.json file, and are used to log the user of your application in, using single sign on, according to the specifics of the SAP Graph tenant. For the user’s convenience, it also saves the obtained access token as a cookie, so that it can be used until it expires.

graph.js

We will re-use the Graph class that we saw in part 2 of this tutorial, but, now that we are required to authenticate the user before we can use the SAP Graph tenant, we need to make a few small changes. Copy the following text into graph.js and save.

const fetch = require("node-fetch");
const credentials = require("./credentials.json");
const apiUrl = credentials.uri;
const apiVersion = "v1"; class Graph { constructor(auth) { this.auth = auth; this.apiUrl = apiUrl; this.apiVersion = apiVersion; } async get(req, entity, params) { const token = this.auth.getToken(req); const url = `${this.apiUrl}/${this.apiVersion}/${entity}${params ? `?${params}` : ""}`; console.log(url) //for debugging const options = { method: "get", headers: { "Authorization": `Bearer ${token}`, "Accept": "application/json" } }; const response = await fetch(url, options); console.log(`${response.status} (${response.statusText})`) // for debugging const json = await response.json(); return json; }
} module.exports = Graph;

You can see that we made two small changes. The SAP Graph URL is now determined from the credentials of the specific SAP Graph tenant, and the authorization token, obtained during user authentication, is passed to SAP Graph.

helloQuotes.js

Now are we finally ready to build the rudimentary basics of a classical three-page enterprise extension web app: a list-details-navigate application. This is what it will eventually look like:

Don’t expect fancy code, with all the necessary error and exception handling of a robust, production-ready application. Our goal is to show how easy it is to just create small business applications using SAP Graph; we will discuss the finer aspects of robust SAP Graph clients in another part of this tutorial.

We will first establish the skeleton of our application, in a file we will call helloQuotes.js:

// Hello Quote - our first SAP Graph extension app const express = require("express");
const Graph = require("./graph");
const Auth = require("./auth");
const app = express();
const port = 3003; const auth = new Auth(); app.use(auth.getMiddleware());
const graph = new Graph(auth); // ------------------ 1) get and display a list of CustomerQuotes ------------------ // ------------------ 2) show one quote and its items ------------------ // ------------------ 3) navigate to the product details for all the items in the quote ------------------ app.listen(port, () => { console.log(`Example app listening at http://localhost:${port}`)
}); 

This code doesn’t do anything interesting. It basically logs in the user, using the standard authentication that we just discussed, and then listens on port 3003 for web (REST) requests. To make the code work, we need to install request handlers.

Using the express framework, we now create three such handlers, corresponding to three different expected URLs:

  1. The root (/)
  2. Request for a quote: /quote/…
  3. Request for a quote’s product details: /quote… /product

Here is the first request handler:

// ------------------ 1) get and display a list of CustomerQuotes ------------------ app.get('/', async (req, res) => { const quotes = await graph.get(req, "sap.odm.sales/CustomerQuote", "$top=20"); const qlist = quotes.value.map(q => `<p> ${q.effectiveDate} (${q.totalAmount} USD) </p>`).join(""); res.send(` <h1>Hello Quotes</h1> ${qlist} `); });

The handler will be fired if the browser requests the root document (“/”). What does the code do? It fetches the first 20 quotes (sap.odm.sales/CustomerQuote), and then wraps the resulting information (date and total amount) in HTML, and returns it.

Go ahead, paste this handler into the app skeleton, save, and run the server-side app on your terminal console as follows:

node helloQuotes.js

Open a new browser tab, and enter the URL http://localhost:3003. If all went well you will now see a list of dates and corresponding amounts in the browser.

That was nice, but not very interesting. To turn your app into an interesting list-details app, modify the qlist assignment above to introduce a link, as follows:

 const qlist = quotes.value.map(q => `<p> <a href="/quote/${q.id}">${q.effectiveDate} </a> (${q.totalAmount} USD) </p>`).join("");

Now, when the user clicks on one of the quotes, your app will be called again, and this time the URL will match ‘/quote…’. Let us now also introduce our second and third handlers:

// ------------------ 2) show one quote and its items ------------------ app.get('/quote/:id', async (req, res) => { const id = req.params.id; const singleQuote = await graph.get(req, `sap.odm.sales/CustomerQuote/${id}`,"$expand=items&$select=items"); res.send(` <h1>Customer Quote - Detail</h1> <h4> <code> id: ${id} </code> </h4> <a href="/quote/${id}/product"><button>Product Details</button></a> <pre><code>${JSON.stringify(singleQuote,null,2)}</code></pre> `); }); // ------------------ 3) navigate to the product details for all the items in the quote ------------------ app.get('/quote/:id/product', async (req, res) => { const id = req.params.id; const singleQuote = await graph.get(req, `sap.odm.sales/CustomerQuote/${id}`,"$expand=items($expand=product($select=displayId))&$select=items"); const productIds = singleQuote.items.map(i => i.product.displayId); const filterClause = productIds.map(p => `displayId eq '${p}'`).join(" or "); const products = await graph.get(req, `sap.odm.product/Product`,`$filter=${filterClause}`); res.send(` <h1>Products for Customer Quote</h1> <h4><code>id: ${id}</code></h4> <pre><code>${JSON.stringify(products,null,2)}</code></pre> `); });

The OData query at the heart of the second handler uses the $expand query parameter to fetch the details of the quote, including what was quoted (items). The product id in the quote is then used in the third handler to navigate across the business graph, to fetch the detailed product information from the product catalog. In both cases, the data is just dumped to the screen as JSON, but evidently, a real app would format the information much more nicely.

Go ahead, make the change in the first handler, and then paste the second and third handlers in your code, save the file, restart the service, and refresh the localhost:3003 page in the browser. Voila! Your app is live.

Note again, how you, as a developer, never had to ask yourself where the data came from. You just navigated from a quote object to a product object, without any effort. The landscape that you accessed via the configured SAP Graph tenant may have managed quotes in SAP Sales Cloud, or in SAP S/4HANA, and the product catalog may have been, theoretically, in yet another system. You simply don’t have to care.


Chaim Bendelac, Chief Product Manager – SAP Graph

Visit the SAP Graph website at http://explore.graph.sap/

Contact us at sap.graph@sap.com