Skip to Content
Technical Articles
Author's profile photo Carlos Roggan

SAP Event Mesh: Sample: Webhook with Security [2]: Productive Account

This tutorial is about SAP Event Mesh (aka SAP Enterprise Messaging) running on SAP Business Technology Platform (SAP BTP, aka SAP Cloud Platform).
We send events to Event Mesh which delivers them via Webhook Subscription. The receiving endpoint is protected with OAuth and scope. The scenario is deployed to productive account of SAP BTP, Cloud Foundry environment.

Quicklinks:
Quick Steps
Sample Code
Previous Blog Post

Content

0. Prerequisites
00. Preparation
1. Event Mesh
1.1. Create Service Instance
1.2. Dashboard Subscription
2. Sender Application
2.1. Create Application
2.2. Dashboard Sender
3. Receiver Application
3.1. Create XSUAA instance
3.2. Create Application
3.3. Webhook Subscription
3.4. Scopes
A1 Sample Code Sender App
A2 Sample Code Receiver App

Overview

This post is a sibling of the previous blog, where the scenario and configurations are explained in detail.
There are a few differences between trial and productive account, so I’m creating a separate post  for each landscape. to avoid confusion.
I’m skipping explanations, so please refer to the sibling post in case of doubts.
Just posting an overview diagram for your convenience:

0. Prerequisites

The previous blog and all of its text is a prerequisite, but only if you need an introduction and detailed explanations.

00. Preparation

In this tutorial we’re creating an application to receive events (optionally a second app to send events).
Project structure as follows:

C:\hookorcrook
receiver
sender

1. Event Mesh

We need an instance of SAP Event Mesh service and we need access to the Event Mesh dashboard.

1.1. Create Service Instance

To create instance of Event Mesh service, “default” plan, we need configuration parameters which we store in a config file in the “sender” app folder:
C:\hookorcrook\sender\config-messaging.json

{
  "emname": "hookorcrookmessagingclient",
  "namespace": "hook/or/crook",
  "version": "1.1.0",
  "options": {
      "management": true,
      "messagingrest": true,
      "messaging": true
  },
  "rules": {
      "queueRules": {
          "publishFilter": [
              "${namespace}/*"
          ],
          "subscribeFilter": [
              "${namespace}/*"
          ]
      },
      "topicRules": {
          "publishFilter": [
              "${namespace}/*"
          ],
          "subscribeFilter": [
              "${namespace}/*"
          ]
      }
  }
}

There’s nothing special in this configuration, nothing to explain here.
The content of this file can be found in the Appendix 1 section.

The creation command:
cf cs enterprise-messaging default hookorcrookMsg -c config-messaging.json

1.2. Dashboard

The following steps are done in the Event Mesh Dashboard.
This requires a few preparation steps.

Subscribe to Event Mesh, “Standard” plan

We need to access the Event Mesh administration dashboard, we need to subscribe to the Event Mesh service plan “standard”.
It can be found in the “Service Marketplace” -> “Event Mesh”, as shown in below screenshot:

To subscribe, press “Create” and choose the “standard” plan.
After creation, please please resist the temptation to click on “Go to Application”.
You would be disappointed – because not authorized.
And even if you assign the required authorizations…. the browser cache will remember the evil error page. So in case you DID GO, then you need to deeply empty the browser cache or use a different browser.
That’s how quick fingers are punished…

Open Dashboard

No no no – DON’T press on “Go” yet.

Assign roles

Yes yes yes – first assign the required roles to your user.
To assign roles, you need a role collection – ideally create a new role collection for this silly tutorial.

Create Role Collection
In your subaccount, expand menu item “Security” and select “Role Collections”.
Click the Plus button and enter a name of your choice:

Add roles
The new role collection is empty, so we can fill it now in 2 steps.
Find your new role collection in the list, click on it, then press the “Edit” button.
Go to “Role” and press the value help icon.
Enter filter for “Application Identifier” as “xbem-app”
Select all roles.
Press “Add”

