Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ The libraries focus on streamlining [OAuth 2.0](https://oauth.net) access token
- [2.1.2 Spring Boot applications](#212-spring-boot-web-applications)
- [2.2 Token Flows](#22-token-flows-for-token-retrievals)
- [2.3 Testing utilities](#23-testing-utilities)
- [2.4 Token Exchange for Hybrid Authentication](#24-token-exchange-for-hybrid-authentication)
- [2.4.1 Jakarta Example](#241-jakarta-example-using-hybridtokenauthenticator)
- [2.4.2 Spring Boot Example](#242-spring-boot-example-using-hybridjwtdecoder)
3. [Installation](#installation)
4. [Troubleshooting](#troubleshooting)
5. [Common Pitfalls](#common-pitfalls)
Expand Down Expand Up @@ -118,6 +121,97 @@ In the table below you'll find links to detailed information.
|-------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [java-security-test](/java-security-test) | [Integration test code snippet](/samples/spring-security-hybrid-usage/src/test/java/sample/spring/security/junitjupiter/TestControllerIasTest.java) for Spring application <br/>[Integration test code snippet](/samples/java-security-usage/src/test/java/com/sap/cloud/security/samples/HelloJavaServletIntegrationTest.java) for Jakarta EE web.xml based servlets <br/> [Integration test code snippet](/samples/java-security-usage-ias/src/test/java/com/sap/cloud/security/samples/ias/HelloJavaServletIntegrationTest.java) for Jakarta EE annotation based servlets <br/> |

### 2.4 Token Exchange for Hybrid Authentication

In hybrid authentication setups, your application can accept tokens from both **SAP Identity Authentication Service (
IAS)** and **XSUAA** simultaneously. This approach eases migration from XSUAA to IAS by exchanging IAS user tokens for
XSUAA tokens behind the scenes.

**Goal**: Maintain backward compatibility during migration. Users authenticate via IAS, but the application continues
using XSUAA-based authorization (scopes, role collections).

#### Important Constraints

**IAS User Tokens Only**: Token exchange only applies to end-user tokens from IAS. Client credentials tokens are **not**
exchanged. Attempting exchange on technical tokens will result in errors.

**Performance**: Exchanged tokens are cached per request and reused until expiration. Caching is automatic and requires
no configuration.

#### How Token Exchange Works

```
1. Request arrives with Authorization: Bearer <token>
2. Library identifies issuer (IAS vs XSUAA) from token claims
3. Token validated against appropriate identity service
4. [IF IAS token + exchange enabled]
├─ Obtain strong IAS ID token (if access token provided)
├─ Call XSUAA /oauth/token endpoint with JWT bearer grant
└─ Store exchanged XSUAA token in SecurityContext
5. [IF exchange disabled OR XSUAA token]
└─ Use validated token directly
6. The XSUAA token contains the user's roles/scopes as defined in XSUAA
7. Authorization proceeds using familiar XSUAA token attributes
```

If the incoming token is already an XSUAA token, no exchange occurs—it's validated and used directly.
The XSUAA token will then be available in the security context as usual via getToken() as well as via getXsuaaToken().

**Failure Handling**: If token exchange fails (network issues, misconfiguration), authentication fails with 401
Unauthorized. No silent fallback occurs since IAS access tokens typically lack scopes needed for authorization.

#### Token Exchange Modes

The [`TokenExchangeMode`](java-security/src/main/java/com/sap/cloud/security/token/TokenExchangeMode.java)enum controls
when
and how IAS tokens are exchanged:

| Mode | Behavior |
|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`DISABLED`** | No exchange. Each token type is validated and used as-is. |
| **`PROVIDE_XSUAA`** | IAS token is validated and exchanged for XSUAA, but the IAS token remains primary in the security context. The XSUAA token is accessible via [`SecurityContext.getXsuaaToken()`](java-api/src/main/java/com/sap/cloud/security/token/SecurityContext.java). |
| **`FORCE_XSUAA`** | IAS token is exchanged for XSUAA and the XSUAA token replaces the IAS token in the security context. The resulting security context looks as if an XSUAA token had been received directly. |

The Initial token is still available via the [`SecurityContext.getInitialToken()`](java-api/src/main/java/com/sap/cloud/security/token/SecurityContext.java) getter and the ID token is available with [`SecurityContext.getIdToken()`](java-api/src/main/java/com/sap/cloud/security/token/SecurityContext.java)

**Mode Selection Guide**:

- Use **`PROVIDE_XSUAA`** when the app is migrated to AMS authorization and wants to offer combined XSUAA and AMS authorizations for migrated tenants (requires additional configuration of the AMS client library)
- Use **`FORCE_XSUAA`** for maximum backward compatibility—the app operates based on XSUAA tokens like before
- Use **`DISABLED`** or remove the property completely after completing the migration to IAS

#### Prerequisites for Token Exchange

1. Both XSUAA and IAS service bindings must be configured
2. IAS service binding must include `xsuaa-cross-consumption: true` parameter
3. Ensure XSUAA trusts the IAS identity provider

#### 2.4.1 Jakarta Example: Using [`HybridTokenAuthenticator`](java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenAuthenticator.java)

For Jakarta EE applications, use [
`HybridTokenAuthenticator`](java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenAuthenticator.java) in
a servlet filter.

For more information, see the [HybridTokenAuthenticator Javadoc](java-security/README.md#hybridtokenauthenticator-usage).

#### 2.4.2 Spring Boot Example: Using [`HybridJwtDecoder`](spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/HybridJwtDecoder.java)

For Spring Boot applications, [
`HybridIdentityServicesAutoConfiguration`](spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfiguration.java)
automatically configures hybrid authentication when both IAS and XSUAA bindings are detected.

For more information, see the [HybridJwtDecoder Javadoc](spring-security/README.md#token-exchange-configuration).


#### Troubleshooting

Common issues and solutions:

| Issue | Cause | Solution |
|-----------------------------------|------------------------------------------------|--------------------------------------------------|
| `Token exchange failed` exception | Missing XSUAA binding or invalid configuration | Verify both IAS and XSUAA service bindings exist |
| Exchange returns 401 | IAS binding missing `xsuaa-cross-consumption` | Add parameter to IAS service binding |

## Installation
The SAP Cloud Security Services Integration is published to maven central: https://search.maven.org/search?q=com.sap.cloud.security and is available as a Maven dependency. Add the following BOM to your dependency management in your `pom.xml`:
```xml
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,127 @@
package com.sap.cloud.security.token;

import javax.annotation.Nullable;

/**
* Extension interface for the {@link SecurityContext} to provide additional methods for extended
* security contexts.
* Extension interface for resolving and caching ID tokens from access tokens.
*
* <p>This interface defines the contract for automatic ID token resolution in {@link
* SecurityContext}. Implementations are responsible for:
*
* <ul>
* <li><b>Token Exchange:</b> Converting access tokens to ID tokens (e.g., via OAuth2 token
* exchange)
* <li><b>Caching Logic:</b> Determining when to return cached tokens vs. re-resolving
* <li><b>Expiration Handling:</b> Validating token expiration and triggering re-resolution when
* needed
* </ul>
*
* <p><b>Caching Strategy:</b>
*
* <p>The extension receives the currently cached ID token (if any) and decides whether to:
*
* <ol>
* <li><b>Return cached token:</b> If it exists and is still valid
* <li><b>Re-resolve token:</b> If cached token is expired, missing, or otherwise invalid
* </ol>
*
* This design decouples caching policy from {@link SecurityContext}, allowing implementations to
* customize expiration checks, implement token refresh logic, or add custom validation rules.
*
* <p><b>Thread Safety:</b>
*
* <p>Implementations must be thread-safe as the extension is registered globally via {@link
* SecurityContext#registerIdTokenExtension(IdTokenExtension)} but operates on thread-local tokens.
* Multiple threads may call {@link #resolveIdToken(Token)} concurrently.
*
* <p><b>Lifecycle:</b>
*
* <ol>
* <li><b>Registration:</b> Extension is registered once at application startup via {@link
* SecurityContext#registerIdTokenExtension(IdTokenExtension)}
* <li><b>Resolution:</b> Called by {@link SecurityContext#getIdToken()} when ID token is
* requested
* <li><b>Caching:</b> Returned token is cached in thread-local {@link SecurityContext}
* <li><b>Re-resolution:</b> Called again on next {@link SecurityContext#getIdToken()} if cached
* token expired
* </ol>
*
* <p><b>Usage Example (Spring Boot):</b>
*
* <pre>{@code
* @Configuration
* public class SecurityConfig {
* @PostConstruct
* public void registerExtensions() {
* SecurityContext.registerIdTokenExtension(
* new DefaultIdTokenExtension(tokenService, iasConfig)
* );
* }
* }
* }</pre>
*
* <p><b>Error Handling:</b>
*
* <p>Implementations should handle errors gracefully and return {@code null} if resolution fails
* (network errors, invalid tokens, missing configuration, etc.). {@link SecurityContext} will
* propagate the {@code null} to callers, allowing them to handle missing ID tokens appropriately.
*
* @see SecurityContext#getIdToken()
* @see SecurityContext#registerIdTokenExtension(IdTokenExtension)
* @see SecurityContext#clearIdToken()
*/
public interface IdTokenExtension {

/**
* Resolves the ID token from the extended security context.
* Resolves an ID token from the current security context.
*
* <p>This method is called by {@link SecurityContext#getIdToken()} to lazily resolve ID tokens
* when needed. The implementation receives the currently cached ID token (if any) and decides
* whether to return it or resolve a new one.
*
* <p><b>Caching Responsibility:</b>
*
* <p>The implementation is responsible for:
*
* <ol>
* <li><b>Checking cached token validity:</b> Inspect {@code cachedIdToken} expiration
* <li><b>Deciding whether to re-resolve:</b> Return cached token if valid, otherwise resolve
* new token
* <li><b>Token exchange:</b> If re-resolution needed, exchange access token for ID token
* </ol>
*
* <p><b>Access Token Availability:</b>
*
* <p>The access token is available via {@link SecurityContext#getToken()}. If no access token
* exists, the implementation should return {@code null} since token exchange is impossible.
*
* <p><b>Return Value Handling:</b>
*
* <ul>
* <li><b>Non-null token:</b> Cached in {@link SecurityContext} for subsequent {@link
* SecurityContext#getIdToken()} calls
* <li><b>{@code null}:</b> No caching occurs; subsequent calls will re-invoke this method
* </ul>
*
* <p><b>Thread Safety:</b>
*
* <p>This method may be called concurrently from multiple threads. Implementations must be
* stateless or use proper synchronization.
*
* @param cachedIdToken the currently cached ID token from thread-local {@link SecurityContext},
* or {@code null} if:
* <ul>
* <li>No ID token has been resolved yet for this thread
* <li>The cached token was cleared via {@link SecurityContext#clearIdToken()}
* <li>The security context was reset via {@link SecurityContext#setToken(Token)}
* </ul>
*
* @return the ID token or null if not available.
* @return the resolved ID token (may be the cached token if still valid), or {@code null} if:
* <ul>
* <li>No access token is available in the security context
* <li>Token exchange fails (network error, invalid configuration, etc.)
* <li>The access token does not support ID token exchange
* </ul>
*/
Token resolveIdToken();
Token resolveIdToken(@Nullable Token cachedIdToken);
}
Loading
Loading