Skip to content

Conversation

@davidkna-sap
Copy link
Member

Closes SAP/ai-sdk-js-backlog#347.

@davidkna-sap davidkna-sap changed the title feat: IAS App-To-App Auth feat: [DO NOT MERGE] IAS App-To-App Auth Dec 2, 2025
@davidkna-sap davidkna-sap added the don't merge Don't merge label Dec 2, 2025
- Rename IAS resource properties for clarity (clientId → providerClientId, tenantId → providerTenantId)
- Move iasOptions parameter to main function signature in getDestinationFromServiceBinding for better API ergonomics
- Enhance IasClientCredentialsResponse interface with additional JWT claims (scimId, custom_iss, app_tid)
- Use IdentityServiceToken class from @sap/xssec for IAS-specific token decoding
- Add zone_uuid fallback for legacy tenant ID resolution
- Prioritize XSUAA user_id over IAS user_uuid in userId() function (aligning with priority-handling in other parts)
- Remove IAS handling in jwtBearerToken() to only handle XSUAA flow
- Add comprehensive test coverage for IAS token caching and resource isolation
- Improve documentation for targetUrl and resource parameters
/**
* IAS API resources. Present when resource parameter is specified in the token request.
*/
ias_apis?: string[];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even for single items it returns an array in the jwt, and when a providerClientId is given instead of name it's possible for it's supposed to return a token for everything the providerClientId applies to (possibly multiple depencies with different API permissions on the same provider).