Assign to user
Even a role collection with roles doesn’t make sense, until it is assigned to users.
Back in the “edit role collection” screen, go to “Users” section and enter your user ID (email) and accept the proposal.
Make sure that your email is proposed, otherwise assignment might fail and you might get crazy later on.
Press the PLUS (+) button to add this user.

Finally, don’t forget to press “Save” on the top right corner, to persist the changes in the role collection.

Open Dashboard

If you’re sure that you saved the changes in the previous step – ok ok ok , then you may now press the “Go” button.
If you’ve forgotten where to find this crazy button:
Go back to our subaccount overview page -> “Instances and Subscriptions” -> find “Event Mesh” Subscription..
Then use the icon or the crazy context menu to open the dashboard.

Create Queue

To create our queue, we choose “Messaging Clients” in the menu, then click on our client created above.

Enter the name as “hookorcrookQueue” and leave the default values

Note:

The Event Mesh concepts require that “all queue names must have a namespace as prefix”.
The creation dialog is so helpful that it prepends the namespace that we defined in the service descriptor (config-messaging file).
Just make sure that you don’t add the namespace twice.

2. Sender

In productive landscape, where Event Mesh is available in service plan “default”, we have the possibility to use the “Test” tool to publish or consume messages.
The focus of this tutorial is on the receiving side, where we want to protect our webhook endpoint.
As such, it isn’t necessary to deploy a sender app, we can just use the “Test” tool.
However, if you’d like to create and deploy a sender app, you can use below sample code.

2.1. Create Application

In the previous blog post, we created a simple sample sender app, based on Event Mesh “Dev” plan, in trial account.
It cannot be simply reused, because in “Default” plan, there are some additional features and requirements.
For us, that means that we need to add a few lines of code, see below:

