From 62969017590acab337abaf37af63169f1ad8e0e1 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Mar 2026 14:01:19 +0100 Subject: [PATCH 1/2] [Security] Support new `sap_id_type` claim --- .../OidcAuthTokenPrincipalExtractor.java | 38 +++++++++++--- .../OidcAuthTokenPrincipalExtractorTest.java | 50 +++++++++++++++++++ 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/cloudplatform/security/src/main/java/com/sap/cloud/sdk/cloudplatform/security/principal/OidcAuthTokenPrincipalExtractor.java b/cloudplatform/security/src/main/java/com/sap/cloud/sdk/cloudplatform/security/principal/OidcAuthTokenPrincipalExtractor.java index 02d7fbb04..a88a4b218 100644 --- a/cloudplatform/security/src/main/java/com/sap/cloud/sdk/cloudplatform/security/principal/OidcAuthTokenPrincipalExtractor.java +++ b/cloudplatform/security/src/main/java/com/sap/cloud/sdk/cloudplatform/security/principal/OidcAuthTokenPrincipalExtractor.java @@ -15,6 +15,9 @@ */ class OidcAuthTokenPrincipalExtractor implements PrincipalExtractor { + private static final String JWT_SAP_ID_TYPE_CLAIM = "sap_id_type"; + private static final String JWT_SAP_ID_TYPE_USER_VALUE = "user"; + private static final String JWT_SUB_CLAIM = "sub"; private static final String JWT_USER_UUID_CLAIM = "user_uuid"; private static final String JWT_EMAIL_CLAIM = "email"; @@ -42,18 +45,39 @@ public Try tryGetCurrentPrincipal() private Try tryGetPrincipalId( @Nonnull final DecodedJWT jwt ) { return Try.of(() -> { - final Claim userUuidClaim = jwt.getClaim(JWT_USER_UUID_CLAIM); + // First, try to use the new sap_id_type and sub claims (preferred approach) + final String sapIdType = getClaimAsString(jwt, JWT_SAP_ID_TYPE_CLAIM); + if( JWT_SAP_ID_TYPE_USER_VALUE.equals(sapIdType) ) { + final String sub = getClaimAsString(jwt, JWT_SUB_CLAIM); + if( sub != null ) { + return sub; + } + } - if( userUuidClaim != null && !userUuidClaim.isMissing() && !userUuidClaim.isNull() ) { - return userUuidClaim.asString(); + // Fallback to legacy user_uuid claim + final String userUuid = getClaimAsString(jwt, JWT_USER_UUID_CLAIM); + if( userUuid != null ) { + return userUuid; } - final Claim emailClaim = jwt.getClaim(JWT_EMAIL_CLAIM); - if( emailClaim != null && !emailClaim.isMissing() && !emailClaim.isNull() ) { - return emailClaim.asString(); + // Fallback to email claim + final String email = getClaimAsString(jwt, JWT_EMAIL_CLAIM); + if( email != null ) { + return email; } - throw new PrincipalAccessException("The current JWT does not contain the IAS user uuid or an email."); + throw new PrincipalAccessException( + "The current JWT does not contain a valid principal identifier. " + + "Expected one of: sap_id_type='user' with sub claim, user_uuid, or email."); }); } + + private String getClaimAsString( @Nonnull final DecodedJWT jwt, @Nonnull final String claimName ) + { + final Claim claim = jwt.getClaim(claimName); + if( claim != null && !claim.isMissing() && !claim.isNull() ) { + return claim.asString(); + } + return null; + } } diff --git a/cloudplatform/security/src/test/java/com/sap/cloud/sdk/cloudplatform/security/principal/OidcAuthTokenPrincipalExtractorTest.java b/cloudplatform/security/src/test/java/com/sap/cloud/sdk/cloudplatform/security/principal/OidcAuthTokenPrincipalExtractorTest.java index fba71f080..919f2b67c 100644 --- a/cloudplatform/security/src/test/java/com/sap/cloud/sdk/cloudplatform/security/principal/OidcAuthTokenPrincipalExtractorTest.java +++ b/cloudplatform/security/src/test/java/com/sap/cloud/sdk/cloudplatform/security/principal/OidcAuthTokenPrincipalExtractorTest.java @@ -66,6 +66,56 @@ void testReadPrincipalFromEmailFallback() assertThat(principal.getPrincipalId()).isEqualTo("fallback@example.com"); } + @Test + void testReadPrincipalFromSapIdTypeAndSub() + { + mockAuthTokenFacade(JWT.create().withClaim("sap_id_type", "user").withClaim("sub", "P123456")); + + final Principal principal = new OidcAuthTokenPrincipalExtractor().tryGetCurrentPrincipal().get(); + + assertThat(principal.getPrincipalId()).isEqualTo("P123456"); + } + + @Test + void testSapIdTypeAndSubPreferredOverUserUuid() + { + mockAuthTokenFacade( + JWT + .create() + .withClaim("sap_id_type", "user") + .withClaim("sub", "preferred-id") + .withClaim("user_uuid", "legacy-uuid")); + + final Principal principal = new OidcAuthTokenPrincipalExtractor().tryGetCurrentPrincipal().get(); + + assertThat(principal.getPrincipalId()).isEqualTo("preferred-id"); + } + + @Test + void testIgnoreSapIdTypeIfNotUser() + { + mockAuthTokenFacade( + JWT + .create() + .withClaim("sap_id_type", "app") + .withClaim("sub", "client-id") + .withClaim("user_uuid", "user-id")); + + final Principal principal = new OidcAuthTokenPrincipalExtractor().tryGetCurrentPrincipal().get(); + + assertThat(principal.getPrincipalId()).isEqualTo("user-id"); + } + + @Test + void testIgnoreSapIdTypeIfSubMissing() + { + mockAuthTokenFacade(JWT.create().withClaim("sap_id_type", "user").withClaim("email", "user@example.com")); + + final Principal principal = new OidcAuthTokenPrincipalExtractor().tryGetCurrentPrincipal().get(); + + assertThat(principal.getPrincipalId()).isEqualTo("user@example.com"); + } + private void mockAuthTokenFacadeWithMissingAuthToken() { AuthTokenAccessor.setAuthTokenFacade(() -> Try.failure(new AuthTokenAccessException("Auth token not mocked."))); From 5038fd8450d6155fbbb1419dbc9f95eafbfe2cc8 Mon Sep 17 00:00:00 2001 From: I538344 Date: Tue, 17 Mar 2026 14:11:35 +0100 Subject: [PATCH 2/2] release notes --- release_notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release_notes.md b/release_notes.md index 3112514a9..6d2169a53 100644 --- a/release_notes.md +++ b/release_notes.md @@ -12,7 +12,7 @@ ### ✨ New Functionality -- +- Added support for SAP Cloud Identity Services (SCI) `sap_id_type` and `sub` claims in OIDC principal extraction. When `sap_id_type=user`, the `sub` claim is now used as the [Subject Name Identifier](https://help.sap.com/docs/SAP_DATASPHERE/9f804b8efa8043539289f42f372c4862/fac3155d77154775b919ceba36ffc325.html) (User ID, Email, or Custom Attribute as configured in SCI). ### 📈 Improvements