Mobile First – Making the SAP IAS-based Proof Key for Code Exchange (PKCE) great again! 🎉

Important Links

Hello, fellow community members! 🌐

It’s been a while since our last blog post on the PKCE (Proof Key for Code Exchange) flow in SAP Build Apps, which enables secure and enterprise-grade authentication for public (mobile) clients (such as SAP Build Apps solutions), that we do not consider trustworthy enough to store a Client Secret on.

SAP Build Apps (AppGyver) and Proof Key for Code Exchange (PKCE)
or “Striving for enterprise-grade security in SAP low-code applications”

Today, we have some exciting news to share, and while the name SAP AppGyver may now be SAP Build Apps (yepp – we still owe you an update of that tutorial!), the essence of the PKCE flow remains as critical as ever. PKCE continues to be the go-to standard for ensuring robust authentication of public clients. If you’re unfamiliar with PKCE, we recommend checking out our previous blog post (click here) and RFC7636 (click here) first for more details. 📱💼

Now, let’s dive into the new developments! SAP Identity Authentication (IAS) Service recently tightened its security guidelines. As a result, there are some important changes to consider when using the PKCE flow in your SAP Business Technology Platform (BTP) applications secured by XSUAA.

Previously, the required Client ID of the respective XSUAA app registration (using SAP IAS as trusted OIDC IDP), was included in the JWT token “aud” (audience) claim issued through the PKCE flow. This setup allowed a seamless (behind the scenes) token exchange, managed by the @sap/xssec package.🛡🔄

However, for security reasons (as is often the case 😄), this behavior, where the XSUAA Client ID was part of the “aud” claim, has been modified. Now, the XSUAA Client ID is no longer part of the audience claim for tokens issued to public clients using the PKCE flow. 🔒🔄

In consequence, the the automated token exchange (SAP IAS to XSUAA) handled by @sap/xssec is not applicable (out of the box) anymore. While this leads to some additional development effort, the changes underscore the commitment to strengthening security measures within the SAP ecosystem. So let’s try to make the best out of it and see how to overcome this restriction. 🚀🔐

Current state

Screenshot of an Application Registration in SAP Identity Authentication Service showing the configuration and Client ID of a public-client enabled Application Registration

As you can see below, the respective JWT token “aud” claim requested through this application, only contains the Client ID of the respective application used to initiate the PKCE flow, but no longer the Client ID of the XSUAA instance required for “xsuaa-cross-consumption“.

Recap – xsuaa-cross-consumption: Enabling this option will add the Client ID of the XSUAA application registration (which was created in SAP IAS when you configured the trust between SAP BTP XSUAA and SAP IAS) to the Audience claim of tokens issued by an app registration associated to a respective SAP Cloud Identity Service Instance (created in the corresponding Subaccount).

{
  ...
  "iss": "https://sap-demo.accounts.ondemand.com",
  "groups": "SubaccountAdmin",
  "given_name": "John",
  "family_name": "Doe",
  "email": "[email protected]"
  // Public App Registration Client ID
  "aud": "422c38a9-c8b5-4960-a0ed-f5f173c6d0c3",
  ...
}

Long story short – In scenarios using SAP IAS, for enhanced PKCE protection, an additional token exchange is required. This exchange must be initiated by the backend, utilizing either a X.509 certificate/key or Client Credentials issued by SAP IAS. In this second token exchange, a new JWT token is generated, which carries the XSUAA Client ID once again in the “aud” claim. Back to normal, right? 🔄

So, what’s the buzz about this additional step? Think of it as step number four in the visual representation below. Your CAP app or Approuter takes the public access token (missing the required XSUAA “aud value) and exchanges it for another private access token (including the required XSUAA Client ID), ideally caching it afterward for improved performance. 🚀

The reason is pretty simple and will be reiterated again and again in this blog post. It is all about security! While we do not want our public (mobile) clients to store any credentials or certificates, our backend server can handle this for us in a safe manner. Therefore, instead of directly relying on a JWT token of a public client for accessing our backend services, we can just do another round-trip using a valid certificate/key or a secret, to get back an “ordinary” access token which our CAP application can trust!

The rationale behind this approach is straightforward, and we’ll emphasize it throughout this blog post: it’s all about security! 🛡 When it comes to our public (mobile) clients, we want to ensure that they don’t store any credentials or certificates. Instead, we can delegate this responsibility to our secure backend server. Rather than relying directly on a JWT token from a public client to access our backend services, we opt for an extra layer of protection. 🤝 Here’s the game plan – we perform an additional round-trip, utilizing a valid certificate/key or a secret bound to our CAP app or Approuter, to obtain a regular access token that our backend can wholeheartedly trust! 🔐💼

Visualization of a public token exchange flow in a SAP BTP scenario using a public client, SAP IAS and a CAP app or an SAP Application Router

Token Exchange Architecture