function _composeMsgRestUrlForSendMsg (msgCredentials, queueName){
    const SLASH = '%2F'
    const namespace = msgCredentials.namespace
    const namespaceEncoded = namespace.replace(/\//g, SLASH) 
    const fullQueue = namespaceEncoded + SLASH + queueName
    const uri = _getUaaForRest(msgCredentials).uri    
    return `${uri}/messagingrest/v1/queues/${fullQueue}/messages` 
}

When sending a REST request to Event Mesh, for publishing a message, we need to add the queue name to the URL.
In “Dev” plan, that’s only the queue name.
Now, in “Default” plan, the queue name contains the namespace.
To make it a bit more tricky, the slashes have to be encoded.
To make the trickyness even a bit more tricky, not all slashes have to be encoded, only the namespace slashes…

The rest of the code is the same like in previous blog.
Anyways, please find the full sender sample code in the  Appendix 1 section.

2.2. Use Dashboad

Even if you prefer to deploy a sender app – I’m anyways going to deploy a screenshot here, to show how to use the Test tool – even though it is not required, just because I like screenshots.
As such, to send a message, in the dashboard, we select our messaging client, then go to “Test” tab, select our queue, enter a text and press “Publish”..
And here it is, the screenshot:

Note:
In our scenario as described in previous blog we’re using plain text as content type.
That’s why we leave the default setting in the test tool.

The right side of the test tool looks tempting: “Consume Message”
However, we resist and don’t consume.

After sending a few messages we can see that the message counter of our queue increases.
Good.

3. Receiver

The setup for receiver is the same like in previous blog.
Just one small addition, for curiosity.

3.1. Create instance of XSUAA service

We want to protect our app endpoint with OAuth 2.0, therefore we need to bind our app to an instance of XSUAA.
To create the instance, we use a config file xs-security.json which we create in the app folder C:\hookorcrook\receiver.
The content can be copied from Appendix 2 section.

Creation command to be executed inside the receiver folder:
cf cs xsuaa application hookorcrookXsuaa -c xs-security.json

The configuration:

{
    "xsappname": "hookorcrookxsappname",
    "tenant-mode": "dedicated",
    "scopes": [
        {
            "name": "$XSAPPNAME.scopeforhookorcrook",
            "description": "Scope required to access webhook endpoint"
        },
        {
            "name": "$XSAPPNAME.scopefornothing",
            "description": "Scope just for test"
        }
    ],
    "authorities":["$XSAPPNAME.scopeforhookorcrook","$XSAPPNAME.scopefornothing"]
}

Note:
The explanation of the settings can be found in previous blog post.

Only one small addition:
Today, let’s add one additional scope: $XSAPPNAME.scopefornothing
We don’t require it anywhere, we only use it to verify the filtering option in the webhook.
Later.

3.2. Create Application

The application code is all the same like in previous blog post.

Only one small addition:
We want to print the scopes which are sent along with the JWT bearer token.
To do so, we only need to add one single line to our webhook endpoint implementation:

console.log(`JWT scopes: ${req.tokenInfo.getPayload().scope}`)

The module @sap/xssec provides a convenience API to deal with the JWT token, so we can easily access the “scope” property and print the value.

Also, we can easily access the full sample code in Appendix 2 and copy the value.

3.3. Create Webhook Subscription

After deployment of our receiver app, we can create the webhook subscription in the Event Mesh dashboard.
We know how to do that.
To view the environment variables:
cf env hookorcrookreceiver
The Webhook URL in my example: https://hookorcrookreceiver.cfapps.eu10.hana.ondemand.com/webhook/hookorcrook
Default Content-Type:
text/plain
OAuth token URL:
https:// abc123trial.authentication.sap.hana.ondemand.com/oauth/token

After creation of the webhook subscription we can observe our output written in the log:

Note:
If you aren’t happy with this approach, of if you’re just interested, have a look at the next blog post to see an alternative approach of configuring the webhook subscription.

3.4. Dealing with Scopes

This section is optional, it is just meant to verify what we anyways already know:
How the JWT carries scopes and how they can be filtered by the webhook subscription configuration.

After sending messages, we can view the scopes in the Cloud Foundry log.
As expected, the token contains our 2 scopes which we defined in the xs-security.json file:

In addition, there’s the default scope which is added in case of client_credentials flow (other than in case of logged-in user in authorization_code flow).
We can say that XSUAA does its job and sends all scopes – thanks, XSUAA

However….

You know those ungrateful tools…
As there are cases where you just don’t want ALL scopes to be sent in a JWT token, because there can be many scopes and they can increase the length of the JWT token which in turn can lead to the request being exploded….
So we need the possibility to reduce the amount of scopes sent along with the request.
And that’s achieved with the “Scopes” field in the webhook subscription dialog.

When fetching the JWT token from our XSUAA, there’s an option to specify the required scopes.
See documentation at Cloud Foundry.

OK, let’s try it.
Now we create a new webhook subscription (we have to delete or pause the existing one).
This time we specify our required filter in the “Scopes” field.
We need to enter the exact scope name, just the one which is known to XSUAA.
Thanks, but what exactly is the exact scope name?
In our example, we have either
scopeforhookorcrook
or
hookorcrookxsappname.scopeforhookorcrook
and there’s also the hashed name which we’ve seen in the console:
hookorcrookxsappname!t22273.scopeforhookorcrook

So which one is correct?
Yes… we fear it….
it is the scope name which is concatenated with the generated xsappname.
How to find out how it is generated?
We don’t need to dump the scopes to the console, as we did as an exercise in this tutorial.
We can find the generated xsappname in the environment of our deployed app (or in the service key of our xsuaa instance).
We need to anyways look into the environment, in order to view the clientid/secret of the xsuaa instance, there we can view as well the generated value of xsappname.
So here it is, the generated xsappname in my example:

We copy the value and concatenate it with the scopename suffix which we defined in the xs-security.json file.
In my example:
hookorcrookxsappname!t22273.scopeforhookorcrook<>

Finally, we can go ahead and create a new webhook subscription, with scope, as shown in below screenshot:

After sending a message, we can see the desired result in the Cloud Foundry log:

As we can see, now only one scope is being sent.
Yes, really it is only one scope and not a fake: in the screenshot I’ve left enough space on the right side…

OK.
That’s all we wanted to learn for today.

Ehmm. one additional (last) test I’ve done for your convenience:
I’ve once more created another webhook subscription, this time I’ve specified the second scope, which we’ve defined for our application:
hookorcrookxsappname!t22273.scopefornothing

Remember that this is a useless scope.
As a result, a valid token is fetched and sent to our webhook endpoint.
From messaging side that’s all fine.
However, our own scope check fails and prints an error text.
Below screenshot shows that only one scope is being sent – but unfortunately, it is the wrong one:

Sigh – we cannot always be on the winning side….
But we can see that our error handling is working as expected.

Summary

Today we’ve learned how to proceed in the productive landscape, in order to achieve just the same what we achieved in the trial landscape in previous blog post.
With other words:
We’ve protected our webhook endpoint with OAuth and with scope check.
We’ve created a webhook subscription, where we entered the credentials of the xsuaa instance which we created beforehand.
We’ve also entered a scope in order to reduce the amount of scopes that are sent by Event Mesh to our endpoint.
We’ve learned that we have to enter the generated full scope name (i.e. concatenated with generated xsappname, as shown in env).

In the next blog post, we want to have a look into an alternative way of protecting the endpoint.

Quick Steps

For the receiver (webhook) app, create xsuaa with following security config:

    "scopes": [{ "name": "$XSAPPNAME.yourscope"}],
    "authorities":["$XSAPPNAME.yourscope"]

View the credentials of THIS xsuaa instance, which is bound to receiver app.
Enter these credentials in the webhook subscription dialog.

Appendix 1: Sender App

Note: everything can be copy&pasted, only URLs need to be adapted

config-messaging.json

{
  "emname": "hookorcrookmessagingclient",
  "namespace": "hook/or/crook",
  "version": "1.1.0",
  "options": {
      "management": true,
      "messagingrest": true,
      "messaging": true
  },
  "rules": {
      "queueRules": {
          "publishFilter": [
              "${namespace}/*"
          ],
          "subscribeFilter": [
              "${namespace}/*"
          ]
      },
      "topicRules": {
          "publishFilter": [
              "${namespace}/*"
          ],
          "subscribeFilter": [
              "${namespace}/*"
          ]
      }
  }
}

manifest.yml

---
applications:
  - name: hookorcrooksender
    memory: 128M
    routes:
    - route: hookorcrooksender.cfapps.eu10.hana.ondemand.com
    services:
      - hookorcrookMsg

package.json

{
  "dependencies": {
    "express": "^4.17.1",
    "passport": "^0.4.0",  
    "@sap/xsenv": "latest",
    "@sap/xssec": "latest",
    "node-fetch": "2.6.2"
  }
}

server.js

const fetch = require('node-fetch')
const xssec = require('@sap/xssec')
const xsenv = require('@sap/xsenv')
const express = require('express')
const app = express()

const BINDING = xsenv.getServices({ myMessaging: { tag: 'enterprise-messaging' } })


/* App Server */
app.listen(process.env.PORT, function () { console.log('===> Server started') })

/* App Endpoints */
app.get('/send', async (req, res) => {
   const response = await sendMessage('Remember: Hook or Crook', 'hookorcrookQueue', BINDING.myMessaging)            
   res.send(`Done sending message (Status '${response}')`)
})


/* Helper */

async function sendMessage(msg, queueNamePostfix, msgCredentials){
    const uaa = _getUaaForRest(msgCredentials)
    const jwtToken = await _fetchJwtToken(uaa)
    const messagingRestUrl = _composeMsgRestUrlForSendMsg(msgCredentials, queueNamePostfix)

    const options = {
        method: 'POST',
        body: msg,
        headers: { 
            Authorization: 'Bearer ' + jwtToken,
            'Content-Type': 'text/plain',
            'x-qos' : 1  
        }
    }
    const response = await fetch(messagingRestUrl, options) 
    return response.status
}

function _getUaaForRest(msgCredentials){
    const msgRest = _findMsgRest(msgCredentials.messaging)
    return {
        uri : msgRest.uri,
        clientid : msgRest.oa2.clientid,
        clientsecret : msgRest.oa2.clientsecret,
        url : msgRest.oa2.tokenendpoint
    }
}

function _findMsgRest(messagingEntries){
    var result =  messagingEntries.filter(entry => { 
        const protocolEntries = entry.protocol
        const res = protocolEntries.filter(protocolEntry => {
            if(protocolEntry === "httprest"){
                return protocolEntry
            }
        })
        if(res[0]){
            return entry
        }	
    })
    return result[0] 
}

function _composeMsgRestUrlForSendMsg (msgCredentials, queueName){
    const SLASH = '%2F'
    const namespace = msgCredentials.namespace
    const namespaceEncoded = namespace.replace(/\//g, SLASH) 
    const fullQueue = namespaceEncoded + SLASH + queueName
    const uri = _getUaaForRest(msgCredentials).uri    
    return `${uri}/messagingrest/v1/queues/${fullQueue}/messages` 
}

async function _fetchJwtToken (uaa){
    return new Promise ((resolve, reject) => {
        xssec.requests.requestClientCredentialsToken(null, uaa, null, null, (error, token)=>{
            resolve(token)
        })
    })  
}

Appendix 2: Receiver App

Note: everything can be copy&pasted, only URLs need to be adapted

xs-security.json

{
    "xsappname": "hookorcrookxsappname",
    "tenant-mode": "dedicated",
    "scopes": [
        {
            "name": "$XSAPPNAME.scopeforhookorcrook",
            "description": "Scope required to access webhook endpoint"
        },
        {
            "name": "$XSAPPNAME.scopefornothing",
            "description": "Scope just for test"
        }
    ],
    "authorities":["$XSAPPNAME.scopeforhookorcrook","$XSAPPNAME.scopefornothing"]
}

manifest.yml

---
applications:
  - name: hookorcrookreceiver
    memory: 64M
    routes:
    - route: hookorcrookreceiver.cfapps.eu10.hana.ondemand.com
    services:
      - hookorcrookXsuaa

package.json

{
  "dependencies": {
    "@sap/xsenv": "latest",
    "@sap/xssec": "^3.2.10",
    "express": "^4.17.1",
    "passport": "^0.4.0"
  }
}

server.js

const xsenv = require('@sap/xsenv')
const serviceBindings = xsenv.getServices({ 
   myXsuaa: {tag: 'xsuaa'}
})
const UAA_CREDENTIALS = serviceBindings.myXsuaa

const express = require('express')
const app = express();
const xssec = require('@sap/xssec')
const passport = require('passport')
const JWTStrategy = xssec.JWTStrategy
passport.use('JWT', new JWTStrategy(UAA_CREDENTIALS))
app.use(passport.initialize())
app.use(express.text())


app.listen(process.env.PORT,  () => { console.log('===> Server started') })


app.post('/webhook/hookorcrook', passport.authenticate('JWT', {session: false}), (req, res) => {
    console.log(`===> [/webhook/hookorcrook] JWT scopes: ${req.tokenInfo.getPayload().scope}`)

    const auth = req.authInfo  
    if (! auth.checkScope(UAA_CREDENTIALS.xsappname + '.scopeforhookorcrook')) {
        console.log(`===> [/webhook/hookorcrook] ERROR scope for webhook access ('${UAA_CREDENTIALS.xsappname}.scopeforhookorcrook') is missing`)
        res.status(403).end('Forbidden. Authorization for webhook access is missing.')
    }else{
        console.log(`===> [/webhook/hookorcrook] Received message with payload: ${req.body}`)

        res.status(201).send()
    }      
})

Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.