export function isIasToken(decodedJwt: JwtPayload): boolean {
if (!decodedJwt.iss) {
return false;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could potentially check for ias_apis, ias_iss or scim_id but I'm not sure if exchanged-from-IAS XSUAA tokens could have these properties.

Comment on lines 48 to 63
if (assertion) {
const decodedJwt = new IdentityServiceToken(assertion);
const issuer = decodedJwt.issuer;
const issuerUrl = tryParseUrl(issuer, 'JWT assertion issuer');
subdomain = issuerUrl.hostname.split('.')[0];
// Replace subdomain in the URL from the service binding
// Reason: We don't want to blindly trust the URL in the assertion
const credentialsUrl = tryParseUrl(credentials.url, 'Identity Service');
const credentialsSplit = credentialsUrl.hostname.split('.');
credentialsUrl.hostname = [subdomain, ...credentialsSplit.slice(1)].join(
'.'
);
let normalizedUrl = credentialsUrl.toString();
if (normalizedUrl.endsWith('/')) {
normalizedUrl = normalizedUrl.slice(0, -1);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this it might be better to use IdentityService.getSafeUrlFromTokenIssuer(token, credentials.domains) and not rewrite the URL.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand, how does the xssec function help here? Wouldn't it just help in validation? The actual URL manipulation still needs to be done right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xssec uses the function to also determine the issuer for token verification, so in this case it could be enough verification to use the issuer url as is in this case. Additionally it has the advantage of catching cases of accounts400 vs accounts and any potential custom domains for ias.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidkna-sap Are you aware of this module? => packages/connectivity/src/scp-cf/subdomain-replacer.ts

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I migrated the code to use packages/connectivity/src/scp-cf/subdomain-replacer.ts with some IAS-specific changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xssec uses the function to also determine the issuer for token verification, so in this case it could be enough verification to use the issuer url as is in this case. Additionally it has the advantage of catching cases of accounts400 vs accounts and any potential custom domains for ias.

Why don't we use it then? is there a downside to it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't use it because for better consistency with the handling in XSUAA and the Java-SDK (where it seems to work well).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, let's avoid scope creep. 👍

Copy link
Collaborator

@KavithaSiva KavithaSiva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly looks good to me, added some minor comments.
Great work!

* ATTENTION: The property is mandatory in the following cases:
* - User-dependent authentication flow is used, e.g., `OAuth2UserTokenExchange`, `OAuth2JWTBearer`, `OAuth2SAMLBearerAssertion`, `SAMLAssertion` or `PrincipalPropagation`.
* - Multi-tenant scenarios with destinations maintained in the subscriber account. This case is implied if the `selectionStrategy` is set to `alwaysSubscriber`.
* - IAS token exchange with `iasOptions.authenticationType` set to `'OAuth2JWTBearer'`. In this case, the JWT is automatically used as the assertion for IAS token exchange.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q] Where is iasOptions.authenticationType set? It's not clear from the comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to clarify the comment, though arguably the IAS-case is covered by "User-dependent authentication flow".

Comment on lines 48 to 63
if (assertion) {
const decodedJwt = new IdentityServiceToken(assertion);
const issuer = decodedJwt.issuer;
const issuerUrl = tryParseUrl(issuer, 'JWT assertion issuer');
subdomain = issuerUrl.hostname.split('.')[0];
// Replace subdomain in the URL from the service binding
// Reason: We don't want to blindly trust the URL in the assertion
const credentialsUrl = tryParseUrl(credentials.url, 'Identity Service');
const credentialsSplit = credentialsUrl.hostname.split('.');
credentialsUrl.hostname = [subdomain, ...credentialsSplit.slice(1)].join(
'.'
);
let normalizedUrl = credentialsUrl.toString();
if (normalizedUrl.endsWith('/')) {
normalizedUrl = normalizedUrl.slice(0, -1);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand, how does the xssec function help here? Wouldn't it just help in validation? The actual URL manipulation still needs to be done right?

`Could not fetch IAS client credentials token for service of type ${resolvedService.label}: ${err.message}`
);
});
return token;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the usages of this function, and right now we use ClientCredentialsResponse base interface during caching. So, we would also have to change the caching function.

We can consider migrating to token classes available from xssec, with caching changes in a follow-up.

@davidkna-sap davidkna-sap force-pushed the davidkna-sap_poc-ias branch 2 times, most recently from 04e7de2 to 91a9927 Compare December 30, 2025 13:30
Comment on lines +171 to +173
if (arg.appTid) {
tokenOptions.app_tid = arg.appTid;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[req] I would use the suggested bool flag above in IasOptionsTechnical to determine if the provider tenant id(app_tid from the ias credentials) or the tenant id(from incoming jwt) needs to be supplied here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, but it still needs unit-tests.

davidkna-sap and others added 2 commits December 30, 2025 17:17
Co-authored-by: KavithaSiva <32287936+KavithaSiva@users.noreply.github.com>
Copy link
Contributor

@marikaner marikaner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM API-wise, I left quite a lot of comments (many minor things) and I didn't look at the tests for now. If there is anything you would like to have reviewed specifically, let me know.

Comment on lines 48 to 63
if (assertion) {
const decodedJwt = new IdentityServiceToken(assertion);
const issuer = decodedJwt.issuer;
const issuerUrl = tryParseUrl(issuer, 'JWT assertion issuer');
subdomain = issuerUrl.hostname.split('.')[0];
// Replace subdomain in the URL from the service binding
// Reason: We don't want to blindly trust the URL in the assertion
const credentialsUrl = tryParseUrl(credentials.url, 'Identity Service');
const credentialsSplit = credentialsUrl.hostname.split('.');
credentialsUrl.hostname = [subdomain, ...credentialsSplit.slice(1)].join(
'.'
);
let normalizedUrl = credentialsUrl.toString();
if (normalizedUrl.endsWith('/')) {
normalizedUrl = normalizedUrl.slice(0, -1);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidkna-sap Are you aware of this module? => packages/connectivity/src/scp-cf/subdomain-replacer.ts

Co-authored-by: Marika Marszalkowski <marika.marszalkowski@sap.com>
@davidkna-sap davidkna-sap requested a review from marikaner January 5, 2026 15:58
Copy link
Contributor

@marikaner marikaner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left one question. Good work!

Comment on lines 48 to 63
if (assertion) {
const decodedJwt = new IdentityServiceToken(assertion);
const issuer = decodedJwt.issuer;
const issuerUrl = tryParseUrl(issuer, 'JWT assertion issuer');
subdomain = issuerUrl.hostname.split('.')[0];
// Replace subdomain in the URL from the service binding
// Reason: We don't want to blindly trust the URL in the assertion
const credentialsUrl = tryParseUrl(credentials.url, 'Identity Service');
const credentialsSplit = credentialsUrl.hostname.split('.');
credentialsUrl.hostname = [subdomain, ...credentialsSplit.slice(1)].join(
'.'
);
let normalizedUrl = credentialsUrl.toString();
if (normalizedUrl.endsWith('/')) {
normalizedUrl = normalizedUrl.slice(0, -1);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xssec uses the function to also determine the issuer for token verification, so in this case it could be enough verification to use the issuer url as is in this case. Additionally it has the advantage of catching cases of accounts400 vs accounts and any potential custom domains for ias.

Why don't we use it then? is there a downside to it?

'experimental',
'defaultValue'
],
// The other default-allowed tags are not supported by tsdoc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥴

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

don't merge Don't merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants