Skip to Content
Technical Articles
Author's profile photo Pieter Janssens

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.

 

 

Assigned Tags

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

      Thank you for sharing, Pieter! Nice effort and write-up.

      Author's profile photo Holger Schäfer
      Holger Schäfer

      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

      app.get('/services', function (_, res) {
        const index = require('@sap/cds/app/index.js')
        res.send(index.html)
      })

      or adding a POST helper route for making it able to be called by VS Code REST client directly (no odata)

      app.use(express.json());
      app.post('/plantJSON', async function (req, res) {
          const { mode } = req.query
          let jsonObject = req.body
      
          const json = (mode === 'v2') 
              ? v2ToJSON(jsonObject) : JSON.stringify(jsonObject)
      
          // render SVG using plantuml
          const svg = await plantuml("@startjson\r\n" + json + "\r\n@endjson");
      
          // return custom content type by overriding CAP response handling
          res.set('Content-Type', 'image/svg+xml');
          res.send(svg)
      })

       

      Best place to start is alyways having a look into CAPS starting point, that nearly changes with every release 😉

       

      Regards

      Holger

      Author's profile photo Sebastian Van Syckel
      Sebastian Van Syckel

      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:

      // "tile service"
      const tileRouter = express.Router()
      tileRouter.use(
        passport.initialize(),
        passport.authenticate([...]),
        (req, res, next) => {
          // auth checks
          next()
        }
      )
      // /tile is relative to where the router is registered, i.e., absolute path is /ci/tile
      tileRouter.get('/tile', (req, res) => {
        // do stuff and send response
      })
      
      cds.on('bootstrap', (app) => {
        // register an express router that, in turn, has the actual middlewares registered
        app.use(
          /^\/(v2\/)?(?!ci).*/, //replace 'ci' with the name of your service path
          tileRouter
        )
      }

       

      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

      Author's profile photo Pieter Janssens
      Pieter Janssens
      Blog Post Author

      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

      Author's profile photo Sebastian Van Syckel
      Sebastian Van Syckel

      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