Those exchanged tokens, containing the necessary Client ID in the aud” claim again, can be seamlessly transformed into SAP XSUAA tokens that are fully compatible with your SAP BTP applications, including CAP. This magic happens behind the scenes, thanks to the @sap/xssec package and has already been part of our existing sample-scenario. 🪄🎩

For your convenience, we’ve gone the extra mile and updated up our SAP-Samples repository. 🌟 It’s your one-stop destination for a sample setup that puts the PKCE flow into action. Whether you’re targeting SAP Approuter or a CAP-only approach, the repository has got you covered. You’ll find two samples that handle the additional token exchange seamlessly. 📦🚀

CAP Application (without Approuter)

CAP Application (with Approuter)

Thanks to the SAP Approuter or your CAP application having a SAP Cloud Identity Service Binding inked to your SAP IAS instance, you can leverage the “urn:ietf:params:oauth:grant-type:jwt-bearer” grant flow (click here for details). Besides the initial public JWT token (acting as assertion), this flow requires valid Client Credentials or a X.509 Certificate/Key from the binding itself. 🔮🔑

Below, you can find a snippet of the code that orchestrates the additional token exchange. It turns an existing “public” JWT token (issued during the PKCE flow) into a “private” (that’s what we call it here) JWT token using the jwt-bearer grant type and the binding details of SAP IAS. 🪄📜

async function exchangeToken(token) {
  try {
    // Prepare the request options for the token exchange.
    const options = {
      method: "POST",
      // The URL of the SAP IAS token endpoint.
      url: `${iasConfig.url}/oauth2/token`,
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      // The body of the request, including the grant type, client ID, 
      // and the public JWT token issued through PKCE as assertion.
      data: new URLSearchParams({
        grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
        client_id: iasConfig.clientid,
        // Remove 'Bearer ' prefix from the token if it exists.
        assertion: token.split("Bearer ")[1] || token,
      }).toString(),
      // Configure the HTTPS agent with the certificate and key.
      httpsAgent: new https.Agent({
        cert: iasConfig.certificate,
        key: iasConfig.key,
      }),
    };

    // Send the request and wait for the response.
    const response = await axios(options);
    // Return the ID token from the response.
    return response.data?.id_token;
  } catch (err) {
    // Log the error message and re-throw the error.
    console.error(err.message);
    throw(err)
  }
}

Handling this exchange on the public client side would be risky from a security perspective (actually that’s why we are using the PKCE flow). However, our backend takes on this task seamlessly and reliably, having access to a X.509 certificate/key. 🤝

By authenticating itself as a trusted, non-public client to SAP IAS, the Approuter or CAP application is eligible to obtain the necessary JWT token containing the required XSUAA Client ID in the audience claim.

Updated State

Screenshot of an Application Registration in SAP Identity Authentication Service showing the configuration and Client ID of an XSUAA Application Registration

XSUAA Application Registration (created by SAP XSUAA – SAP IAS trust configuration)

As you can see below, the respective JWT token “aud” claim now contains the Client ID of the public application used to initiate the PKCE flow, as well as the Client ID of our XSUAA application registration required for “xsuaa-cross-consumption“.

Wow, that’s all we need to continue our regular flow as this token will be seamlessly accepted by the @sap/xssec package, initiating an automated token exchange to a valid XSUAA token given your CAP application is bound to an XSUAA service instance and xsuaa is your chosen authentication strategy.

{
  ...
  "iss": "https://sap-demo.accounts.ondemand.com",
  "groups": "SubaccountAdmin",
  "given_name": "John",
  "family_name": "Doe",
  "email": "[email protected]"
  "aud": [
    // XSUAA App Registration Client ID
    "4bd01a9d-a7cf-481c-ab46-c0e654d34939",
    // Public App Registration Client ID
    "422c38a9-c8b5-4960-a0ed-f5f173c6d0c3"
  ],
  ...
}

In our previous blog post, we emphasized the importance of the PKCE flow in safeguarding your authentication process. By shifting the responsibility of handling the additional token exchange to the backend and caching the tokens right there, we maintain security without burdening the public client. 🤝 Although the additional exchange happens at lightning-speed ⚡, using a caching approach (in-memory / Redis / …), you can reduce the impact of the exchange to a minimum!

Disclaimer – While our example provides a solid foundation for the additional token exchange, your specific requirements may necessitate additional verifications, route exclusions, orchestration of multiple SAP IAS or XSUAA bindings, handling of multiple Approuter or CAP Service instances, or token caching in a Redis Cache 💽 for a production-ready setup. Our sample operates under the assumption that token exchange is essential for all Approuter routes or CAP services being bound to a single SAP IAS and XSUAA instance. Keep in mind that your unique setup may diverge – especially when it comes to caching requirements… 🌟

Stay secure and keep those tokens protected! 🛡

Scroll to Top