Skip to content

Commit

Permalink
app_tid adaption (#1238)
Browse files Browse the repository at this point in the history
* add app_tid claim, add deprecation notice for zone-uuid

* add getAppTid method and deprecate getZoneId in Token interface

* add app_tid claim to JwtGenerator

* add x-app_tid and x-client_id headers

* update DefaultOAuth2TokenKeyService

* update SpringOAuth2TokenKeyService with x-app_tid and x-client_id
headers

* update OAuth2TokenKeyServiceWithCache
  • Loading branch information
liga-oz committed Jul 21, 2023
1 parent 66762f9 commit 6e1a7b9
Show file tree
Hide file tree
Showing 30 changed files with 350 additions and 190 deletions.
18 changes: 13 additions & 5 deletions java-api/src/main/java/com/sap/cloud/security/token/Token.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,22 @@ default Set<String> getAudiences() {
}

/**
* Returns the Zone identifier, which can be used as tenant discriminator
* @deprecated use {@link Token#getAppTid()} instead
*/
@Deprecated
default String getZoneId() {
return getAppTid();
}

/**
* Returns the app tenant identifier, which can be used as tenant discriminator
* (tenant guid).
*
* @return the unique Zone identifier.
* @return the unique application tenant identifier.
*/
String getZoneId();
default String getAppTid(){
return hasClaim(SAP_GLOBAL_APP_TID) ? getClaimAsString(SAP_GLOBAL_APP_TID) : getClaimAsString(SAP_GLOBAL_ZONE_ID);
}

/**
* Returns the OAuth2 client identifier of the authentication token if present.
Expand Down Expand Up @@ -271,7 +281,6 @@ default Map<String, Object> getClaims() {
* Example: <br>
* <code>
* import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;
*
* token.getAttributeFromClaimAsString(EXTERNAL_ATTRIBUTE, EXTERNAL_ATTRIBUTE_SUBACCOUNTID);
* </code>
*
Expand All @@ -296,7 +305,6 @@ default String getAttributeFromClaimAsString(String claimName, String attributeN
* Example: <br>
* <code>
* import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;
*
* token.getAttributeFromClaimAsString(XS_USER_ATTRIBUTES, "custom_role");
* </code>
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ private TokenClaims() {
*/
public static final String SAP_GLOBAL_SCIM_ID = "scim_id";
public static final String SAP_GLOBAL_USER_ID = "user_uuid";
public static final String SAP_GLOBAL_ZONE_ID = "zone_uuid"; // tenant GUID


/**
* @deprecated Use {@link TokenClaims#SAP_GLOBAL_APP_TID} instead.
*/
@Deprecated(forRemoval = true)
public static final String SAP_GLOBAL_ZONE_ID = "zone_uuid"; // legacy claim
public static final String SAP_GLOBAL_APP_TID = "app_tid"; // tenant GUID

public static final String GROUPS = "groups"; // scim groups
public static final String AUTHORIZATION_PARTY = "azp"; // Authorization party contains OAuth client identifier
public static final String CNF = "cnf"; // X509 certificate ("cnf" (confirmation)) claim
Expand All @@ -46,7 +54,7 @@ private XSUAA() {

public static final String ORIGIN = "origin";
public static final String GRANT_TYPE = "grant_type"; // OAuth grant type used for token creation
public static final String ZONE_ID = "zid"; // tenant GUID -> SAP_GLOBAL_ZONE_ID
public static final String ZONE_ID = "zid"; // tenant GUID same value as SAP_GLOBAL_APP_TID
public static final String CLIENT_ID = "cid"; // avoid using directly, make use of Token#getClientId() instead
public static final String SCOPES = "scope"; // list of scopes including app id, e.g. "my-app!t123.Display"
public static final String ISSUED_AT = "iat";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class JwtGenerator {
public static final String DEFAULT_KEY_ID = "default-kid";
public static final String DEFAULT_KEY_ID_IAS = "default-kid-ias";
public static final String DEFAULT_ZONE_ID = "the-zone-id";
public static final String DEFAULT_APP_TID = "the-app-tid";
public static final String DEFAULT_USER_ID = "the-user-id";

private static final Logger LOGGER = LoggerFactory.getLogger(JwtGenerator.class);
Expand Down Expand Up @@ -147,7 +148,8 @@ private void setDefaultsForNewToken(String azp) {
withClaimValue(TokenClaims.XSUAA.CLIENT_ID, azp); // Client Id left for backward compatibility
if (service == Service.IAS) {
jsonPayload.put(TokenClaims.AUDIENCE, azp);
jsonPayload.put(TokenClaims.SAP_GLOBAL_ZONE_ID, DEFAULT_ZONE_ID);
jsonPayload.put(TokenClaims.SAP_GLOBAL_ZONE_ID, DEFAULT_ZONE_ID); //TODO to be removed once fallback is not supported
jsonPayload.put(TokenClaims.SAP_GLOBAL_APP_TID, DEFAULT_APP_TID);
jsonPayload.put(TokenClaims.SAP_GLOBAL_USER_ID, DEFAULT_USER_ID);
jsonPayload.put(TokenClaims.SAP_GLOBAL_SCIM_ID, DEFAULT_USER_ID);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public void setUp() {
}

@Test
public void createToken_setsDefaultsForTesting() {
public void createXsuaaToken_setsDefaultsForTesting() {
Token token = cut.createToken();

assertThat(token).isNotNull();
Expand All @@ -77,14 +77,14 @@ public void createToken_setsDefaultsForTesting() {
assertThat(token.getClaimAsString(AUTHORIZATION_PARTY)).isEqualTo(DEFAULT_CLIENT_ID);
assertThat(token.getClientId()).isEqualTo(DEFAULT_CLIENT_ID);
assertThat(token.getExpiration()).isEqualTo(JwtGenerator.NO_EXPIRE_DATE);
assertThat(token.getZoneId()).isEqualTo(DEFAULT_ZONE_ID);
assertThat(token.getAppTid()).isEqualTo(DEFAULT_ZONE_ID);
assertThat(token.getClaimAsString(TokenClaims.XSUAA.ZONE_ID)).isEqualTo(DEFAULT_ZONE_ID);
assertThat(token.getExpiration()).isEqualTo(JwtGenerator.NO_EXPIRE_DATE);
assertThat(((AbstractToken) token).isXsuaaToken()).isTrue();
}

@Test
public void createIasToken_isNotNull() {
public void createIasToken() {
cut = JwtGenerator.getInstance(IAS, "T000310")
.withClaimValue(SUBJECT, "P176945")
.withClaimValue(ISSUER, "https://application.myauth.com")
Expand All @@ -98,7 +98,7 @@ public void createIasToken_isNotNull() {

assertThat(token).isNotNull();
assertThat(token.getHeaderParameterAsString(TokenHeader.KEY_ID)).isEqualTo(DEFAULT_KEY_ID_IAS);
assertThat(token.getClaimAsString(SAP_GLOBAL_ZONE_ID)).isEqualTo(DEFAULT_ZONE_ID);
assertThat(token.getClaimAsString(SAP_GLOBAL_APP_TID)).isEqualTo(DEFAULT_APP_TID);
assertThat(token.getClaimAsString(AUDIENCE)).isEqualTo("T000310");
assertThat(token.getClaimAsString(AUTHORIZATION_PARTY)).isEqualTo("T000310");
assertThat(token.getClientId()).isEqualTo("T000310");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
import java.util.Objects;
import java.util.regex.Pattern;

import static com.sap.cloud.security.token.TokenClaims.*;
import static com.sap.cloud.security.token.TokenClaims.EXPIRATION;
import static com.sap.cloud.security.token.TokenClaims.NOT_BEFORE;
import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;

/**
Expand Down Expand Up @@ -168,11 +169,6 @@ public int hashCode() {
return Objects.hash(getTokenValue());
}

@Override
public String getZoneId() {
return getClaimAsString(SAP_GLOBAL_ZONE_ID);
}

@Override
public String toString() {
return decodedJwt.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import javax.annotation.Nullable;
import java.security.Principal;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

Expand Down Expand Up @@ -165,7 +164,12 @@ public String getSubaccountId() {

@Override
public String getZoneId() {
return Objects.nonNull(super.getZoneId()) ? super.getZoneId() : getClaimAsString(ZONE_ID);
return getAppTid();
}

@Override
public String getAppTid() {
return getClaimAsString(ZONE_ID);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
class JsonWebKeySet {

private final Set<JsonWebKey> jsonWebKeys = new HashSet<>();
private final Map<String, Boolean> zoneIdAccepted = new HashMap<>();
private final Map<String, Boolean> appTidAccepted = new HashMap<>();

@Nullable
public JsonWebKey getKeyByAlgorithmAndId(JwtSignatureAlgorithm keyAlgorithm, String keyId) {
Expand Down Expand Up @@ -45,16 +45,16 @@ private Stream<JsonWebKey> getTokenStreamWithTypeAndKeyId(JwtSignatureAlgorithm
.filter(jwk -> kid.equals(jwk.getId()));
}

public boolean containsZoneId(String zoneId) {
return zoneIdAccepted.containsKey(zoneId);
public boolean containsAppTid(String appTid) {
return appTidAccepted.containsKey(appTid);
}

public boolean isZoneIdAccepted(String zoneId) {
return zoneIdAccepted.get(zoneId);
public boolean isAppTidAccepted(String appTid) {
return appTidAccepted.get(appTid);
}

public JsonWebKeySet withZoneId(String zoneId, boolean isAccepted) {
zoneIdAccepted.put(zoneId, isAccepted);
public JsonWebKeySet withAppTid(String appTid, boolean isAccepted) {
appTidAccepted.put(appTid, isAccepted);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public ValidationResult validate(Token token) {
zoneIdForTokenKeys = token.getZoneId();
if (isTenantIdCheckEnabled && !token.getIssuer().equals("" + configuration.getUrl())
&& zoneIdForTokenKeys == null) {
return createInvalid("Error occurred during signature validation: OIDC token must provide zone_uuid.");
return createInvalid("Error occurred during signature validation: OIDC token must provide app_tid.");
}
}
try {
Expand Down Expand Up @@ -160,7 +160,7 @@ ValidationResult validate(String token, String tokenAlgorithm, String tokenKeyId
assertHasText(tokenKeysUrl, "tokenKeysUrl must not be null or empty.");

return Validation.getInstance().validate(tokenKeyService, token, tokenAlgorithm, tokenKeyId,
URI.create(tokenKeysUrl), fallbackPublicKey, zoneId);
URI.create(tokenKeysUrl), fallbackPublicKey, zoneId, configuration.getClientId());
}

private static class Validation {
Expand All @@ -177,14 +177,14 @@ static Validation getInstance() {

ValidationResult validate(OAuth2TokenKeyServiceWithCache tokenKeyService, String token,
String tokenAlgorithm, String tokenKeyId, URI tokenKeysUrl, @Nullable String fallbackPublicKey,
@Nullable String zoneId) {
@Nullable String zoneId, String clientId) {
ValidationResult validationResult;

validationResult = setSupportedJwtAlgorithm(tokenAlgorithm);
if (validationResult.isErroneous()) {
return validationResult;
}
validationResult = setPublicKey(tokenKeyService, tokenKeyId, tokenKeysUrl, zoneId);
validationResult = setPublicKey(tokenKeyService, tokenKeyId, tokenKeysUrl, zoneId, clientId);
if (validationResult.isErroneous()) {
if (fallbackPublicKey != null) {
try {
Expand Down Expand Up @@ -220,9 +220,9 @@ private ValidationResult setSupportedJwtAlgorithm(String tokenAlgorithm) {
}

private ValidationResult setPublicKey(OAuth2TokenKeyServiceWithCache tokenKeyService, String keyId,
URI keyUri, String zoneId) {
URI keyUri, String zoneId, String clientId) {
try {
this.publicKey = tokenKeyService.getPublicKey(jwtSignatureAlgorithm, keyId, keyUri, zoneId);
this.publicKey = tokenKeyService.getPublicKey(jwtSignatureAlgorithm, keyId, keyUri, zoneId, clientId);
} catch (OAuth2ServiceException e) {
return createInvalid("Error retrieving Json Web Keys from Identity Service: {}.", e.getMessage());
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ public OAuth2TokenKeyServiceWithCache withTokenKeyService(OAuth2TokenKeyService
* @param keyUri
* the Token Key Uri (jwks) of the Access Token (can be tenant
* specific).
* @param zoneId
* the Zone Id of the tenant
* @param appTid
* the tenant identifier of the tenant
* @return a PublicKey
* @throws OAuth2ServiceException
* in case the call to the jwks endpoint of the identity service
Expand All @@ -126,22 +126,57 @@ public OAuth2TokenKeyServiceWithCache withTokenKeyService(OAuth2TokenKeyService
*
*/
@Nullable
public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, URI keyUri, String zoneId)
public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, URI keyUri, String appTid)
throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException {
assertNotNull(keyAlgorithm, "keyAlgorithm must not be null.");
assertHasText(keyId, "keyId must not be null.");
assertNotNull(keyUri, "keyUrl must not be null.");

return getPublicKey(keyAlgorithm, keyId, keyUri, appTid, null);
}

/**
* Returns the cached key by id and type or requests the keys from the jwks URI
* of the identity service.
*
* @param keyAlgorithm
* the Key Algorithm of the Access Token.
* @param keyId
* the Key Id of the Access Token.
* @param keyUri
* the Token Key Uri (jwks) of the Access Token (can be tenant
* specific).
* @param appTid
* the tenant identifier of the tenant
*
* @param clientId
* client id from the service configuration
* @return a PublicKey
* @throws OAuth2ServiceException
* in case the call to the jwks endpoint of the identity service
* failed.
* @throws InvalidKeySpecException
* in case the PublicKey generation for the json web key failed.
* @throws NoSuchAlgorithmException
* in case the algorithm of the json web key is not supported.
*
*/
public PublicKey getPublicKey(JwtSignatureAlgorithm keyAlgorithm, String keyId, URI keyUri, String appTid, String clientId)
throws OAuth2ServiceException, InvalidKeySpecException, NoSuchAlgorithmException {
assertNotNull(keyAlgorithm, "keyAlgorithm must not be null.");
assertHasText(keyId, "keyId must not be null.");
assertNotNull(keyUri, "keyUrl must not be null.");

JsonWebKeySet keySet = getCache().getIfPresent(keyUri.toString());
if (keySet == null || !keySet.containsZoneId(zoneId)) {
keySet = retrieveTokenKeysAndUpdateCache(keyUri, zoneId, keySet); // creates and updates cache entries
if (keySet == null || !keySet.containsAppTid(appTid)) {
keySet = retrieveTokenKeysAndUpdateCache(keyUri, appTid, keySet, clientId); // creates and updates cache entries
}
if (keySet == null || keySet.getAll().isEmpty()) {
LOGGER.error("Retrieved no token keys from {}", keyUri);
return null;
}
if (!keySet.isZoneIdAccepted(zoneId)) {
throw new OAuth2ServiceException("Keys not accepted for zone_uuid " + zoneId);
if (!keySet.isAppTidAccepted(appTid)) {
throw new OAuth2ServiceException("Keys not accepted for app_tid " + appTid);
}
for (JsonWebKey jwk : keySet.getAll()) {
if (keyId.equals(jwk.getId()) && jwk.getKeyAlgorithm().equals(keyAlgorithm)) {
Expand Down Expand Up @@ -181,22 +216,22 @@ private TokenKeyCacheConfiguration getCheckedConfiguration(CacheConfiguration ca
return TokenKeyCacheConfiguration.getInstance(duration, size, cacheConfiguration.isCacheStatisticsEnabled());
}

private JsonWebKeySet retrieveTokenKeysAndUpdateCache(URI jwksUri, String zoneId,
@Nullable JsonWebKeySet keySetCached)
private JsonWebKeySet retrieveTokenKeysAndUpdateCache(URI jwksUri, String appTid,
@Nullable JsonWebKeySet keySetCached, String clientId)
throws OAuth2ServiceException {
String jwksJson;
try {
jwksJson = getTokenKeyService().retrieveTokenKeys(jwksUri, zoneId);
jwksJson = getTokenKeyService().retrieveTokenKeys(jwksUri, appTid, clientId);
} catch (OAuth2ServiceException e) {
if (keySetCached != null) {
keySetCached.withZoneId(zoneId, false);
keySetCached.withAppTid(appTid, false);
}
throw e;
}
if (keySetCached != null) {
return keySetCached.withZoneId(zoneId, true);
return keySetCached.withAppTid(appTid, true);
}
JsonWebKeySet keySet = JsonWebKeySetFactory.createFromJson(jwksJson).withZoneId(zoneId, true);
JsonWebKeySet keySet = JsonWebKeySetFactory.createFromJson(jwksJson).withAppTid(appTid, true);
getCache().put(jwksUri.toString(), keySet);
return keySet;
}
Expand Down
Loading

0 comments on commit 6e1a7b9

Please sign in to comment.