Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

app_tid adaption 3.x.x #1238

Merged
merged 24 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
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 @@ -78,10 +78,10 @@
String zoneIdForTokenKeys = null;

if (Service.IAS == configuration.getService()) {
zoneIdForTokenKeys = token.getZoneId();

Check notice

Code scanning / CodeQL

Deprecated method or constructor invocation Note

Invoking
Token.getZoneId
should be avoided because it has been deprecated.
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 @@
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 @@

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 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)) {
liga-oz marked this conversation as resolved.
Show resolved Hide resolved
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
Loading