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 s...
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
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": "john.doe@example.org"
// 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!
🔐💼
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
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
@Sisn/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": "john.doe@example.org"
"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!
🛡️