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
Comment on lines 145 to 147
const tokenOptions: IdTokenFetchOptions = {
token_format: 'jwt'
};
Copy link
Member Author

@davidkna-sap davidkna-sap Dec 19, 2025

Choose a reason for hiding this comment

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

In the e2e multi-tenant testing with user token exchange this was required because otherwise IAS returns an opaque token.

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.

Added few more requests to be worked on.

Comment on lines 132 to 153
interface IasOptionsBase {
/**
* The target URL of the destination that the IAS token is requested for.
* @default to the (identity service) URL from the service binding.
*/
targetUrl?: string;
/**
* The application resource(s) for which the token is requested.
* The token will only be usable to call the requested application(s).
* Either provide the app name (common case) or the provider client ID
* and tenant ID (optional).
*/
resource?: IasResource | IasResource[];
/**
* The consumer (BTP) tenant ID of the application.
* May be required for multi-tenant communication.
*/
appTid?: string;
/**
* Additional parameters to be sent along with the token request.
*/
extraParams?: Record<string, string>;
Copy link
Collaborator

@KavithaSiva KavithaSiva Dec 19, 2025

Choose a reason for hiding this comment

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

For the App2App case, both targetUrl and resource are required.
Even if they are optional here, we have to document this. Consider adding to JSDoc and to the documentation portal.

/**
* IAS API resources. Present when resource parameter is specified in the token request.
*/
ias_apis?: string[];
Copy link
Collaborator

Choose a reason for hiding this comment

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

[q] Should this be an array? Given we only pass one resource name?

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).

* @param options.iasOptions - Options to change IAS token fetching (see {@link IasOptions}).
* @returns A JWT bearer token.
*/
export async function jwtBearerToken(
Copy link
Collaborator

@KavithaSiva KavithaSiva Dec 19, 2025

Choose a reason for hiding this comment

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

[req] The changes to this function are to support using an IAS token to fetch destinations using the destination service, and I would keep this out of this PR and do it in the the dedicated PR instead.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should the changes to serviceToken also be reverted? It is responsible for (some of) the caching.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes. I think we can also revert the changes to serviceToken function for now as it also is mostly used with functions calling a service using an access token. Other usages might require more checks.

The transformServiceBindingToClientCredentialsDestination seems to be an exception, but I think we can add IAS support to this function later.

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've moved the IAS-specific parts of serviceToken to serviceTokenIas for now and marked it as experimental to keep the client credentials cache for IAS around.

KavithaSiva and others added 2 commits December 22, 2025 09:21
- 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.

* @param options.iasOptions - Options to change IAS token fetching (see {@link IasOptions}).
* @returns A JWT bearer token.
*/
export async function jwtBearerToken(
Copy link
Member Author

Choose a reason for hiding this comment

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

Should the changes to serviceToken also be reverted? It is responsible for (some of) the caching.

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
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.

* @param options.iasOptions - Options to change IAS token fetching (see {@link IasOptions}).
* @returns A JWT bearer token.
*/
export async function jwtBearerToken(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes. I think we can also revert the changes to serviceToken function for now as it also is mostly used with functions calling a service using an access token. Other usages might require more checks.

The transformServiceBindingToClientCredentialsDestination seems to be an exception, but I think we can add IAS support to this function later.

@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>
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.

3 participants