SAP / cloud-sdk-js

Use the SAP Cloud SDK for JavaScript / TypeScript to reduce development effort when building applications on SAP Business Technology Platform that communicate with SAP solutions and services such as SAP S/4HANA Cloud, SAP SuccessFactors, and many others.
Apache License 2.0
164 stars 56 forks source link

Support for non-XSUAA JWT #2016

Closed piejanssens closed 2 years ago

piejanssens commented 2 years ago

The BTP Connectivity service supports specifying the JWKS (x_token_user.jwks) or JKU (x_token_user.jwks_uri) in the destination properties. This is useful in a scenario where the backend needs to generate a JWT to be able to call a destination with a specific - at runtime determined - user_name.

The topic of "bring your own JWT" is also covered in blog posts by your colleague Piotr Tesny: e.g. ​https://blogs.sap.com/2021/07/12/bring-your-self-made-user-jwt-with-keycloak-oidc./

At the moment SAP Cloud SDK seems to be working only with JWT's that are coming from UAA:

I have a workaround to show that BTP Connectivity supports self-signed JWT's.

        const fs = require('fs')
        const jwt = require('jsonwebtoken')
        const { randomUUID } = require('crypto')
        const axios = require('axios').default

        let key = fs.readFileSync('private.pem')

        let sSelfGeneratedJwt = jwt.sign(
          {
            user_name: 'test_user',
            jti: randomUUID(),
            iss: 'testIss'
          },
          key,
          {
            algorithm: 'RS256',
            expiresIn: '1d',
            keyid: 'testKey'
          }
        )

        const cred = await core.getDestinationServiceCredentials()
        const sServiceToken = await core.serviceToken('destination')
        const resp = await axios.get(`${cred.uri}/destination-configuration/v1/destinations/sf-learning-end-user`, {
          headers: {
            Authorization: 'Bearer ' + sServiceToken,
            'X-user-token': sSelfGeneratedJwt
          }
        })

       Result: valid OAuthSAMLBearer auth token in resp.data.authTokens[0].value

Can you please implement a more generic JWT verification process in order to support self signed JWT's?

Once this is supported it should be possible to use the existing API's to use a self-generated JWT as such:


        const dest = await core.getDestination(
          'competence-matrix-learning-end-user',
          {
            userJwt: sSelfGeneratedJwt
          }
        )
florian-richter commented 2 years ago

Thanks for the detailed request. I will copy it to our internal backlog and keep you posted once we implement your suggestion.

jjtang1985 commented 2 years ago

Hi @piejanssens ,

We implemented the feature about a month ago and the stable version was released. Please try version > 2.4.0. Here you can find some general guild about the configuration on BTP.

Please reopen, if you have questions.

Best, Junjie

piejanssens commented 1 year ago

Hi @jjtang1985,

Could we reopen this, please? I can't get it to work using the Cloud SDK...

        let sSelfGeneratedJwt = jwt.sign(
          {
            user_name: 'sfadmin',
            jti: randomUUID(),
            iss: 'whatever',
          },
          key,
          {
            algorithm: 'RS256',
            expiresIn: '1d',
            keyid: 'testKey',
          },
        )

Testing through a direct CF destinations API call:


        const cred = await core.getDestinationServiceCredentials()
        const sServiceToken = await core.serviceToken('destination')

        const axios = require('axios').default

        let sUrl = `${cred.uri}/destination-configuration/v1/destinations/sf-jwt-lms`

        const resp = await axios.get(sUrl, {
          headers: {
            Authorization: 'Bearer ' + sServiceToken,
            'X-user-token': sSelfGeneratedJwt,
          },
        })

This works, resp.data.authTokens[0] contains a valid access token.

Cloud SDK way:

        const dest = await getDestination({ destinationName: 'sf-jwt-lms', jwt: sSelfGeneratedJwt })
// ERROR HERE
        const response = await executeHttpRequest(
          dest,
          {
            //middleware: [sfLmsAuthenticator({ sUserId: 'sfadmin' })],
            method: 'get',
            url: `/odatav4/searchStudent/v1/Students?$filter=criteria/learnerID eq '${req.user.id}'&$select=firstName`,
          },
        )

It's a 401, with the following interesting body:

data:
{error: 'unauthorized', error_description: 'Unable to map issuer, whatever , to a single registered provider'}
error:
'unauthorized'
error_description:
'Unable to map issuer, whatever , to a single registered provider'

Note that it's indeed 'whatever' that I'm setting as the 'iss' value when generating the JWT. See https://github.com/cloudfoundry/uaa/blob/971ea56f3b1b71b6543dbe09eacfa9cd21582c13/server/src/main/java/org/cloudfoundry/identity/uaa/provider/oauth/ExternalOAuthAuthenticationManager.java#L172 There is no way to register an additional issuer in BTP, is there?