The goal of this series is to introduce the different destination types of the SAP BTP, to show how they work and also to make them tangible with basic and easy to execute code examples. In parts one and two of this series, I looked at the basics, as well as the simplest destination types, NoAuthentication and BasicAuthentication. In this part, I’ll dive into a more complex topic for the first time: I’ll show you how to implement an OAuth 2.0 client credentials flow using Destination.
Before I turn to the concrete scenario and implementation, I will first take a closer look at the topic of OAuth. Most people in the IT environment have come across this term before, and a majority is still able to provide information when it comes to explaining at a high-level what it is and what it is used for. But as soon as it comes to the technical details, the exact workings of the protocols, the different authentication flows, etc., many (including me) quickly get tripped up. But fortunately I’m not alone in providing the most important information about OAuth, because there are also a large number of wonderful people who have a very deep understanding of the subject and are willing to document this knowledge and make it digitally available to their fellow world in an easy to understand way. One resource I came across a couple of years ago that has helped sharpen my understanding of this topic is this talk by Nate Barbettini: https://www.youtube.com/watch?v=996OiexHze0 I can highly recommend this video to anyone who would like to learn more about OAuth 2.0 (and for good measure, OIDC) or is looking for a simple yet solid introduction to the topic. The investment of this one hour is definitely worth it.
For those who are in a hurry, here is the most important information in a nutshell:
OAuth 2.0 is an open protocol that allows standardized, secure API authorization for desktop, web and mobile applications. This also covers the scenario of access delegation, i.e. that an application gets access to certain data or functionalities of another application with the permission of the user. Classic example: an application is granted permission by a user to send posts on Facebook or read contact data on their behalf.
Resource Owner – The owner of a resource (data). As a rule, this is simply a user, i.e. the person sitting in front of the computer / smartphone / tablet / …
Resource Server – This is the application that stores the resources (data) of the resource owner (user)
Client – The client is the third-party application that wants to access resources (data) of the resource server on behalf of the resource owner (user) and needs a token for authentication that contains the corresponding authorization grant (so to speak, the proof that the client has received permission from the resource owner to access the resource server)
Authorization Server – The Authorization Server is the instance that manages Authorization grants and issues the tokens (these are usually JWT (JSON Web Tokens), i.e. JSON objects that contain the relevant token information) that authorize the client to access the Resource Server on behalf of the Resource Owner. In some cases, Resource Server and Authorization Server are identical, but often they are separate instances
Depending on the specific use case, there are different flows in the OAuth 2.0 standard. In this part of the series, we will look at the client credentials flow. In order for a client to access a resource server, it must be registered with the Authorization Server, for each registered client there are client credentials, these are ultimately username and password. In the client credentials flow, the client also acts as the resource owner, because it does not obtain delegated access to the resource server, but instead requests a token for itself from the authentication server (using basic authentication with the client credentials) in order to then authenticate itself to the resource server with its own (technical) identity outside of a user context.
The XSUAA service, inside of the SAP BTP, handles the authorization flow between users, identity providers, and the applications or services. The XSUAA service is an internal development from SAP dedicated for the SAP BTP. In the Cloud Foundry project, there is an open-source component called UAA. UAA is an OAuth provider which takes care of authentication and authorization. SAP used the base of UAA and extended it with SAP specific features to be used in SAP BTP.
https://learning.sap.com/learning-journey/discover-sap-business-technology-platform/illustrating-sap-authorization-and-trust-management-service-xsuaa-_b9fde282-4cff-4dca-b146-7c8f8dde9955
In the context of OAuth 2.0, XSUAA is acting as the authentication server, i.e. it manages authorization grants and issues and validates JWT tokens. Important to know is, that there is always one XSUAA service instance per subaccount. As you will also later see in the concrete example, it is best practice to bind a dedicated XSUAA “service” resource to every application service (in our example: Client and Server). It took me a while to understand that those XSUAA services are not real running services in terms of a deployed container or something similar, but actually this are “only” pairs of Client ID and Client Secret (plus some additional meta information) that are injected into the application environment variables (VCAP_SERVICES). I.e. this binding represents a client in the sense of OAuth 2.0 terminology.
https://github.com/jfranken/sap-btp-destinations/tree/cloud-2-cloud-oauth2-client-credentials
This all sounds rather abstract so far, so at this point I will fall back on our well-known example from the previous parts of this series to illustrate the use case:
In this scenario, the BTP Destinations Enthusiast acts as the resource owner, the server app is the resource server, and XSUAA represents the authorization server. When the user calls the client/helloWorldClientPlain(), the client app subsequently attempts to access the /server/helloWorldServer() endpoint upon execution, but not on behalf of the user, but in this scenarios by means of technical client credentials. This endpoint is secured using XSUAA, which means that the server app expects a JWT token in the request header, which is issued and validated by XSUAA. To obtain this token, the client makes use of an according OAuth2ClientCredentials destination in the destination service.
At first, I will show you the basics of the implementation of the server app.
To be able to secure any endpoint of this service via XSUAA, we need to establish a binding between server app and XSUAA. This is done in the mta.yaml file that we use for the server app deployment:
...
resources:
...
- name: xsuaa-server-service
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
service-name: xsuaa-server-service
In addition, it is required to enable JWT authentication. With SAP CAP, this is pretty staightforward within the cds configuration, which is located in the .cdsrc.js:
{
"requires": {
"auth": {
"kind": "jwt-auth"
},
"uaa": {
"kind": "xsuaa"
}
}
}
Authentication checks can be added by means of annotations which can be added in the service CDS files (the example below shows the server-service.cds file), either on service level or for single entities / functions / actions. In the given example, I only add a very rudimentary check that requires nothing else than the caller to be authenticated at all (i.e. sending a valid JWT token, authenticated-user is an according pseudo role). It is also possible to check for concrete role assignments within the annotation, which is in real world scenarios normally the case, however, since this series aims at keeping the focus on the pure communication flow between the different instances, I try to keep it as simple as possible:
service ServerService @(requires: 'authenticated-user') {
function helloWorldServer() returns String;
}
The service implementation within the file server-service.js isn’t very spectacular and should be familiar to the attentive readers of the first two parts of the series:
module.exports = async (srv) => {
srv.on('helloWorldServer', async (req) => {
return 'Hello World from Server'
})
}
As described above, the client app needs to use a destination to obtain a valid JWT token that allows access to the endpoint of the server app, which is secured via XSUAA. The most important aspect here is that I store the client credentials from the XSUAA binding of the server app in the destination, because these represent the OAuth 2.0 client that is authorized to access the server app. The destination configuration in detail is as follows:
Please check the README.md file in the github repo. There you find a detailed description how you can find the concrete URLs and credentials via the BTP Cockpit and the Cloud Foundry CLI.
As always, the client includes two alternative implementations. On the one hand, the server call via Cloud SDK, and on the other hand, a plain variant that illustrates the communication processes step by step.
srv.on('helloWorldClientCloudSDK', async () => {
return executeHttpRequest({
destinationName: 'Server'
}).then((response) => response.data)
})
First, the client authenticates against XSUAA with the client credentials from the destination service binding. I.e. here we already have the first client credentials flow to obtain a token that allows the client app to access the destination service. Afterwards, the client app requests the destination “Server” from the destination service, which is described in the chapter above. During this call, the destination service executes another client credentials flow and adds the token into the response which is then sent back to the client app. This could already be used to access the endpoint of the server app. However, for demonstration purposes, the client app implementation triggers another client credentials flow against XSUAA with the client credentials of the destination configuration.
srv.on('helloWorldClientPlain', async () => {
// get all the necessary destination service parameters from the
// service binding in the VCAP_SERVICES env variable
const vcapServices = JSON.parse(process.env.VCAP_SERVICES)
const destinationServiceUrl =
vcapServices.destination[0].credentials.uri +
'/destination-configuration/v1/destinations/'
const destinationServiceClientId =
vcapServices.destination[0].credentials.clientid
const destinationServiceClientSecret =
vcapServices.destination[0].credentials.clientsecret
const destinationServiceTokenUrl =
vcapServices.destination[0].credentials.url +
'/oauth/token?grant_type=client_credentials'
// before we can fetch the destination from the destination
// service, we need to retrieve an auth token
const token = await axios.post(
destinationServiceTokenUrl,
null,
{
headers: {
authorization: 'Basic ' +
Buffer.from(
destinationServiceClientId +
':' +
destinationServiceClientSecret'
).toString('base64'),
},
}
)
const destinationServiceToken = token.data.access_token
// with this token, we can now request the "Server" destination
// from the destination service
const headers = {
authorization: 'Bearer ' + destinationServiceToken,
}
const destinationResult = await axios.get(
destinationServiceUrl + 'Server',
{ headers }
)
const destination = destinationResult.data
// now, we use the retrieved the destination information to send
// a HTTP request to the token service endpoint;
// to authenticate, we take the Client ID attribute and the
// Client Secret attribute from the destination,
// encode ClientId:ClientSecret to Base64 and send the resulting
// string prefixed with "Basic " as Authorization
// header of the request;
// as a response, we receive a JWT token that we can then use to
// authenticate against the server
// alternatively, the JWT token could directly be fetched from
// destination.authTokens[0].value
const jwtTokenResponse = await axios.get(
destination.destinationConfiguration.tokenServiceURL +
'?grant_type=client_credentials',
{
headers: {
Authorization: 'Basic ' +
btoa(destination.destinationConfiguration.clientId +
':' +
destination.destinationConfiguration.clientSecret),
}
}
)
const jwtToken = jwtTokenResponse.data.access_token
// here we call the server instance with the bearer token we
// received from the token service endpoint
const destinationResponse = await axios.get(
destination.destinationConfiguration.URL,
{
headers: {
Authorization: 'Bearer ' + jwtToken,
},
}
)
return destinationResponse.data
})
Once the client and server are deployed and the destination is configured correctly, calls to both client endpoints should return the following result:
That’s it! In the next part of this series, I will go one step further. Instead of using technical client credentials, I will show you how you can use OAuth2TokenExchange destinations with authorization delegation. The client app will call the server app by propagating the identity of the user who calls the client endpoint in the beginning, i.e. we will cover one of the core scenarios for which OAuth 2.0 has been invented.
The code examples provided are not suitable to be transferred 1:1 into productive implementations, but they only serve to illustrate the functioning of destinations and Destination Service. In some cases, the interaction with the Destination Service is deliberately implemented in a “cumbersome” manner in order to illustrate the communication processes as explicitly and in detail as possible. SAP CAP and the SAP Cloud SDK provide various functionality to simplify and abstract the use of destinations. However, this would be more of a hindrance than a benefit to the purpose of the illustration.