Technical Articles
Create authenticated endpoints in CAP that serve any type of response
CDS actions and functions are an easy way to respond with a response format other than OData. These are however limited to set of supported types. Unfortunately, Object/JSON is not part of the supported types. If you have a scenario like me (return specific JSON structure from a service to use in a dynamic SuccessFactors tile), where your service needs to return JSON in a specific structure and OData is not an option, then there is still a solution.
Luckily, under the hood, CAP uses express.js as its web application framework and this offers the possibility to serve anything supported by that framework. The authentication of the user seems daunting outside of CDS, but Sebastian Van Syckel pointed out that it’s a lot less complex than one might think.
After tweaking Sebastian’s example a little bit I ended up with this simple snippet:
const proxy = require('@sap/cds-odata-v2-adapter-proxy')
const cds = require('@sap/cds')
const xsenv = require('@sap/xsenv')
const passport = require('passport')
const JWTStrategy = require('@sap/xssec').JWTStrategy
cds.on('bootstrap', (app) => {
app.use(proxy())
app.get('/favicon.ico', (req, res) => res.status(204))
app.use(
/^\/(v2\/)?(?!ci).*/, //replace 'ci' with the name of your service path
passport.initialize(),
passport.authenticate(
new JWTStrategy(xsenv.getServices({ xsuaa: { tag: 'xsuaa' } }).xsuaa),
{ session: false }
),
(req, res, next) => {
if (!req.authInfo) return res.status(401).end()
//if (!req.authInfo.checkLocalScope('<role>')) return res.status(403).end()
next()
}
)
app.get('/tile', (req, res) => {
// do stuff using the req.user.id and return whatever you'd like
res.json({
tileType: 'sap.sf.homepage3.tiles.DynamicTile',
properties: {
title: 'Flashy Tile',
subtitle: 'Best Run Company',
numberValue: '8.67',
numberState: 'Positive',
numberDigits: 2,
stateArrow: 'Up',
numberFactor: '$',
icon: 'sap-icon://line-chart',
info: 'NASDAQ',
subinfo: 'BSTR'
}
})
})
})
module.exports = cds.server
Some things to note here:
- I’m using a negative lookahead regular expression to apply the authentication strategy on any request that does not match the path of my cds service (or the v2 proxy path)
- Not sure what path name your service has?
- The app.use() where the authentication is setup, is only required once. You can have any other number of endpoints such as app.get(‘/somepath’), app.post(‘/yikes’) or that are also processed (that’s what next() makes sure of).
- This sample shows how this can be combined with the cds odata v2 adapter proxy, this is optional
- I like to 204 the favicon.ico requests (because I don’t need a favicon for my service during testing from a browser + it makes sure this request doesn’t hit any breakpoints)
- You can use req.authInfo.checkLocalScope in any endpoint where you want to check for specific roles
- I left out the extension of the strategy (to avoid collisions with cds) since the regex of the path where the passport handler is used, is excluding cds requests (I hope Sebastian Van Syckel can confirm this is safe/unsafe and will update the post when he does)
This example shows that CAP can still cover these exceptional scenario’s where for some functionality of your app, you really need to return a different data type (or support a different incoming request) besides the built in types supported by cds.
Thank you for sharing, Pieter! Nice effort and write-up.
Hi Pieter,
great sharing!
Concerning custom responses, i also have a puplic repo with a CAP app doing PlantUML things with a custom server.js:
https://github.com/hschaefer123/cap-plantuml/blob/main/server.js
There are also some examples like
using custom app/index.html flp sandbox using /services for original index.html
or adding a POST helper route for making it able to be called by VS Code REST client directly (no odata)
Best place to start is alyways having a look into CAPS starting point, that nearly changes with every release 😉
Regards
Holger
Hi Pieter Janssens,
Nice post, thanks!
Yes, that is perfectly safe. All middlewares registered in on bootstrap are the first to be registered, and express calls them in order as long as next is invoked. However, could it be that you're missing the service path prefix for your get /tile middleware?
You can actually register an express router to a path (see this), so you could also do something like this:
FYI, we allow you to specify a type as the return value of an action/ function, which should cover returning a JSON, at least if you know the structure (OData doesn't allow for additional properties).
Best,
Sebastian
Hi Sebastian,
That improves readability/structure, I'm certainly going to use that express router approach on a specific path, thanks.
I experimented returning a custom type, but AFAIK it still contains some metadata not defined as a property, which could conflict with the strict expected JSON body by the requestor.
Best,
Pieter
Hi Pieter Janssens ,
Yes, that's true, the OData protocol adds metadata like @odata.context. However, you could try switching to serving to REST via .to('rest') or @protocol: 'rest'. Our plain REST adapter still has some limitations (we're actually working on right now), but unbound actions/ functions with a complex return type should be fully supported.
Best,
Sebastian