Skip to Content
Technical Articles
Author's profile photo Chaim Bendelac

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 SalesQuotes   ------------------ 

// ------------------    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 SalesQuotes   ------------------ 

  app.get('/', async (req, res) => {
  const quotes = await graph.get(req, "sap.graph/SalesQuote", "$top=20");
  const qlist = quotes.value.map(q => `(${q.netAmount} ${q.netAmountCurrency})`).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.graph/SalesQuote), 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.pricingDate} </a>
 (${q.netAmount} ${q.netAmountCurrency}) </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.graph/SalesQuote/${id}`, "$expand=items&$select=items");
    const allItemLinks = singleQuote.items.map(item => `<p><a href="/quote/${id}/item/${item.itemId}"><button>Product details for item ${item.itemId}: ${item.product}</button></a></p>`).join("");
    res.send(`
      <h1>SalesQuote - Detail</h1>
      <h4><code>id: ${id}</code></h4>
      ${allItemLinks}
    `);
});

// ------------------ 3) navigate to the product details for an item in the quote ------------------

app.get('/quote/:id/item/:itemId', async (req, res) => {
    const id = req.params.id;
    const itemId = req.params.itemId;
    const product = await graph.get(req, `sap.graph/SalesQuote/${id}/items/${itemId}/_product`, "$expand=distributionChains");
    res.send(`
      <h1>Product Detail</h1>
      <h4><code>For SalesQuote ${id} and item ${itemId}</code></h4>
      <pre><code>${JSON.stringify(product, 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


 

Assigned Tags

      5 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Mio Yasutake
      Mio Yasutake

      Hello Chaim Bendelac,

      First of all, thanks for this tutorial. Authorization part was especially interesting.

      I noticed the following issues.

       

      1.when I press a quote link and go to the detail screen it shows the following error.

      If I remove "C4-SQ~" from the id, it started to work.

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

       

      2. Pressing "Product Details" button shows no data.

      Seems that products used in quotes don't exist in Product entity.

      Regards,

      Mio

       

      Author's profile photo Chaim Bendelac
      Chaim Bendelac
      Blog Post Author

      Hello Mio-san,

       

      Thanks for taking the time to provide this valuable feedback. We are sorry, and confirm the problems you faced, which were due to a manual configuration error. We have corrected this manual error, and your code should now run without problems. We are also taking steps for this never to recur.

      Please reach out if you have any further questions!

      --

      Chaim Bendelac, SAP Graph

      Author's profile photo Luis Murillo
      Luis Murillo

      Hi Chaim, thanks a lot for your tutorial. I have a question. Is it posible, not only get data, but also write, for example, create a new Sales Order? (Put or Post?)

      Author's profile photo Chaim Bendelac
      Chaim Bendelac
      Blog Post Author

      Hi Luis, yes. SAP Graph will support "write/update/delete" operations as well, following the "read" operations. At this time (beta), only "read-access" is enabled.

      Author's profile photo Arpit Oberoi
      Arpit Oberoi

      Hi Team,

       

      There is an issue with API Sandbox as Customer Quote API returns 500 if you add $expand. And if this doesn't work, you can see the result of tutorial.

       

       

      Thanks,

      Arpit