From b26df796b9106ad562dbd85c5cc558f38332c277 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Wed, 11 Sep 2024 12:19:36 -0700 Subject: [PATCH 01/18] Added code to handle claims in authentication challenges. --- .../KeyVaultCredentialPolicy.java | 123 ++++++++++++----- .../KeyVaultCredentialPolicy.java | 119 ++++++++++++----- .../KeyVaultCredentialPolicy.java | 124 +++++++++++++----- .../KeyVaultCredentialPolicy.java | 119 ++++++++++++----- 4 files changed, 353 insertions(+), 132 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java index 79ca22b1d6913..ec538a5579c4d 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java @@ -4,11 +4,11 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; -import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; +import com.azure.core.util.Base64Util; import com.azure.core.util.BinaryData; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; @@ -27,6 +27,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import static com.azure.core.http.HttpHeaderName.CONTENT_LENGTH; +import static com.azure.core.http.HttpHeaderName.WWW_AUTHENTICATE; + /** * A policy that authenticates requests with the Azure Key Vault service. The content added by this policy is * leveraged in {@link TokenCredential} to get and set the correct "Authorization" header value. @@ -67,16 +70,17 @@ private static Map extractChallengeAttributes(String authenticat return Collections.emptyMap(); } - authenticateHeader = - authenticateHeader.toLowerCase(Locale.ROOT).replace(authChallengePrefix.toLowerCase(Locale.ROOT), ""); - - String[] attributes = authenticateHeader.split(", "); + String[] attributes = authenticateHeader.substring(authChallengePrefix.length()).split(", "); Map attributeMap = new HashMap<>(); for (String pair : attributes) { - String[] keyValue = pair.split("="); + if (pair.startsWith("claims=")) { + attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); + } else { + String[] keyValue = pair.split("="); - attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); + attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); + } } return attributeMap; @@ -102,31 +106,31 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // If this policy doesn't have challenge parameters cached try to get it from the static challenge cache. if (this.challenge == null) { - String authority = getRequestAuthority(request); - this.challenge = CHALLENGE_CACHE.get(authority); + this.challenge = CHALLENGE_CACHE.get(getRequestAuthority(request)); } if (this.challenge != null) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext); } - // The body is removed from the initial request because Key Vault supports other authentication schemes which - // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending - // an unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK but we - // still don't want to send any unprotected data to vaults which require it. + // The body is removed from the initial request because Key Vault supports other authentication schemes + // which also protect the body of the request. As a result, before we know the auth scheme we need to + // avoid sending an unprotected body to Key Vault. We don't currently support this enhanced auth scheme + // in the SDK, but we still don't want to send any unprotected data to vaults which require it. // Do not overwrite previous contents if retrying after initial request failed (e.g. timeout). if (!context.getData(KEY_VAULT_STASHED_CONTENT_KEY).isPresent()) { if (request.getBody() != null) { context.setData(KEY_VAULT_STASHED_CONTENT_KEY, request.getBody()); context.setData(KEY_VAULT_STASHED_CONTENT_LENGTH_KEY, - request.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH)); - request.setHeader(HttpHeaderName.CONTENT_LENGTH, "0"); + request.getHeaders().getValue(CONTENT_LENGTH)); + request.setHeader(CONTENT_LENGTH, "0"); request.setBody((Flux) null); } } @@ -145,13 +149,26 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (request.getBody() == null && contentOptional.isPresent() && contentLengthOptional.isPresent()) { request.setBody((Flux) contentOptional.get()); - request.setHeader(HttpHeaderName.CONTENT_LENGTH, (String) contentLengthOptional.get()); + request.setHeader(CONTENT_LENGTH, (String) contentLengthOptional.get()); } String authority = getRequestAuthority(request); Map challengeAttributes = - extractChallengeAttributes(response.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE), - BEARER_TOKEN_PREFIX); + extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + String claims = null; + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + String base64Claims = challengeAttributes.get("claims"); + + if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { + claims = new String(Base64Util.decodeString(base64Claims)); + } + } + String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -165,6 +182,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (this.challenge == null) { return Mono.just(false); + } else if (claims != null) { + CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -194,14 +213,15 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext) .then(Mono.just(true)); @@ -214,38 +234,38 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // If this policy doesn't have challenge parameters cached try to get it from the static challenge cache. if (this.challenge == null) { - String authority = getRequestAuthority(request); - this.challenge = CHALLENGE_CACHE.get(authority); + this.challenge = CHALLENGE_CACHE.get(getRequestAuthority(request)); } if (this.challenge != null) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); + return; } // The body is removed from the initial request because Key Vault supports other authentication schemes which - // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending - // an unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK but we - // still don't want to send any unprotected data to vaults which require it. + // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending an + // unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK, but we still + // don't want to send any unprotected data to vaults which require it. // Do not overwrite previous contents if retrying after initial request failed (e.g. timeout). if (!context.getData(KEY_VAULT_STASHED_CONTENT_KEY).isPresent()) { if (request.getBodyAsBinaryData() != null) { context.setData(KEY_VAULT_STASHED_CONTENT_KEY, request.getBodyAsBinaryData()); context.setData(KEY_VAULT_STASHED_CONTENT_LENGTH_KEY, - request.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH)); - request.setHeader(HttpHeaderName.CONTENT_LENGTH, "0"); + request.getHeaders().getValue(CONTENT_LENGTH)); + request.setHeader(CONTENT_LENGTH, "0"); request.setBody((BinaryData) null); } } } - @SuppressWarnings("unchecked") @Override public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, HttpResponse response) { HttpRequest request = context.getHttpRequest(); @@ -254,12 +274,26 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (request.getBody() == null && contentOptional.isPresent() && contentLengthOptional.isPresent()) { request.setBody((BinaryData) (contentOptional.get())); - request.setHeader(HttpHeaderName.CONTENT_LENGTH, (String) contentLengthOptional.get()); + request.setHeader(CONTENT_LENGTH, (String) contentLengthOptional.get()); } String authority = getRequestAuthority(request); Map challengeAttributes = - extractChallengeAttributes(response.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + String claims = null; + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + String base64Claims = challengeAttributes.get("claims"); + + if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { + claims = new String(Base64Util.decodeString(base64Claims)); + } + } + String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -273,6 +307,8 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (this.challenge == null) { return false; + } else if (claims != null) { + CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -302,16 +338,18 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); + return true; } @@ -319,6 +357,7 @@ private static class ChallengeParameters { private final URI authorizationUri; private final String tenantId; private final String[] scopes; + private String claims; ChallengeParameters(URI authorizationUri, String[] scopes) { this.authorizationUri = authorizationUri; @@ -347,6 +386,22 @@ public String[] getScopes() { public String getTenantId() { return tenantId; } + + /** + * Get the {@code claims} parameter from the challenge response. + */ + public String getClaims() { + return claims; + } + + /** + * Set the {@code claims} parameter from the challenge response. + */ + public ChallengeParameters setClaims(String claims) { + this.claims = claims; + + return this; + } } public static void clearCache() { diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java index 6a2920aeddcfa..42df065aa2e48 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java @@ -8,6 +8,7 @@ import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; +import com.azure.core.util.Base64Util; import com.azure.core.util.BinaryData; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; @@ -26,6 +27,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import static com.azure.core.http.HttpHeaderName.CONTENT_LENGTH; +import static com.azure.core.http.HttpHeaderName.WWW_AUTHENTICATE; + /** * A policy that authenticates requests with the Azure Key Vault service. The content added by this policy is * leveraged in {@link TokenCredential} to get and set the correct "Authorization" header value. @@ -35,10 +39,8 @@ public class KeyVaultCredentialPolicy extends BearerTokenAuthenticationPolicy { private static final ClientLogger LOGGER = new ClientLogger(KeyVaultCredentialPolicy.class); private static final String BEARER_TOKEN_PREFIX = "Bearer "; - private static final String CONTENT_LENGTH_HEADER = "Content-Length"; private static final String KEY_VAULT_STASHED_CONTENT_KEY = "KeyVaultCredentialPolicyStashedBody"; private static final String KEY_VAULT_STASHED_CONTENT_LENGTH_KEY = "KeyVaultCredentialPolicyStashedContentLength"; - private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; private static final ConcurrentMap CHALLENGE_CACHE = new ConcurrentHashMap<>(); private ChallengeParameters challenge; private final boolean disableChallengeResourceVerification; @@ -68,16 +70,17 @@ private static Map extractChallengeAttributes(String authenticat return Collections.emptyMap(); } - authenticateHeader = - authenticateHeader.toLowerCase(Locale.ROOT).replace(authChallengePrefix.toLowerCase(Locale.ROOT), ""); - - String[] attributes = authenticateHeader.split(", "); + String[] attributes = authenticateHeader.substring(authChallengePrefix.length()).split(", "); Map attributeMap = new HashMap<>(); for (String pair : attributes) { - String[] keyValue = pair.split("="); + if (pair.startsWith("claims=")) { + attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); + } else { + String[] keyValue = pair.split("="); - attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); + attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); + } } return attributeMap; @@ -103,31 +106,31 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // If this policy doesn't have challenge parameters cached try to get it from the static challenge cache. if (this.challenge == null) { - String authority = getRequestAuthority(request); - this.challenge = CHALLENGE_CACHE.get(authority); + this.challenge = CHALLENGE_CACHE.get(getRequestAuthority(request)); } if (this.challenge != null) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext); } - // The body is removed from the initial request because Key Vault supports other authentication schemes which - // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending - // an unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK but we - // still don't want to send any unprotected data to vaults which require it. + // The body is removed from the initial request because Key Vault supports other authentication schemes + // which also protect the body of the request. As a result, before we know the auth scheme we need to + // avoid sending an unprotected body to Key Vault. We don't currently support this enhanced auth scheme + // in the SDK, but we still don't want to send any unprotected data to vaults which require it. // Do not overwrite previous contents if retrying after initial request failed (e.g. timeout). if (!context.getData(KEY_VAULT_STASHED_CONTENT_KEY).isPresent()) { if (request.getBody() != null) { context.setData(KEY_VAULT_STASHED_CONTENT_KEY, request.getBody()); context.setData(KEY_VAULT_STASHED_CONTENT_LENGTH_KEY, - request.getHeaders().getValue(CONTENT_LENGTH_HEADER)); - request.setHeader(CONTENT_LENGTH_HEADER, "0"); + request.getHeaders().getValue(CONTENT_LENGTH)); + request.setHeader(CONTENT_LENGTH, "0"); request.setBody((Flux) null); } } @@ -146,12 +149,26 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (request.getBody() == null && contentOptional.isPresent() && contentLengthOptional.isPresent()) { request.setBody((Flux) contentOptional.get()); - request.setHeader(CONTENT_LENGTH_HEADER, (String) contentLengthOptional.get()); + request.setHeader(CONTENT_LENGTH, (String) contentLengthOptional.get()); } String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + String claims = null; + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + String base64Claims = challengeAttributes.get("claims"); + + if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { + claims = new String(Base64Util.decodeString(base64Claims)); + } + } + String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -165,6 +182,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (this.challenge == null) { return Mono.just(false); + } else if (claims != null) { + CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -194,14 +213,15 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext) .then(Mono.just(true)); @@ -214,38 +234,38 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // If this policy doesn't have challenge parameters cached try to get it from the static challenge cache. if (this.challenge == null) { - String authority = getRequestAuthority(request); - this.challenge = CHALLENGE_CACHE.get(authority); + this.challenge = CHALLENGE_CACHE.get(getRequestAuthority(request)); } if (this.challenge != null) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); + return; } // The body is removed from the initial request because Key Vault supports other authentication schemes which - // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending - // an unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK but we - // still don't want to send any unprotected data to vaults which require it. + // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending an + // unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK, but we still + // don't want to send any unprotected data to vaults which require it. // Do not overwrite previous contents if retrying after initial request failed (e.g. timeout). if (!context.getData(KEY_VAULT_STASHED_CONTENT_KEY).isPresent()) { if (request.getBodyAsBinaryData() != null) { context.setData(KEY_VAULT_STASHED_CONTENT_KEY, request.getBodyAsBinaryData()); context.setData(KEY_VAULT_STASHED_CONTENT_LENGTH_KEY, - request.getHeaders().getValue(CONTENT_LENGTH_HEADER)); - request.setHeader(CONTENT_LENGTH_HEADER, "0"); + request.getHeaders().getValue(CONTENT_LENGTH)); + request.setHeader(CONTENT_LENGTH, "0"); request.setBody((BinaryData) null); } } } - @SuppressWarnings("unchecked") @Override public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, HttpResponse response) { HttpRequest request = context.getHttpRequest(); @@ -254,12 +274,26 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (request.getBody() == null && contentOptional.isPresent() && contentLengthOptional.isPresent()) { request.setBody((BinaryData) (contentOptional.get())); - request.setHeader(CONTENT_LENGTH_HEADER, (String) contentLengthOptional.get()); + request.setHeader(CONTENT_LENGTH, (String) contentLengthOptional.get()); } String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + String claims = null; + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + String base64Claims = challengeAttributes.get("claims"); + + if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { + claims = new String(Base64Util.decodeString(base64Claims)); + } + } + String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -273,6 +307,8 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (this.challenge == null) { return false; + } else if (claims != null) { + CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -302,16 +338,18 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); + return true; } @@ -319,6 +357,7 @@ private static class ChallengeParameters { private final URI authorizationUri; private final String tenantId; private final String[] scopes; + private String claims; ChallengeParameters(URI authorizationUri, String[] scopes) { this.authorizationUri = authorizationUri; @@ -347,6 +386,22 @@ public String[] getScopes() { public String getTenantId() { return tenantId; } + + /** + * Get the {@code claims} parameter from the challenge response. + */ + public String getClaims() { + return claims; + } + + /** + * Set the {@code claims} parameter from the challenge response. + */ + public ChallengeParameters setClaims(String claims) { + this.claims = claims; + + return this; + } } public static void clearCache() { diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java index 8bfc86fafd5e3..c6f3ed1cfb6da 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java @@ -4,11 +4,11 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; -import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; +import com.azure.core.util.Base64Util; import com.azure.core.util.BinaryData; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; @@ -27,6 +27,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import static com.azure.core.http.HttpHeaderName.CONTENT_LENGTH; +import static com.azure.core.http.HttpHeaderName.WWW_AUTHENTICATE; + /** * A policy that authenticates requests with the Azure Key Vault service. The content added by this policy is * leveraged in {@link TokenCredential} to get and set the correct "Authorization" header value. @@ -67,16 +70,17 @@ private static Map extractChallengeAttributes(String authenticat return Collections.emptyMap(); } - authenticateHeader = - authenticateHeader.toLowerCase(Locale.ROOT).replace(authChallengePrefix.toLowerCase(Locale.ROOT), ""); - - String[] attributes = authenticateHeader.split(", "); + String[] attributes = authenticateHeader.substring(authChallengePrefix.length()).split(", "); Map attributeMap = new HashMap<>(); for (String pair : attributes) { - String[] keyValue = pair.split("="); + if (pair.startsWith("claims=")) { + attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); + } else { + String[] keyValue = pair.split("="); - attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); + attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); + } } return attributeMap; @@ -102,31 +106,31 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // If this policy doesn't have challenge parameters cached try to get it from the static challenge cache. if (this.challenge == null) { - String authority = getRequestAuthority(request); - this.challenge = CHALLENGE_CACHE.get(authority); + this.challenge = CHALLENGE_CACHE.get(getRequestAuthority(request)); } if (this.challenge != null) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext); } - // The body is removed from the initial request because Key Vault supports other authentication schemes which - // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending - // an unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK but we - // still don't want to send any unprotected data to vaults which require it. + // The body is removed from the initial request because Key Vault supports other authentication schemes + // which also protect the body of the request. As a result, before we know the auth scheme we need to + // avoid sending an unprotected body to Key Vault. We don't currently support this enhanced auth scheme + // in the SDK, but we still don't want to send any unprotected data to vaults which require it. // Do not overwrite previous contents if retrying after initial request failed (e.g. timeout). if (!context.getData(KEY_VAULT_STASHED_CONTENT_KEY).isPresent()) { if (request.getBody() != null) { context.setData(KEY_VAULT_STASHED_CONTENT_KEY, request.getBody()); context.setData(KEY_VAULT_STASHED_CONTENT_LENGTH_KEY, - request.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH)); - request.setHeader(HttpHeaderName.CONTENT_LENGTH, "0"); + request.getHeaders().getValue(CONTENT_LENGTH)); + request.setHeader(CONTENT_LENGTH, "0"); request.setBody((Flux) null); } } @@ -145,12 +149,26 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (request.getBody() == null && contentOptional.isPresent() && contentLengthOptional.isPresent()) { request.setBody((Flux) contentOptional.get()); - request.setHeader(HttpHeaderName.CONTENT_LENGTH, (String) contentLengthOptional.get()); + request.setHeader(CONTENT_LENGTH, (String) contentLengthOptional.get()); } String authority = getRequestAuthority(request); - Map challengeAttributes = extractChallengeAttributes( - response.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + Map challengeAttributes = + extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + String claims = null; + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + String base64Claims = challengeAttributes.get("claims"); + + if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { + claims = new String(Base64Util.decodeString(base64Claims)); + } + } + String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -164,6 +182,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (this.challenge == null) { return Mono.just(false); + } else if (claims != null) { + CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -193,14 +213,15 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext) .then(Mono.just(true)); @@ -213,38 +234,38 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // If this policy doesn't have challenge parameters cached try to get it from the static challenge cache. if (this.challenge == null) { - String authority = getRequestAuthority(request); - this.challenge = CHALLENGE_CACHE.get(authority); + this.challenge = CHALLENGE_CACHE.get(getRequestAuthority(request)); } if (this.challenge != null) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); + return; } // The body is removed from the initial request because Key Vault supports other authentication schemes which - // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending - // an unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK but we - // still don't want to send any unprotected data to vaults which require it. + // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending an + // unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK, but we still + // don't want to send any unprotected data to vaults which require it. // Do not overwrite previous contents if retrying after initial request failed (e.g. timeout). if (!context.getData(KEY_VAULT_STASHED_CONTENT_KEY).isPresent()) { if (request.getBodyAsBinaryData() != null) { context.setData(KEY_VAULT_STASHED_CONTENT_KEY, request.getBodyAsBinaryData()); context.setData(KEY_VAULT_STASHED_CONTENT_LENGTH_KEY, - request.getHeaders().getValue(HttpHeaderName.CONTENT_LENGTH)); - request.setHeader(HttpHeaderName.CONTENT_LENGTH, "0"); + request.getHeaders().getValue(CONTENT_LENGTH)); + request.setHeader(CONTENT_LENGTH, "0"); request.setBody((BinaryData) null); } } } - @SuppressWarnings("unchecked") @Override public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, HttpResponse response) { HttpRequest request = context.getHttpRequest(); @@ -253,12 +274,26 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (request.getBody() == null && contentOptional.isPresent() && contentLengthOptional.isPresent()) { request.setBody((BinaryData) (contentOptional.get())); - request.setHeader(HttpHeaderName.CONTENT_LENGTH, (String) contentLengthOptional.get()); + request.setHeader(CONTENT_LENGTH, (String) contentLengthOptional.get()); } String authority = getRequestAuthority(request); Map challengeAttributes = - extractChallengeAttributes(response.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + String claims = null; + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + String base64Claims = challengeAttributes.get("claims"); + + if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { + claims = new String(Base64Util.decodeString(base64Claims)); + } + } + String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -272,6 +307,8 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (this.challenge == null) { return false; + } else if (claims != null) { + CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -301,16 +338,18 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); + return true; } @@ -318,6 +357,7 @@ private static class ChallengeParameters { private final URI authorizationUri; private final String tenantId; private final String[] scopes; + private String claims; ChallengeParameters(URI authorizationUri, String[] scopes) { this.authorizationUri = authorizationUri; @@ -346,6 +386,22 @@ public String[] getScopes() { public String getTenantId() { return tenantId; } + + /** + * Get the {@code claims} parameter from the challenge response. + */ + public String getClaims() { + return claims; + } + + /** + * Set the {@code claims} parameter from the challenge response. + */ + public ChallengeParameters setClaims(String claims) { + this.claims = claims; + + return this; + } } public static void clearCache() { diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index 536a09fc114da..abf0089092f74 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -8,6 +8,7 @@ import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; +import com.azure.core.util.Base64Util; import com.azure.core.util.BinaryData; import com.azure.core.util.CoreUtils; import com.azure.core.util.logging.ClientLogger; @@ -26,6 +27,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import static com.azure.core.http.HttpHeaderName.CONTENT_LENGTH; +import static com.azure.core.http.HttpHeaderName.WWW_AUTHENTICATE; + /** * A policy that authenticates requests with the Azure Key Vault service. The content added by this policy is * leveraged in {@link TokenCredential} to get and set the correct "Authorization" header value. @@ -35,10 +39,8 @@ public class KeyVaultCredentialPolicy extends BearerTokenAuthenticationPolicy { private static final ClientLogger LOGGER = new ClientLogger(KeyVaultCredentialPolicy.class); private static final String BEARER_TOKEN_PREFIX = "Bearer "; - private static final String CONTENT_LENGTH_HEADER = "Content-Length"; private static final String KEY_VAULT_STASHED_CONTENT_KEY = "KeyVaultCredentialPolicyStashedBody"; private static final String KEY_VAULT_STASHED_CONTENT_LENGTH_KEY = "KeyVaultCredentialPolicyStashedContentLength"; - private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; private static final ConcurrentMap CHALLENGE_CACHE = new ConcurrentHashMap<>(); private ChallengeParameters challenge; private final boolean disableChallengeResourceVerification; @@ -68,16 +70,17 @@ private static Map extractChallengeAttributes(String authenticat return Collections.emptyMap(); } - authenticateHeader = - authenticateHeader.toLowerCase(Locale.ROOT).replace(authChallengePrefix.toLowerCase(Locale.ROOT), ""); - - String[] attributes = authenticateHeader.split(", "); + String[] attributes = authenticateHeader.substring(authChallengePrefix.length()).split(", "); Map attributeMap = new HashMap<>(); for (String pair : attributes) { - String[] keyValue = pair.split("="); + if (pair.startsWith("claims=")) { + attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); + } else { + String[] keyValue = pair.split("="); - attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); + attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); + } } return attributeMap; @@ -103,31 +106,31 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // If this policy doesn't have challenge parameters cached try to get it from the static challenge cache. if (this.challenge == null) { - String authority = getRequestAuthority(request); - this.challenge = CHALLENGE_CACHE.get(authority); + this.challenge = CHALLENGE_CACHE.get(getRequestAuthority(request)); } if (this.challenge != null) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext); } - // The body is removed from the initial request because Key Vault supports other authentication schemes which - // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending - // an unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK but we - // still don't want to send any unprotected data to vaults which require it. + // The body is removed from the initial request because Key Vault supports other authentication schemes + // which also protect the body of the request. As a result, before we know the auth scheme we need to + // avoid sending an unprotected body to Key Vault. We don't currently support this enhanced auth scheme + // in the SDK, but we still don't want to send any unprotected data to vaults which require it. // Do not overwrite previous contents if retrying after initial request failed (e.g. timeout). if (!context.getData(KEY_VAULT_STASHED_CONTENT_KEY).isPresent()) { if (request.getBody() != null) { context.setData(KEY_VAULT_STASHED_CONTENT_KEY, request.getBody()); context.setData(KEY_VAULT_STASHED_CONTENT_LENGTH_KEY, - request.getHeaders().getValue(CONTENT_LENGTH_HEADER)); - request.setHeader(CONTENT_LENGTH_HEADER, "0"); + request.getHeaders().getValue(CONTENT_LENGTH)); + request.setHeader(CONTENT_LENGTH, "0"); request.setBody((Flux) null); } } @@ -146,12 +149,26 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (request.getBody() == null && contentOptional.isPresent() && contentLengthOptional.isPresent()) { request.setBody((Flux) contentOptional.get()); - request.setHeader(CONTENT_LENGTH_HEADER, (String) contentLengthOptional.get()); + request.setHeader(CONTENT_LENGTH, (String) contentLengthOptional.get()); } String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + String claims = null; + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + String base64Claims = challengeAttributes.get("claims"); + + if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { + claims = new String(Base64Util.decodeString(base64Claims)); + } + } + String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -165,6 +182,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (this.challenge == null) { return Mono.just(false); + } else if (claims != null) { + CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -194,14 +213,15 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext) .then(Mono.just(true)); @@ -214,38 +234,38 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // If this policy doesn't have challenge parameters cached try to get it from the static challenge cache. if (this.challenge == null) { - String authority = getRequestAuthority(request); - this.challenge = CHALLENGE_CACHE.get(authority); + this.challenge = CHALLENGE_CACHE.get(getRequestAuthority(request)); } if (this.challenge != null) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); + return; } // The body is removed from the initial request because Key Vault supports other authentication schemes which - // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending - // an unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK but we - // still don't want to send any unprotected data to vaults which require it. + // also protect the body of the request. As a result, before we know the auth scheme we need to avoid sending an + // unprotected body to Key Vault. We don't currently support this enhanced auth scheme in the SDK, but we still + // don't want to send any unprotected data to vaults which require it. // Do not overwrite previous contents if retrying after initial request failed (e.g. timeout). if (!context.getData(KEY_VAULT_STASHED_CONTENT_KEY).isPresent()) { if (request.getBodyAsBinaryData() != null) { context.setData(KEY_VAULT_STASHED_CONTENT_KEY, request.getBodyAsBinaryData()); context.setData(KEY_VAULT_STASHED_CONTENT_LENGTH_KEY, - request.getHeaders().getValue(CONTENT_LENGTH_HEADER)); - request.setHeader(CONTENT_LENGTH_HEADER, "0"); + request.getHeaders().getValue(CONTENT_LENGTH)); + request.setHeader(CONTENT_LENGTH, "0"); request.setBody((BinaryData) null); } } } - @SuppressWarnings("unchecked") @Override public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, HttpResponse response) { HttpRequest request = context.getHttpRequest(); @@ -254,12 +274,26 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (request.getBody() == null && contentOptional.isPresent() && contentLengthOptional.isPresent()) { request.setBody((BinaryData) (contentOptional.get())); - request.setHeader(CONTENT_LENGTH_HEADER, (String) contentLengthOptional.get()); + request.setHeader(CONTENT_LENGTH, (String) contentLengthOptional.get()); } String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + String claims = null; + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + String base64Claims = challengeAttributes.get("claims"); + + if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { + claims = new String(Base64Util.decodeString(base64Claims)); + } + } + String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -273,6 +307,8 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (this.challenge == null) { return false; + } else if (claims != null) { + CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -302,16 +338,18 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); + return true; } @@ -319,6 +357,7 @@ private static class ChallengeParameters { private final URI authorizationUri; private final String tenantId; private final String[] scopes; + private String claims; ChallengeParameters(URI authorizationUri, String[] scopes) { this.authorizationUri = authorizationUri; @@ -347,6 +386,22 @@ public String[] getScopes() { public String getTenantId() { return tenantId; } + + /** + * Get the {@code claims} parameter from the challenge response. + */ + public String getClaims() { + return claims; + } + + /** + * Set the {@code claims} parameter from the challenge response. + */ + public ChallengeParameters setClaims(String claims) { + this.claims = claims; + + return this; + } } public static void clearCache() { From c50077ea080b140e08bb16b6daddcece28b52ea1 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Tue, 17 Sep 2024 13:43:37 -0700 Subject: [PATCH 02/18] Updated KeyVaultCredentialPolicy to ensure CAE is enabled. --- .../implementation/KeyVaultCredentialPolicy.java | 4 ++++ .../certificates/implementation/KeyVaultCredentialPolicy.java | 4 ++++ .../keys/implementation/KeyVaultCredentialPolicy.java | 4 ++++ .../secrets/implementation/KeyVaultCredentialPolicy.java | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java index ec538a5579c4d..abdfd4c62e74e 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java @@ -114,6 +114,7 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext); @@ -221,6 +222,7 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext) @@ -242,6 +244,7 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -346,6 +349,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java index 42df065aa2e48..f0100b3926cec 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java @@ -114,6 +114,7 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext); @@ -221,6 +222,7 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext) @@ -242,6 +244,7 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -346,6 +349,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java index c6f3ed1cfb6da..9aa29726abd49 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java @@ -114,6 +114,7 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext); @@ -221,6 +222,7 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext) @@ -242,6 +244,7 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -346,6 +349,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index abf0089092f74..a8bf2b7a570aa 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -114,6 +114,7 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext); @@ -221,6 +222,7 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); return setAuthorizationHeader(context, tokenRequestContext) @@ -242,6 +244,7 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -346,6 +349,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true) .setClaims(this.challenge.getClaims()); setAuthorizationHeaderSync(context, tokenRequestContext); From 90cbd2e65302c4dfd98ddaa4b783fd6c107be539 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Wed, 18 Sep 2024 23:23:07 -0700 Subject: [PATCH 03/18] Updated KeyVaultCredentialPolicy to handle a second 401 in case a token is revoked immediately after acquiring it for the first time. --- .../KeyVaultCredentialPolicy.java | 146 +++++++++++++----- .../KeyVaultCredentialPolicy.java | 146 +++++++++++++----- .../KeyVaultCredentialPolicy.java | 146 +++++++++++++----- .../KeyVaultCredentialPolicy.java | 146 +++++++++++++----- 4 files changed, 444 insertions(+), 140 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java index abdfd4c62e74e..69fe1622ec5fc 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java @@ -4,7 +4,10 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; +import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; +import com.azure.core.http.HttpPipelineNextPolicy; +import com.azure.core.http.HttpPipelineNextSyncPolicy; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; @@ -74,6 +77,9 @@ private static Map extractChallengeAttributes(String authenticat Map attributeMap = new HashMap<>(); for (String pair : attributes) { + // This is ugly, but we need to trim here because currently the 'claims' attribute comes after two spaces. + pair = pair.trim(); + if (pair.startsWith("claims=")) { attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); } else { @@ -113,9 +119,7 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); return setAuthorizationHeader(context, tokenRequestContext); } @@ -183,8 +187,6 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (this.challenge == null) { return Mono.just(false); - } else if (claims != null) { - CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -214,16 +216,20 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(claims); + } return setAuthorizationHeader(context, tokenRequestContext) .then(Mono.just(true)); @@ -243,9 +249,7 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -310,8 +314,6 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (this.challenge == null) { return false; - } else if (claims != null) { - CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -341,27 +343,117 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(claims); + } setAuthorizationHeaderSync(context, tokenRequestContext); return true; } + @Override + public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { + if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { + return Mono.error(new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); + } + + HttpPipelineNextPolicy nextPolicy = next.clone(); + + return authorizeRequest(context).then(Mono.defer(next::process)).flatMap(httpResponse -> { + String authHeader = httpResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (httpResponse.getStatusCode() == 401 && authHeader != null) { + return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { + if (authorized) { + // The body needs to be closed or read to the end to release the connection. + httpResponse.close(); + + return nextPolicy.process(); + } else { + return Mono.just(httpResponse); + } + }); + } + + return Mono.just(httpResponse); + }); + } + + @Override + public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNextSyncPolicy next) { + if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { + throw LOGGER.logExceptionAsError( + new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); + } + + HttpPipelineNextSyncPolicy firstClone = next.clone(); + + authorizeRequestSync(context); + + HttpResponse firstResponse = next.processSync(); + String firstAuthHeader = firstResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (firstResponse.getStatusCode() == 401 && firstAuthHeader != null) { + if (authorizeRequestOnChallengeSync(context, firstResponse)) { + // The body needs to be closed or read to the end to release the connection. + firstResponse.close(); + + HttpPipelineNextSyncPolicy secondClone = firstClone.clone(); + HttpResponse secondResponse = firstClone.processSync(); + String secondAuthHeader = secondResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (secondResponse.getStatusCode() == 401 + && secondAuthHeader != null + // We should only retry if the first challenge does not have an 'insufficient claims' error. + && !firstAuthHeader.contains("insufficient_claims")) { + + if (authorizeRequestOnChallengeSync(context, secondResponse)) { + // The body needs to be closed or read to the end to release the connection. + secondResponse.close(); + + return secondClone.processSync(); + } else { + return secondResponse; + } + } else { + return secondResponse; + } + } else { + return firstResponse; + } + } + + return firstResponse; + } + + private HttpResponse handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextSyncPolicy nextPolicy) { + if (authorizeRequestOnChallengeSync(context, httpResponse)) { + // The body needs to be closed or read to the end to release the connection. + httpResponse.close(); + + return nextPolicy.processSync(); + } else { + return httpResponse; + } + } + private static class ChallengeParameters { private final URI authorizationUri; private final String tenantId; private final String[] scopes; - private String claims; ChallengeParameters(URI authorizationUri, String[] scopes) { this.authorizationUri = authorizationUri; @@ -390,22 +482,6 @@ public String[] getScopes() { public String getTenantId() { return tenantId; } - - /** - * Get the {@code claims} parameter from the challenge response. - */ - public String getClaims() { - return claims; - } - - /** - * Set the {@code claims} parameter from the challenge response. - */ - public ChallengeParameters setClaims(String claims) { - this.claims = claims; - - return this; - } } public static void clearCache() { diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java index f0100b3926cec..7413d86cc36a0 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java @@ -4,7 +4,10 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; +import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; +import com.azure.core.http.HttpPipelineNextPolicy; +import com.azure.core.http.HttpPipelineNextSyncPolicy; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; @@ -74,6 +77,9 @@ private static Map extractChallengeAttributes(String authenticat Map attributeMap = new HashMap<>(); for (String pair : attributes) { + // This is ugly, but we need to trim here because currently the 'claims' attribute comes after two spaces. + pair = pair.trim(); + if (pair.startsWith("claims=")) { attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); } else { @@ -113,9 +119,7 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); return setAuthorizationHeader(context, tokenRequestContext); } @@ -183,8 +187,6 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (this.challenge == null) { return Mono.just(false); - } else if (claims != null) { - CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -214,16 +216,20 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(claims); + } return setAuthorizationHeader(context, tokenRequestContext) .then(Mono.just(true)); @@ -243,9 +249,7 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -310,8 +314,6 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (this.challenge == null) { return false; - } else if (claims != null) { - CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -341,27 +343,117 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(claims); + } setAuthorizationHeaderSync(context, tokenRequestContext); return true; } + @Override + public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { + if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { + return Mono.error(new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); + } + + HttpPipelineNextPolicy nextPolicy = next.clone(); + + return authorizeRequest(context).then(Mono.defer(next::process)).flatMap(httpResponse -> { + String authHeader = httpResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (httpResponse.getStatusCode() == 401 && authHeader != null) { + return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { + if (authorized) { + // The body needs to be closed or read to the end to release the connection. + httpResponse.close(); + + return nextPolicy.process(); + } else { + return Mono.just(httpResponse); + } + }); + } + + return Mono.just(httpResponse); + }); + } + + @Override + public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNextSyncPolicy next) { + if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { + throw LOGGER.logExceptionAsError( + new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); + } + + HttpPipelineNextSyncPolicy firstClone = next.clone(); + + authorizeRequestSync(context); + + HttpResponse firstResponse = next.processSync(); + String firstAuthHeader = firstResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (firstResponse.getStatusCode() == 401 && firstAuthHeader != null) { + if (authorizeRequestOnChallengeSync(context, firstResponse)) { + // The body needs to be closed or read to the end to release the connection. + firstResponse.close(); + + HttpPipelineNextSyncPolicy secondClone = firstClone.clone(); + HttpResponse secondResponse = firstClone.processSync(); + String secondAuthHeader = secondResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (secondResponse.getStatusCode() == 401 + && secondAuthHeader != null + // We should only retry if the first challenge does not have an 'insufficient claims' error. + && !firstAuthHeader.contains("insufficient_claims")) { + + if (authorizeRequestOnChallengeSync(context, secondResponse)) { + // The body needs to be closed or read to the end to release the connection. + secondResponse.close(); + + return secondClone.processSync(); + } else { + return secondResponse; + } + } else { + return secondResponse; + } + } else { + return firstResponse; + } + } + + return firstResponse; + } + + private HttpResponse handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextSyncPolicy nextPolicy) { + if (authorizeRequestOnChallengeSync(context, httpResponse)) { + // The body needs to be closed or read to the end to release the connection. + httpResponse.close(); + + return nextPolicy.processSync(); + } else { + return httpResponse; + } + } + private static class ChallengeParameters { private final URI authorizationUri; private final String tenantId; private final String[] scopes; - private String claims; ChallengeParameters(URI authorizationUri, String[] scopes) { this.authorizationUri = authorizationUri; @@ -390,22 +482,6 @@ public String[] getScopes() { public String getTenantId() { return tenantId; } - - /** - * Get the {@code claims} parameter from the challenge response. - */ - public String getClaims() { - return claims; - } - - /** - * Set the {@code claims} parameter from the challenge response. - */ - public ChallengeParameters setClaims(String claims) { - this.claims = claims; - - return this; - } } public static void clearCache() { diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java index 9aa29726abd49..b851cd99196a5 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java @@ -4,7 +4,10 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; +import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; +import com.azure.core.http.HttpPipelineNextPolicy; +import com.azure.core.http.HttpPipelineNextSyncPolicy; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; @@ -74,6 +77,9 @@ private static Map extractChallengeAttributes(String authenticat Map attributeMap = new HashMap<>(); for (String pair : attributes) { + // This is ugly, but we need to trim here because currently the 'claims' attribute comes after two spaces. + pair = pair.trim(); + if (pair.startsWith("claims=")) { attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); } else { @@ -113,9 +119,7 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); return setAuthorizationHeader(context, tokenRequestContext); } @@ -183,8 +187,6 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (this.challenge == null) { return Mono.just(false); - } else if (claims != null) { - CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -214,16 +216,20 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(claims); + } return setAuthorizationHeader(context, tokenRequestContext) .then(Mono.just(true)); @@ -243,9 +249,7 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -310,8 +314,6 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (this.challenge == null) { return false; - } else if (claims != null) { - CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -341,27 +343,117 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(claims); + } setAuthorizationHeaderSync(context, tokenRequestContext); return true; } + @Override + public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { + if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { + return Mono.error(new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); + } + + HttpPipelineNextPolicy nextPolicy = next.clone(); + + return authorizeRequest(context).then(Mono.defer(next::process)).flatMap(httpResponse -> { + String authHeader = httpResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (httpResponse.getStatusCode() == 401 && authHeader != null) { + return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { + if (authorized) { + // The body needs to be closed or read to the end to release the connection. + httpResponse.close(); + + return nextPolicy.process(); + } else { + return Mono.just(httpResponse); + } + }); + } + + return Mono.just(httpResponse); + }); + } + + @Override + public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNextSyncPolicy next) { + if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { + throw LOGGER.logExceptionAsError( + new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); + } + + HttpPipelineNextSyncPolicy firstClone = next.clone(); + + authorizeRequestSync(context); + + HttpResponse firstResponse = next.processSync(); + String firstAuthHeader = firstResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (firstResponse.getStatusCode() == 401 && firstAuthHeader != null) { + if (authorizeRequestOnChallengeSync(context, firstResponse)) { + // The body needs to be closed or read to the end to release the connection. + firstResponse.close(); + + HttpPipelineNextSyncPolicy secondClone = firstClone.clone(); + HttpResponse secondResponse = firstClone.processSync(); + String secondAuthHeader = secondResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (secondResponse.getStatusCode() == 401 + && secondAuthHeader != null + // We should only retry if the first challenge does not have an 'insufficient claims' error. + && !firstAuthHeader.contains("insufficient_claims")) { + + if (authorizeRequestOnChallengeSync(context, secondResponse)) { + // The body needs to be closed or read to the end to release the connection. + secondResponse.close(); + + return secondClone.processSync(); + } else { + return secondResponse; + } + } else { + return secondResponse; + } + } else { + return firstResponse; + } + } + + return firstResponse; + } + + private HttpResponse handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextSyncPolicy nextPolicy) { + if (authorizeRequestOnChallengeSync(context, httpResponse)) { + // The body needs to be closed or read to the end to release the connection. + httpResponse.close(); + + return nextPolicy.processSync(); + } else { + return httpResponse; + } + } + private static class ChallengeParameters { private final URI authorizationUri; private final String tenantId; private final String[] scopes; - private String claims; ChallengeParameters(URI authorizationUri, String[] scopes) { this.authorizationUri = authorizationUri; @@ -390,22 +482,6 @@ public String[] getScopes() { public String getTenantId() { return tenantId; } - - /** - * Get the {@code claims} parameter from the challenge response. - */ - public String getClaims() { - return claims; - } - - /** - * Set the {@code claims} parameter from the challenge response. - */ - public ChallengeParameters setClaims(String claims) { - this.claims = claims; - - return this; - } } public static void clearCache() { diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index a8bf2b7a570aa..8dfb58cfae01e 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -4,7 +4,10 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; +import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; +import com.azure.core.http.HttpPipelineNextPolicy; +import com.azure.core.http.HttpPipelineNextSyncPolicy; import com.azure.core.http.HttpRequest; import com.azure.core.http.HttpResponse; import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; @@ -74,6 +77,9 @@ private static Map extractChallengeAttributes(String authenticat Map attributeMap = new HashMap<>(); for (String pair : attributes) { + // This is ugly, but we need to trim here because currently the 'claims' attribute comes after two spaces. + pair = pair.trim(); + if (pair.startsWith("claims=")) { attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); } else { @@ -113,9 +119,7 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); return setAuthorizationHeader(context, tokenRequestContext); } @@ -183,8 +187,6 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context if (this.challenge == null) { return Mono.just(false); - } else if (claims != null) { - CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -214,16 +216,20 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(claims); + } return setAuthorizationHeader(context, tokenRequestContext) .then(Mono.just(true)); @@ -243,9 +249,7 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -310,8 +314,6 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, if (this.challenge == null) { return false; - } else if (claims != null) { - CHALLENGE_CACHE.put(authority, this.challenge.setClaims(claims)); } } else { if (!disableChallengeResourceVerification) { @@ -341,27 +343,117 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String.format("The challenge authorization URI '%s' is invalid.", authorization), e)); } - this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}).setClaims(claims); + this.challenge = new ChallengeParameters(authorizationUri, new String[] {scope}); CHALLENGE_CACHE.put(authority, this.challenge); } TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()) - .setCaeEnabled(true) - .setClaims(this.challenge.getClaims()); + .setTenantId(this.challenge.getTenantId()); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(claims); + } setAuthorizationHeaderSync(context, tokenRequestContext); return true; } + @Override + public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { + if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { + return Mono.error(new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); + } + + HttpPipelineNextPolicy nextPolicy = next.clone(); + + return authorizeRequest(context).then(Mono.defer(next::process)).flatMap(httpResponse -> { + String authHeader = httpResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (httpResponse.getStatusCode() == 401 && authHeader != null) { + return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { + if (authorized) { + // The body needs to be closed or read to the end to release the connection. + httpResponse.close(); + + return nextPolicy.process(); + } else { + return Mono.just(httpResponse); + } + }); + } + + return Mono.just(httpResponse); + }); + } + + @Override + public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNextSyncPolicy next) { + if (!"https".equals(context.getHttpRequest().getUrl().getProtocol())) { + throw LOGGER.logExceptionAsError( + new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); + } + + HttpPipelineNextSyncPolicy firstClone = next.clone(); + + authorizeRequestSync(context); + + HttpResponse firstResponse = next.processSync(); + String firstAuthHeader = firstResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (firstResponse.getStatusCode() == 401 && firstAuthHeader != null) { + if (authorizeRequestOnChallengeSync(context, firstResponse)) { + // The body needs to be closed or read to the end to release the connection. + firstResponse.close(); + + HttpPipelineNextSyncPolicy secondClone = firstClone.clone(); + HttpResponse secondResponse = firstClone.processSync(); + String secondAuthHeader = secondResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + + if (secondResponse.getStatusCode() == 401 + && secondAuthHeader != null + // We should only retry if the first challenge does not have an 'insufficient claims' error. + && !firstAuthHeader.contains("insufficient_claims")) { + + if (authorizeRequestOnChallengeSync(context, secondResponse)) { + // The body needs to be closed or read to the end to release the connection. + secondResponse.close(); + + return secondClone.processSync(); + } else { + return secondResponse; + } + } else { + return secondResponse; + } + } else { + return firstResponse; + } + } + + return firstResponse; + } + + private HttpResponse handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextSyncPolicy nextPolicy) { + if (authorizeRequestOnChallengeSync(context, httpResponse)) { + // The body needs to be closed or read to the end to release the connection. + httpResponse.close(); + + return nextPolicy.processSync(); + } else { + return httpResponse; + } + } + private static class ChallengeParameters { private final URI authorizationUri; private final String tenantId; private final String[] scopes; - private String claims; ChallengeParameters(URI authorizationUri, String[] scopes) { this.authorizationUri = authorizationUri; @@ -390,22 +482,6 @@ public String[] getScopes() { public String getTenantId() { return tenantId; } - - /** - * Get the {@code claims} parameter from the challenge response. - */ - public String getClaims() { - return claims; - } - - /** - * Set the {@code claims} parameter from the challenge response. - */ - public ChallengeParameters setClaims(String claims) { - this.claims = claims; - - return this; - } } public static void clearCache() { From 103d113716823d77e3931e14afa25dd457df75f7 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Wed, 25 Sep 2024 18:56:54 -0700 Subject: [PATCH 04/18] Fixed an issue where `KeyVaultCredentialPolicy` would make an extra call. Added async functionality for CAE. --- .../KeyVaultCredentialPolicy.java | 163 +++++++++--------- .../KeyVaultCredentialPolicy.java | 163 +++++++++--------- .../KeyVaultCredentialPolicy.java | 163 +++++++++--------- .../KeyVaultCredentialPolicy.java | 163 +++++++++--------- 4 files changed, 340 insertions(+), 312 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java index 69fe1622ec5fc..69e44beb9226d 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java @@ -4,7 +4,6 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; -import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpPipelineNextSyncPolicy; @@ -160,20 +159,6 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); - - String error = challengeAttributes.get("error"); - String claims = null; - - if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); - - String base64Claims = challengeAttributes.get("claims"); - - if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { - claims = new String(Base64Util.decodeString(base64Claims)); - } - } - String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -225,10 +210,20 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()); - if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(claims); + String error = challengeAttributes.get("error"); + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + if ("insufficient_claims".equalsIgnoreCase(error)) { + String claims = challengeAttributes.get("claims"); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(new String(Base64Util.decodeString(claims))); + } + } } return setAuthorizationHeader(context, tokenRequestContext) @@ -287,20 +282,6 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); - - String error = challengeAttributes.get("error"); - String claims = null; - - if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); - - String base64Claims = challengeAttributes.get("claims"); - - if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { - claims = new String(Base64Util.decodeString(base64Claims)); - } - } - String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -352,10 +333,20 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()); - if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(claims); + String error = challengeAttributes.get("error"); + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + if ("insufficient_claims".equalsIgnoreCase(error)) { + String claims = challengeAttributes.get("claims"); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(new String(Base64Util.decodeString(claims))); + } + } } setAuthorizationHeaderSync(context, tokenRequestContext); @@ -372,19 +363,10 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN HttpPipelineNextPolicy nextPolicy = next.clone(); return authorizeRequest(context).then(Mono.defer(next::process)).flatMap(httpResponse -> { - String authHeader = httpResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + String authHeader = httpResponse.getHeaderValue(WWW_AUTHENTICATE); if (httpResponse.getStatusCode() == 401 && authHeader != null) { - return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { - if (authorized) { - // The body needs to be closed or read to the end to release the connection. - httpResponse.close(); - - return nextPolicy.process(); - } else { - return Mono.just(httpResponse); - } - }); + return handleChallenge(context, httpResponse, nextPolicy); } return Mono.just(httpResponse); @@ -398,56 +380,81 @@ public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNex new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); } - HttpPipelineNextSyncPolicy firstClone = next.clone(); + HttpPipelineNextSyncPolicy nextPolicy = next.clone(); authorizeRequestSync(context); - HttpResponse firstResponse = next.processSync(); - String firstAuthHeader = firstResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + HttpResponse httpResponse = next.processSync(); + String authHeader = httpResponse.getHeaderValue(WWW_AUTHENTICATE); + + if (httpResponse.getStatusCode() == 401 && authHeader != null) { + return handleChallengeSync(context, httpResponse, nextPolicy); + } - if (firstResponse.getStatusCode() == 401 && firstAuthHeader != null) { - if (authorizeRequestOnChallengeSync(context, firstResponse)) { + return httpResponse; + } + + private Mono handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextPolicy next) { + return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { + if (authorized) { // The body needs to be closed or read to the end to release the connection. - firstResponse.close(); + httpResponse.close(); - HttpPipelineNextSyncPolicy secondClone = firstClone.clone(); - HttpResponse secondResponse = firstClone.processSync(); - String secondAuthHeader = secondResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + HttpPipelineNextPolicy nextPolicy = next.clone(); - if (secondResponse.getStatusCode() == 401 - && secondAuthHeader != null - // We should only retry if the first challenge does not have an 'insufficient claims' error. - && !firstAuthHeader.contains("insufficient_claims")) { + return next.process().flatMap(newResponse -> { + String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (authorizeRequestOnChallengeSync(context, secondResponse)) { - // The body needs to be closed or read to the end to release the connection. - secondResponse.close(); + if (newResponse.getStatusCode() == 401 && authHeader != null + && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { - return secondClone.processSync(); + return handleChallenge(context, newResponse, nextPolicy); } else { - return secondResponse; + return Mono.just(newResponse); } - } else { - return secondResponse; - } - } else { - return firstResponse; + }); } - } - return firstResponse; + return Mono.just(httpResponse); + }); } - private HttpResponse handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, - HttpPipelineNextSyncPolicy nextPolicy) { + private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextSyncPolicy next) { if (authorizeRequestOnChallengeSync(context, httpResponse)) { // The body needs to be closed or read to the end to release the connection. httpResponse.close(); - return nextPolicy.processSync(); - } else { - return httpResponse; + HttpPipelineNextSyncPolicy nextPolicy = next.clone(); + HttpResponse newResponse = next.processSync(); + String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); + + if (newResponse.getStatusCode() == 401 && authHeader != null + && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + + return handleChallengeSync(context, newResponse, nextPolicy); + } + + return newResponse; } + + return httpResponse; + } + + private boolean isClaimsPresent(HttpResponse httpResponse) { + Map challengeAttributes = + extractChallengeAttributes(httpResponse.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + + if (error != null) { + String base64Claims = challengeAttributes.get("claims"); + + return "insufficient_claims".equalsIgnoreCase(error) && base64Claims != null; + } + + return false; } private static class ChallengeParameters { diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java index 7413d86cc36a0..74b10cae4dfed 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java @@ -4,7 +4,6 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; -import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpPipelineNextSyncPolicy; @@ -160,20 +159,6 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); - - String error = challengeAttributes.get("error"); - String claims = null; - - if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); - - String base64Claims = challengeAttributes.get("claims"); - - if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { - claims = new String(Base64Util.decodeString(base64Claims)); - } - } - String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -225,10 +210,20 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()); - if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(claims); + String error = challengeAttributes.get("error"); + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + if ("insufficient_claims".equalsIgnoreCase(error)) { + String claims = challengeAttributes.get("claims"); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(new String(Base64Util.decodeString(claims))); + } + } } return setAuthorizationHeader(context, tokenRequestContext) @@ -287,20 +282,6 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); - - String error = challengeAttributes.get("error"); - String claims = null; - - if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); - - String base64Claims = challengeAttributes.get("claims"); - - if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { - claims = new String(Base64Util.decodeString(base64Claims)); - } - } - String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -352,10 +333,20 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()); - if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(claims); + String error = challengeAttributes.get("error"); + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + if ("insufficient_claims".equalsIgnoreCase(error)) { + String claims = challengeAttributes.get("claims"); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(new String(Base64Util.decodeString(claims))); + } + } } setAuthorizationHeaderSync(context, tokenRequestContext); @@ -372,19 +363,10 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN HttpPipelineNextPolicy nextPolicy = next.clone(); return authorizeRequest(context).then(Mono.defer(next::process)).flatMap(httpResponse -> { - String authHeader = httpResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + String authHeader = httpResponse.getHeaderValue(WWW_AUTHENTICATE); if (httpResponse.getStatusCode() == 401 && authHeader != null) { - return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { - if (authorized) { - // The body needs to be closed or read to the end to release the connection. - httpResponse.close(); - - return nextPolicy.process(); - } else { - return Mono.just(httpResponse); - } - }); + return handleChallenge(context, httpResponse, nextPolicy); } return Mono.just(httpResponse); @@ -398,56 +380,81 @@ public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNex new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); } - HttpPipelineNextSyncPolicy firstClone = next.clone(); + HttpPipelineNextSyncPolicy nextPolicy = next.clone(); authorizeRequestSync(context); - HttpResponse firstResponse = next.processSync(); - String firstAuthHeader = firstResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + HttpResponse httpResponse = next.processSync(); + String authHeader = httpResponse.getHeaderValue(WWW_AUTHENTICATE); + + if (httpResponse.getStatusCode() == 401 && authHeader != null) { + return handleChallengeSync(context, httpResponse, nextPolicy); + } - if (firstResponse.getStatusCode() == 401 && firstAuthHeader != null) { - if (authorizeRequestOnChallengeSync(context, firstResponse)) { + return httpResponse; + } + + private Mono handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextPolicy next) { + return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { + if (authorized) { // The body needs to be closed or read to the end to release the connection. - firstResponse.close(); + httpResponse.close(); - HttpPipelineNextSyncPolicy secondClone = firstClone.clone(); - HttpResponse secondResponse = firstClone.processSync(); - String secondAuthHeader = secondResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + HttpPipelineNextPolicy nextPolicy = next.clone(); - if (secondResponse.getStatusCode() == 401 - && secondAuthHeader != null - // We should only retry if the first challenge does not have an 'insufficient claims' error. - && !firstAuthHeader.contains("insufficient_claims")) { + return next.process().flatMap(newResponse -> { + String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (authorizeRequestOnChallengeSync(context, secondResponse)) { - // The body needs to be closed or read to the end to release the connection. - secondResponse.close(); + if (newResponse.getStatusCode() == 401 && authHeader != null + && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { - return secondClone.processSync(); + return handleChallenge(context, newResponse, nextPolicy); } else { - return secondResponse; + return Mono.just(newResponse); } - } else { - return secondResponse; - } - } else { - return firstResponse; + }); } - } - return firstResponse; + return Mono.just(httpResponse); + }); } - private HttpResponse handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, - HttpPipelineNextSyncPolicy nextPolicy) { + private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextSyncPolicy next) { if (authorizeRequestOnChallengeSync(context, httpResponse)) { // The body needs to be closed or read to the end to release the connection. httpResponse.close(); - return nextPolicy.processSync(); - } else { - return httpResponse; + HttpPipelineNextSyncPolicy nextPolicy = next.clone(); + HttpResponse newResponse = next.processSync(); + String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); + + if (newResponse.getStatusCode() == 401 && authHeader != null + && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + + return handleChallengeSync(context, newResponse, nextPolicy); + } + + return newResponse; } + + return httpResponse; + } + + private boolean isClaimsPresent(HttpResponse httpResponse) { + Map challengeAttributes = + extractChallengeAttributes(httpResponse.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + + if (error != null) { + String base64Claims = challengeAttributes.get("claims"); + + return "insufficient_claims".equalsIgnoreCase(error) && base64Claims != null; + } + + return false; } private static class ChallengeParameters { diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java index b851cd99196a5..76952b9e6cdb0 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java @@ -4,7 +4,6 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; -import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpPipelineNextSyncPolicy; @@ -160,20 +159,6 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); - - String error = challengeAttributes.get("error"); - String claims = null; - - if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); - - String base64Claims = challengeAttributes.get("claims"); - - if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { - claims = new String(Base64Util.decodeString(base64Claims)); - } - } - String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -225,10 +210,20 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()); - if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(claims); + String error = challengeAttributes.get("error"); + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + if ("insufficient_claims".equalsIgnoreCase(error)) { + String claims = challengeAttributes.get("claims"); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(new String(Base64Util.decodeString(claims))); + } + } } return setAuthorizationHeader(context, tokenRequestContext) @@ -287,20 +282,6 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); - - String error = challengeAttributes.get("error"); - String claims = null; - - if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); - - String base64Claims = challengeAttributes.get("claims"); - - if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { - claims = new String(Base64Util.decodeString(base64Claims)); - } - } - String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -352,10 +333,20 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()); - if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(claims); + String error = challengeAttributes.get("error"); + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + if ("insufficient_claims".equalsIgnoreCase(error)) { + String claims = challengeAttributes.get("claims"); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(new String(Base64Util.decodeString(claims))); + } + } } setAuthorizationHeaderSync(context, tokenRequestContext); @@ -372,19 +363,10 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN HttpPipelineNextPolicy nextPolicy = next.clone(); return authorizeRequest(context).then(Mono.defer(next::process)).flatMap(httpResponse -> { - String authHeader = httpResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + String authHeader = httpResponse.getHeaderValue(WWW_AUTHENTICATE); if (httpResponse.getStatusCode() == 401 && authHeader != null) { - return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { - if (authorized) { - // The body needs to be closed or read to the end to release the connection. - httpResponse.close(); - - return nextPolicy.process(); - } else { - return Mono.just(httpResponse); - } - }); + return handleChallenge(context, httpResponse, nextPolicy); } return Mono.just(httpResponse); @@ -398,56 +380,81 @@ public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNex new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); } - HttpPipelineNextSyncPolicy firstClone = next.clone(); + HttpPipelineNextSyncPolicy nextPolicy = next.clone(); authorizeRequestSync(context); - HttpResponse firstResponse = next.processSync(); - String firstAuthHeader = firstResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + HttpResponse httpResponse = next.processSync(); + String authHeader = httpResponse.getHeaderValue(WWW_AUTHENTICATE); + + if (httpResponse.getStatusCode() == 401 && authHeader != null) { + return handleChallengeSync(context, httpResponse, nextPolicy); + } - if (firstResponse.getStatusCode() == 401 && firstAuthHeader != null) { - if (authorizeRequestOnChallengeSync(context, firstResponse)) { + return httpResponse; + } + + private Mono handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextPolicy next) { + return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { + if (authorized) { // The body needs to be closed or read to the end to release the connection. - firstResponse.close(); + httpResponse.close(); - HttpPipelineNextSyncPolicy secondClone = firstClone.clone(); - HttpResponse secondResponse = firstClone.processSync(); - String secondAuthHeader = secondResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + HttpPipelineNextPolicy nextPolicy = next.clone(); - if (secondResponse.getStatusCode() == 401 - && secondAuthHeader != null - // We should only retry if the first challenge does not have an 'insufficient claims' error. - && !firstAuthHeader.contains("insufficient_claims")) { + return next.process().flatMap(newResponse -> { + String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (authorizeRequestOnChallengeSync(context, secondResponse)) { - // The body needs to be closed or read to the end to release the connection. - secondResponse.close(); + if (newResponse.getStatusCode() == 401 && authHeader != null + && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { - return secondClone.processSync(); + return handleChallenge(context, newResponse, nextPolicy); } else { - return secondResponse; + return Mono.just(newResponse); } - } else { - return secondResponse; - } - } else { - return firstResponse; + }); } - } - return firstResponse; + return Mono.just(httpResponse); + }); } - private HttpResponse handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, - HttpPipelineNextSyncPolicy nextPolicy) { + private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextSyncPolicy next) { if (authorizeRequestOnChallengeSync(context, httpResponse)) { // The body needs to be closed or read to the end to release the connection. httpResponse.close(); - return nextPolicy.processSync(); - } else { - return httpResponse; + HttpPipelineNextSyncPolicy nextPolicy = next.clone(); + HttpResponse newResponse = next.processSync(); + String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); + + if (newResponse.getStatusCode() == 401 && authHeader != null + && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + + return handleChallengeSync(context, newResponse, nextPolicy); + } + + return newResponse; } + + return httpResponse; + } + + private boolean isClaimsPresent(HttpResponse httpResponse) { + Map challengeAttributes = + extractChallengeAttributes(httpResponse.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + + if (error != null) { + String base64Claims = challengeAttributes.get("claims"); + + return "insufficient_claims".equalsIgnoreCase(error) && base64Claims != null; + } + + return false; } private static class ChallengeParameters { diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index 8dfb58cfae01e..e8c738806551e 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -4,7 +4,6 @@ import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; -import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpPipelineCallContext; import com.azure.core.http.HttpPipelineNextPolicy; import com.azure.core.http.HttpPipelineNextSyncPolicy; @@ -160,20 +159,6 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); - - String error = challengeAttributes.get("error"); - String claims = null; - - if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); - - String base64Claims = challengeAttributes.get("claims"); - - if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { - claims = new String(Base64Util.decodeString(base64Claims)); - } - } - String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -225,10 +210,20 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()); - if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(claims); + String error = challengeAttributes.get("error"); + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + if ("insufficient_claims".equalsIgnoreCase(error)) { + String claims = challengeAttributes.get("claims"); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(new String(Base64Util.decodeString(claims))); + } + } } return setAuthorizationHeader(context, tokenRequestContext) @@ -287,20 +282,6 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String authority = getRequestAuthority(request); Map challengeAttributes = extractChallengeAttributes(response.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); - - String error = challengeAttributes.get("error"); - String claims = null; - - if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); - - String base64Claims = challengeAttributes.get("claims"); - - if (error.equalsIgnoreCase("insufficient_claims") && base64Claims != null) { - claims = new String(Base64Util.decodeString(base64Claims)); - } - } - String scope = challengeAttributes.get("resource"); if (scope != null) { @@ -352,10 +333,20 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, .addScopes(this.challenge.getScopes()) .setTenantId(this.challenge.getTenantId()); - if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(claims); + String error = challengeAttributes.get("error"); + + if (error != null) { + LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + + if ("insufficient_claims".equalsIgnoreCase(error)) { + String claims = challengeAttributes.get("claims"); + + if (claims != null) { + tokenRequestContext + .setCaeEnabled(true) + .setClaims(new String(Base64Util.decodeString(claims))); + } + } } setAuthorizationHeaderSync(context, tokenRequestContext); @@ -372,19 +363,10 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN HttpPipelineNextPolicy nextPolicy = next.clone(); return authorizeRequest(context).then(Mono.defer(next::process)).flatMap(httpResponse -> { - String authHeader = httpResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + String authHeader = httpResponse.getHeaderValue(WWW_AUTHENTICATE); if (httpResponse.getStatusCode() == 401 && authHeader != null) { - return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { - if (authorized) { - // The body needs to be closed or read to the end to release the connection. - httpResponse.close(); - - return nextPolicy.process(); - } else { - return Mono.just(httpResponse); - } - }); + return handleChallenge(context, httpResponse, nextPolicy); } return Mono.just(httpResponse); @@ -398,56 +380,81 @@ public HttpResponse processSync(HttpPipelineCallContext context, HttpPipelineNex new RuntimeException("Token credentials require a URL using the HTTPS protocol scheme.")); } - HttpPipelineNextSyncPolicy firstClone = next.clone(); + HttpPipelineNextSyncPolicy nextPolicy = next.clone(); authorizeRequestSync(context); - HttpResponse firstResponse = next.processSync(); - String firstAuthHeader = firstResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + HttpResponse httpResponse = next.processSync(); + String authHeader = httpResponse.getHeaderValue(WWW_AUTHENTICATE); + + if (httpResponse.getStatusCode() == 401 && authHeader != null) { + return handleChallengeSync(context, httpResponse, nextPolicy); + } - if (firstResponse.getStatusCode() == 401 && firstAuthHeader != null) { - if (authorizeRequestOnChallengeSync(context, firstResponse)) { + return httpResponse; + } + + private Mono handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextPolicy next) { + return authorizeRequestOnChallenge(context, httpResponse).flatMap(authorized -> { + if (authorized) { // The body needs to be closed or read to the end to release the connection. - firstResponse.close(); + httpResponse.close(); - HttpPipelineNextSyncPolicy secondClone = firstClone.clone(); - HttpResponse secondResponse = firstClone.processSync(); - String secondAuthHeader = secondResponse.getHeaderValue(HttpHeaderName.WWW_AUTHENTICATE); + HttpPipelineNextPolicy nextPolicy = next.clone(); - if (secondResponse.getStatusCode() == 401 - && secondAuthHeader != null - // We should only retry if the first challenge does not have an 'insufficient claims' error. - && !firstAuthHeader.contains("insufficient_claims")) { + return next.process().flatMap(newResponse -> { + String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (authorizeRequestOnChallengeSync(context, secondResponse)) { - // The body needs to be closed or read to the end to release the connection. - secondResponse.close(); + if (newResponse.getStatusCode() == 401 && authHeader != null + && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { - return secondClone.processSync(); + return handleChallenge(context, newResponse, nextPolicy); } else { - return secondResponse; + return Mono.just(newResponse); } - } else { - return secondResponse; - } - } else { - return firstResponse; + }); } - } - return firstResponse; + return Mono.just(httpResponse); + }); } - private HttpResponse handleChallenge(HttpPipelineCallContext context, HttpResponse httpResponse, - HttpPipelineNextSyncPolicy nextPolicy) { + private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpResponse httpResponse, + HttpPipelineNextSyncPolicy next) { if (authorizeRequestOnChallengeSync(context, httpResponse)) { // The body needs to be closed or read to the end to release the connection. httpResponse.close(); - return nextPolicy.processSync(); - } else { - return httpResponse; + HttpPipelineNextSyncPolicy nextPolicy = next.clone(); + HttpResponse newResponse = next.processSync(); + String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); + + if (newResponse.getStatusCode() == 401 && authHeader != null + && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + + return handleChallengeSync(context, newResponse, nextPolicy); + } + + return newResponse; } + + return httpResponse; + } + + private boolean isClaimsPresent(HttpResponse httpResponse) { + Map challengeAttributes = + extractChallengeAttributes(httpResponse.getHeaderValue(WWW_AUTHENTICATE), BEARER_TOKEN_PREFIX); + + String error = challengeAttributes.get("error"); + + if (error != null) { + String base64Claims = challengeAttributes.get("claims"); + + return "insufficient_claims".equalsIgnoreCase(error) && base64Claims != null; + } + + return false; } private static class ChallengeParameters { From b478be27e8ea41d7bc16509034ad66235e50af92 Mon Sep 17 00:00:00 2001 From: Victor Colin Amador Date: Wed, 25 Sep 2024 19:15:30 -0700 Subject: [PATCH 05/18] Added tests. --- .../KeyVaultCredentialPolicyTest.java | 152 ++++++++++-------- .../KeyVaultCredentialPolicyTest.java | 76 ++++++--- .../keys/KeyVaultCredentialPolicyTest.java | 77 ++++++--- .../secrets/KeyVaultCredentialPolicyTest.java | 76 ++++++--- 4 files changed, 259 insertions(+), 122 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java index 0e6eeb3876a78..5ba7d4e2bc5f3 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java @@ -16,6 +16,7 @@ import com.azure.core.test.annotation.SyncAsyncTest; import com.azure.core.test.http.MockHttpResponse; import com.azure.core.util.BinaryData; +import com.azure.core.util.Context; import com.azure.security.keyvault.administration.implementation.KeyVaultCredentialPolicy; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -29,7 +30,6 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -43,81 +43,72 @@ public class KeyVaultCredentialPolicyTest { private static final String AUTHENTICATE_HEADER = "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd022db57\", " + "resource=\"https://vault.azure.net\""; + + private static final String AUTHENTICATE_HEADER_WITH_CLAIMS = + "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", " + + "error=\"insufficient_claims\", " + + "claims=\"eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlIjoiY3AxIn19fQ==\""; private static final String BEARER = "Bearer"; private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> - Flux.fromStream( - Stream.of(BODY.split("")) - .map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))) - )); + Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); private BasicAuthenticationCredential credential; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; + private HttpResponse unauthorizedHttpResponseWithoutHeaderAndClaims; private HttpPipelineCallContext callContext; private HttpPipelineCallContext differentScopeContext; private HttpPipelineCallContext testContext; private HttpPipelineCallContext bodyContext; private HttpPipelineCallContext bodyFluxContext; + private static HttpPipelineCallContext createCallContext(HttpRequest request, Context context) { + AtomicReference callContextReference = new AtomicReference<>(); + + HttpPipeline callContextCreator = new HttpPipelineBuilder() + .policies((callContext, next) -> { + callContextReference.set(callContext); + return next.process(); + }) + .httpClient(ignored -> Mono.empty()) + .build(); + + callContextCreator.send(request, context).block(); + + return callContextReference.get(); + } + @BeforeEach public void setup() { HttpRequest request = new HttpRequest(HttpMethod.GET, "https://kvtest.vault.azure.net"); HttpRequest requestWithDifferentScope = new HttpRequest(HttpMethod.GET, "https://mytest.azurecr.io"); - HttpPipelineCallContext plainContext = createMockContext(request, null); + Context bodyContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BinaryData.fromString(BODY)) + .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); - HttpPipelineCallContext differentScopeContext = createMockContext(requestWithDifferentScope, null); + Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) + .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); - HttpPipelineCallContext testContext = createMockContext(request, null); - - HttpPipelineCallContext bodyContext = createMockContext(request, callContext -> { - callContext.setData("KeyVaultCredentialPolicyStashedBody", BinaryData.fromString(BODY)); - callContext.setData("KeyVaultCredentialPolicyStashedContentLength", "21"); - }); - - HttpPipelineCallContext bodyFluxContext = createMockContext(request, callContext -> { - callContext.setData("KeyVaultCredentialPolicyStashedBody", BODY_FLUX); - callContext.setData("KeyVaultCredentialPolicyStashedContentLength", "21"); - }); + MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); MockHttpResponse unauthorizedResponseWithHeader = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://kvtest.vault.azure.net"), - 500, - new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER) - ); + new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER)); - MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://kvtest.vault.azure.net"), 500); + MockHttpResponse unauthorizedResponseWithHeaderAndClaims = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; - this.callContext = plainContext; - this.differentScopeContext = differentScopeContext; + this.unauthorizedHttpResponseWithoutHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; + this.callContext = createCallContext(request, Context.NONE); + this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); - this.testContext = testContext; - this.bodyContext = bodyContext; - this.bodyFluxContext = bodyFluxContext; - - } - - private static HttpPipelineCallContext createMockContext(HttpRequest request, - Consumer contextSetter) { - AtomicReference capturedContext = new AtomicReference<>(); - HttpPipeline httpPipeline = new HttpPipelineBuilder() - .policies((httpPipelineCallContext, httpPipelineNextPolicy) -> { - if (contextSetter != null) { - contextSetter.accept(httpPipelineCallContext); - } - - capturedContext.set(httpPipelineCallContext); - return Mono.empty(); - }) - .httpClient(ignored -> Mono.empty()) - .build(); - - httpPipeline.send(request).block(); - - return capturedContext.get(); + this.testContext = createCallContext(request, Context.NONE); + this.bodyContext = createCallContext(request, bodyContextContext); + this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); } @AfterEach @@ -146,10 +137,53 @@ public void onChallengeCredentialPolicy() { public void onAuthorizeRequestChallengeCachePresent() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + .verifyComplete(); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + // Challenge cache created - onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader).block(); + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); // Challenge cache used - policy.authorizeRequest(this.testContext).block(); + policy.authorizeRequestSync(this.testContext); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentWithClaims() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + .then(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader)) // Challenge with claims received + .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + .verifyComplete(); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + // Challenge cache created + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + // Challenge with claims received + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithoutHeaderAndClaims); + // Challenge cache used alongside claims + policy.authorizeRequestSync(this.testContext); String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); assertFalse(tokenValue.isEmpty()); @@ -232,20 +266,6 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { assertTrue(onChallenge); } - @Test - public void onAuthorizeRequestChallengeCachePresentSync() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - - // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); - // Challenge cache used - policy.authorizeRequestSync(this.testContext); - - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); - } - private Mono onChallenge(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java index 41a547a17179d..a6956b222b1e4 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java @@ -44,6 +44,10 @@ public class KeyVaultCredentialPolicyTest { "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd022db57\", " + "resource=\"https://vault.azure.net\""; + private static final String AUTHENTICATE_HEADER_WITH_CLAIMS = + "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", " + + "error=\"insufficient_claims\", " + + "claims=\"eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlIjoiY3AxIn19fQ==\""; private static final String BEARER = "Bearer"; private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> @@ -51,6 +55,7 @@ public class KeyVaultCredentialPolicyTest { private BasicAuthenticationCredential credential; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; + private HttpResponse unauthorizedHttpResponseWithoutHeaderAndClaims; private HttpPipelineCallContext callContext; private HttpPipelineCallContext differentScopeContext; private HttpPipelineCallContext testContext; @@ -84,22 +89,26 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); + MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithHeader = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER)); - MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithHeaderAndClaims = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; + this.unauthorizedHttpResponseWithoutHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); - } @AfterEach @@ -128,10 +137,53 @@ public void onChallengeCredentialPolicy() { public void onAuthorizeRequestChallengeCachePresent() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + .verifyComplete(); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + // Challenge cache created - onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader).block(); + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); // Challenge cache used - policy.authorizeRequest(this.testContext).block(); + policy.authorizeRequestSync(this.testContext); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentWithClaims() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + .then(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader)) // Challenge with claims received + .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + .verifyComplete(); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + // Challenge cache created + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + // Challenge with claims received + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithoutHeaderAndClaims); + // Challenge cache used alongside claims + policy.authorizeRequestSync(this.testContext); String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); assertFalse(tokenValue.isEmpty()); @@ -214,20 +266,6 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { assertTrue(onChallenge); } - @Test - public void onAuthorizeRequestChallengeCachePresentSync() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - - // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); - // Challenge cache used - policy.authorizeRequestSync(this.testContext); - - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); - } - private Mono onChallenge(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java index f63e9eb841ed1..ae7fbf4586727 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java @@ -43,6 +43,11 @@ public class KeyVaultCredentialPolicyTest { private static final String AUTHENTICATE_HEADER = "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd022db57\", " + "resource=\"https://vault.azure.net\""; + + private static final String AUTHENTICATE_HEADER_WITH_CLAIMS = + "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", " + + "error=\"insufficient_claims\", " + + "claims=\"eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlIjoiY3AxIn19fQ==\""; private static final String BEARER = "Bearer"; private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> @@ -50,6 +55,7 @@ public class KeyVaultCredentialPolicyTest { private BasicAuthenticationCredential credential; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; + private HttpResponse unauthorizedHttpResponseWithoutHeaderAndClaims; private HttpPipelineCallContext callContext; private HttpPipelineCallContext differentScopeContext; private HttpPipelineCallContext testContext; @@ -83,22 +89,26 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); + MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithHeader = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER)); - MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithHeaderAndClaims = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; + this.unauthorizedHttpResponseWithoutHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); - } @AfterEach @@ -127,10 +137,53 @@ public void onChallengeCredentialPolicy() { public void onAuthorizeRequestChallengeCachePresent() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + .verifyComplete(); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + // Challenge cache created - onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader).block(); + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); // Challenge cache used - policy.authorizeRequest(this.testContext).block(); + policy.authorizeRequestSync(this.testContext); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentWithClaims() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + .then(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader)) // Challenge with claims received + .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + .verifyComplete(); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + // Challenge cache created + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + // Challenge with claims received + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithoutHeaderAndClaims); + // Challenge cache used alongside claims + policy.authorizeRequestSync(this.testContext); String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); assertFalse(tokenValue.isEmpty()); @@ -213,20 +266,6 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { assertTrue(onChallenge); } - @Test - public void onAuthorizeRequestChallengeCachePresentSync() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - - // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); - // Challenge cache used - policy.authorizeRequestSync(this.testContext); - - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); - } - private Mono onChallenge(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java index b70ced39fe147..501062b4d8c63 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java @@ -43,6 +43,11 @@ public class KeyVaultCredentialPolicyTest { private static final String AUTHENTICATE_HEADER = "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd022db57\", " + "resource=\"https://vault.azure.net\""; + + private static final String AUTHENTICATE_HEADER_WITH_CLAIMS = + "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", " + + "error=\"insufficient_claims\", " + + "claims=\"eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlIjoiY3AxIn19fQ==\""; private static final String BEARER = "Bearer"; private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> @@ -50,6 +55,7 @@ public class KeyVaultCredentialPolicyTest { private BasicAuthenticationCredential credential; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; + private HttpResponse unauthorizedHttpResponseWithoutHeaderAndClaims; private HttpPipelineCallContext callContext; private HttpPipelineCallContext differentScopeContext; private HttpPipelineCallContext testContext; @@ -83,15 +89,20 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); + MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithHeader = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER)); - MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithHeaderAndClaims = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; + this.unauthorizedHttpResponseWithoutHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); @@ -126,10 +137,53 @@ public void onChallengeCredentialPolicy() { public void onAuthorizeRequestChallengeCachePresent() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + .verifyComplete(); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + // Challenge cache created - onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader).block(); + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); // Challenge cache used - policy.authorizeRequest(this.testContext).block(); + policy.authorizeRequestSync(this.testContext); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentWithClaims() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + .then(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader)) // Challenge with claims received + .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + .verifyComplete(); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + } + + @Test + public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + // Challenge cache created + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + // Challenge with claims received + onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithoutHeaderAndClaims); + // Challenge cache used alongside claims + policy.authorizeRequestSync(this.testContext); String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); assertFalse(tokenValue.isEmpty()); @@ -212,20 +266,6 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { assertTrue(onChallenge); } - @Test - public void onAuthorizeRequestChallengeCachePresentSync() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - - // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); - // Challenge cache used - policy.authorizeRequestSync(this.testContext); - - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); - } - private Mono onChallenge(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); From e0b5dec6e74c0a499758c125242b8a1d9a55b263 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Mon, 7 Oct 2024 12:57:17 -0700 Subject: [PATCH 06/18] Reverted accidental commit. --- .../com/azure/security/keyvault/secrets/SecretClientTest.java | 1 - .../azure/security/keyvault/secrets/SecretClientTestBase.java | 3 --- 2 files changed, 4 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTest.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTest.java index a59196398bd73..452a1ecaa34f8 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTest.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTest.java @@ -45,7 +45,6 @@ private void createClient(HttpClient httpClient, SecretServiceVersion serviceVer private void createClient(HttpClient httpClient, SecretServiceVersion serviceVersion, String testTenantId) { secretClient = getClientBuilder(buildSyncAssertingClient(interceptorManager.isPlaybackMode() ? interceptorManager.getPlaybackClient() : httpClient), testTenantId, getEndpoint(), serviceVersion) - .disableChallengeResourceVerification() .buildClient(); if (!interceptorManager.isLiveMode()) { diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTestBase.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTestBase.java index 906b9b0c2f051..e57529c17573e 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTestBase.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTestBase.java @@ -74,12 +74,10 @@ SecretClientBuilder getClientBuilder(HttpClient httpClient, String testTenantId, if (interceptorManager.isLiveMode()) { credential = new AzurePowerShellCredentialBuilder() - .httpLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS)) .additionallyAllowedTenants("*") .build(); } else if (interceptorManager.isRecordMode()) { credential = new DefaultAzureCredentialBuilder() - .httpLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS)) .additionallyAllowedTenants("*") .build(); } else { @@ -91,7 +89,6 @@ SecretClientBuilder getClientBuilder(HttpClient httpClient, String testTenantId, } SecretClientBuilder builder = new SecretClientBuilder() - .httpLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS)) .vaultUrl(endpoint) .serviceVersion(serviceVersion) .credential(credential) From 5e09204de72db6d902e6718fab44f78ae4d710be Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Mon, 7 Oct 2024 16:14:24 -0700 Subject: [PATCH 07/18] Ensured CAE is always enabled. --- .../KeyVaultCredentialPolicy.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index e8c738806551e..54619b5b1d363 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -118,7 +118,8 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); return setAuthorizationHeader(context, tokenRequestContext); } @@ -208,7 +209,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); String error = challengeAttributes.get("error"); @@ -219,9 +221,7 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(new String(Base64Util.decodeString(claims))); + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); } } } @@ -244,7 +244,8 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -331,7 +332,8 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); String error = challengeAttributes.get("error"); @@ -342,9 +344,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(new String(Base64Util.decodeString(claims))); + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); } } } From 5d31764449ab1f045b794452fd4fa9d73116b1fc Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Mon, 7 Oct 2024 16:39:40 -0700 Subject: [PATCH 08/18] Fixed a bug where challenge handling could fall into an infinite loop if the client received multiple consecutive 401 unauthorized requests without claims. --- .../secrets/implementation/KeyVaultCredentialPolicy.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index 54619b5b1d363..67fef55a0ebad 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -407,7 +407,7 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); if (newResponse.getStatusCode() == 401 && authHeader != null - && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { return handleChallenge(context, newResponse, nextPolicy); } else { @@ -431,7 +431,7 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); if (newResponse.getStatusCode() == 401 && authHeader != null - && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { return handleChallengeSync(context, newResponse, nextPolicy); } From 4aef99f9ec2002e5ef54841212fc32dccbcb9308 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Mon, 7 Oct 2024 16:39:52 -0700 Subject: [PATCH 09/18] Updated tests --- .../secrets/KeyVaultCredentialPolicyTest.java | 163 +++++++++++++----- 1 file changed, 124 insertions(+), 39 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java index 501062b4d8c63..236fc67739efb 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java @@ -32,8 +32,10 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -53,9 +55,10 @@ public class KeyVaultCredentialPolicyTest { private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); private BasicAuthenticationCredential credential; + private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; - private HttpResponse unauthorizedHttpResponseWithoutHeaderAndClaims; + private HttpResponse unauthorizedHttpResponseWithHeaderAndClaims; private HttpPipelineCallContext callContext; private HttpPipelineCallContext differentScopeContext; private HttpPipelineCallContext testContext; @@ -68,12 +71,13 @@ private static HttpPipelineCallContext createCallContext(HttpRequest request, Co HttpPipeline callContextCreator = new HttpPipelineBuilder() .policies((callContext, next) -> { callContextReference.set(callContext); + return next.process(); }) .httpClient(ignored -> Mono.empty()) .build(); - callContextCreator.send(request, context).block(); + callContextCreator.sendSync(request, context); return callContextReference.get(); } @@ -89,20 +93,24 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); - MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + MockHttpResponse unauthorizedResponseWithWrongStatusCode = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401); + MockHttpResponse unauthorizedResponseWithHeader = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER)); MockHttpResponse unauthorizedResponseWithHeaderAndClaims = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); + this.unauthorizedHttpResponseWithWrongStatusCode = unauthorizedResponseWithWrongStatusCode; this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; - this.unauthorizedHttpResponseWithoutHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; + this.unauthorizedHttpResponseWithHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); @@ -116,19 +124,57 @@ public void cleanup() { KeyVaultCredentialPolicy.clearCache(); } + @SyncAsyncTest + public void onNon401ErrorResponse() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(unauthorizedHttpResponseWithWrongStatusCode)) + .build(); + + SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext()) + ); + + assertNull(this.callContext.getHttpRequest().getHeaders().get(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); + } + + @SyncAsyncTest + public void on401UnauthorizedResponseWithHeader() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(unauthorizedHttpResponseWithHeader)) + .build(); + + SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext()) + ); + + assertNotNull(this.callContext.getHttpRequest().getHeaders().get(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); + } + @SyncAsyncTest public void onChallengeCredentialPolicy() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.callContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.callContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.callContext, this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.callContext, this.unauthorizedHttpResponseWithHeader) ); // Validate that the onChallengeSync ran successfully. assertTrue(onChallenge); - String tokenValue = this.callContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -137,11 +183,11 @@ public void onChallengeCredentialPolicy() { public void onAuthorizeRequestChallengeCachePresent() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + StepVerifier.create(onChallengeAndClearCache(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created .then(policy.authorizeRequest(this.testContext))) // Challenge cache used .verifyComplete(); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -151,11 +197,11 @@ public void onAuthorizeRequestChallengeCachePresentSync() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + onChallengeAndClearCacheSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); // Challenge cache used policy.authorizeRequestSync(this.testContext); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -164,14 +210,36 @@ public void onAuthorizeRequestChallengeCachePresentSync() { public void onAuthorizeRequestChallengeCachePresentWithClaims() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created - .then(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader)) // Challenge with claims received - .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created + this.unauthorizedHttpResponseWithHeader) + .flatMap(result -> result ? policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received + this.unauthorizedHttpResponseWithHeaderAndClaims) : Mono.just(false))) + .assertNext(result -> { + assertTrue(result); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + }) .verifyComplete(); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + KeyVaultCredentialPolicy.clearCache(); + } + + @Test + public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created + this.unauthorizedHttpResponseWithHeaderAndClaims)) + .assertNext(result -> { + assertFalse(result); + assertNull(this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); + }) + .verifyComplete(); + + KeyVaultCredentialPolicy.clearCache(); } @Test @@ -179,15 +247,28 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeader)); // Challenge with claims received - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithoutHeaderAndClaims); - // Challenge cache used alongside claims - policy.authorizeRequestSync(this.testContext); + assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, + this.unauthorizedHttpResponseWithHeaderAndClaims)); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); + + KeyVaultCredentialPolicy.clearCache(); + } + + @Test + public void onAuthorizeRequestChallengeNoCachePresentWithClaimsSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + // Challenge with claims received + assertFalse(policy.authorizeRequestOnChallengeSync(this.callContext, + this.unauthorizedHttpResponseWithHeaderAndClaims)); + assertNull(this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); } @SyncAsyncTest @@ -200,7 +281,7 @@ public void onAuthorizeRequestNoCache() { () -> policy.authorizeRequest(this.callContext) ); - assertNull(this.callContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION)); + assertNull(this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); } @SyncAsyncTest @@ -208,21 +289,21 @@ public void testSetContentLengthHeader() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.bodyContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.bodyFluxContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.bodyContext, this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.bodyFluxContext, this.unauthorizedHttpResponseWithHeader) ); // Validate that the onChallengeSync ran successfully. assertTrue(onChallenge); HttpHeaders headers = this.bodyFluxContext.getHttpRequest().getHeaders(); - String tokenValue = headers.getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = headers.getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); assertEquals("21", headers.getValue(HttpHeaderName.CONTENT_LENGTH)); HttpHeaders syncHeaders = this.bodyContext.getHttpRequest().getHeaders(); - String syncTokenValue = headers.getValue(HttpHeaderName.AUTHORIZATION); + String syncTokenValue = headers.getValue(AUTHORIZATION); assertFalse(syncTokenValue.isEmpty()); assertTrue(syncTokenValue.startsWith(BEARER)); assertEquals("21", syncHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); @@ -233,8 +314,8 @@ public void onAuthorizeRequestNoScope() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader), - () -> onChallenge(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader) + () -> onChallengeAndClearCacheSync(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader), + () -> onChallengeAndClearCache(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader) ); assertFalse(onChallenge); @@ -245,9 +326,11 @@ public void onAuthorizeRequestDifferentScope() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); assertThrows(RuntimeException.class, - () -> onChallengeSync(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader)); + () -> onChallengeAndClearCacheSync(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader)); - StepVerifier.create(onChallenge(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader)) + StepVerifier.create(onChallengeAndClearCache(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader)) .verifyErrorMessage("The challenge resource 'https://vault.azure.net/.default' does not match the " + "requested domain. If you wish to disable this check for your client, pass 'true' to the " + "SecretClientBuilder.disableChallengeResourceVerification() method when building it. See " @@ -259,22 +342,24 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, true); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader) ); assertTrue(onChallenge); } - private Mono onChallenge(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, - HttpResponse unauthorizedHttpResponse) { + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, + HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); KeyVaultCredentialPolicy.clearCache(); return onChallenge; } - private boolean onChallengeSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, - HttpResponse unauthorizedHttpResponse) { + private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, + HttpResponse unauthorizedHttpResponse) { boolean onChallengeSync = policy.authorizeRequestOnChallengeSync(callContext, unauthorizedHttpResponse); KeyVaultCredentialPolicy.clearCache(); return onChallengeSync; From 1b5173e5faff062b6fd109f2f2616ed6742b4a34 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Mon, 7 Oct 2024 16:43:11 -0700 Subject: [PATCH 10/18] Updated all other KV packages. --- .../KeyVaultCredentialPolicy.java | 24 +-- .../KeyVaultCredentialPolicyTest.java | 163 +++++++++++++----- .../KeyVaultCredentialPolicy.java | 24 +-- .../KeyVaultCredentialPolicyTest.java | 163 +++++++++++++----- .../KeyVaultCredentialPolicy.java | 24 +-- .../keys/KeyVaultCredentialPolicyTest.java | 163 +++++++++++++----- 6 files changed, 408 insertions(+), 153 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java index 69e44beb9226d..42d318d8bda33 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java @@ -118,7 +118,8 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); return setAuthorizationHeader(context, tokenRequestContext); } @@ -208,7 +209,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); String error = challengeAttributes.get("error"); @@ -219,9 +221,7 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(new String(Base64Util.decodeString(claims))); + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); } } } @@ -244,7 +244,8 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -331,7 +332,8 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); String error = challengeAttributes.get("error"); @@ -342,9 +344,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(new String(Base64Util.decodeString(claims))); + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); } } } @@ -407,7 +407,7 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); if (newResponse.getStatusCode() == 401 && authHeader != null - && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { return handleChallenge(context, newResponse, nextPolicy); } else { @@ -431,7 +431,7 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); if (newResponse.getStatusCode() == 401 && authHeader != null - && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { return handleChallengeSync(context, newResponse, nextPolicy); } diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java index 5ba7d4e2bc5f3..c670eb92ca39c 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java @@ -32,8 +32,10 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -53,9 +55,10 @@ public class KeyVaultCredentialPolicyTest { private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); private BasicAuthenticationCredential credential; + private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; - private HttpResponse unauthorizedHttpResponseWithoutHeaderAndClaims; + private HttpResponse unauthorizedHttpResponseWithHeaderAndClaims; private HttpPipelineCallContext callContext; private HttpPipelineCallContext differentScopeContext; private HttpPipelineCallContext testContext; @@ -68,12 +71,13 @@ private static HttpPipelineCallContext createCallContext(HttpRequest request, Co HttpPipeline callContextCreator = new HttpPipelineBuilder() .policies((callContext, next) -> { callContextReference.set(callContext); + return next.process(); }) .httpClient(ignored -> Mono.empty()) .build(); - callContextCreator.send(request, context).block(); + callContextCreator.sendSync(request, context); return callContextReference.get(); } @@ -89,20 +93,24 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); - MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + MockHttpResponse unauthorizedResponseWithWrongStatusCode = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401); + MockHttpResponse unauthorizedResponseWithHeader = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER)); MockHttpResponse unauthorizedResponseWithHeaderAndClaims = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); + this.unauthorizedHttpResponseWithWrongStatusCode = unauthorizedResponseWithWrongStatusCode; this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; - this.unauthorizedHttpResponseWithoutHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; + this.unauthorizedHttpResponseWithHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); @@ -116,19 +124,57 @@ public void cleanup() { KeyVaultCredentialPolicy.clearCache(); } + @SyncAsyncTest + public void onNon401ErrorResponse() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(unauthorizedHttpResponseWithWrongStatusCode)) + .build(); + + SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext()) + ); + + assertNull(this.callContext.getHttpRequest().getHeaders().get(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); + } + + @SyncAsyncTest + public void on401UnauthorizedResponseWithHeader() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(unauthorizedHttpResponseWithHeader)) + .build(); + + SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext()) + ); + + assertNotNull(this.callContext.getHttpRequest().getHeaders().get(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); + } + @SyncAsyncTest public void onChallengeCredentialPolicy() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.callContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.callContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.callContext, this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.callContext, this.unauthorizedHttpResponseWithHeader) ); // Validate that the onChallengeSync ran successfully. assertTrue(onChallenge); - String tokenValue = this.callContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -137,11 +183,11 @@ public void onChallengeCredentialPolicy() { public void onAuthorizeRequestChallengeCachePresent() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + StepVerifier.create(onChallengeAndClearCache(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created .then(policy.authorizeRequest(this.testContext))) // Challenge cache used .verifyComplete(); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -151,11 +197,11 @@ public void onAuthorizeRequestChallengeCachePresentSync() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + onChallengeAndClearCacheSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); // Challenge cache used policy.authorizeRequestSync(this.testContext); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -164,14 +210,36 @@ public void onAuthorizeRequestChallengeCachePresentSync() { public void onAuthorizeRequestChallengeCachePresentWithClaims() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created - .then(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader)) // Challenge with claims received - .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created + this.unauthorizedHttpResponseWithHeader) + .flatMap(result -> result ? policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received + this.unauthorizedHttpResponseWithHeaderAndClaims) : Mono.just(false))) + .assertNext(result -> { + assertTrue(result); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + }) .verifyComplete(); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + KeyVaultCredentialPolicy.clearCache(); + } + + @Test + public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created + this.unauthorizedHttpResponseWithHeaderAndClaims)) + .assertNext(result -> { + assertFalse(result); + assertNull(this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); + }) + .verifyComplete(); + + KeyVaultCredentialPolicy.clearCache(); } @Test @@ -179,15 +247,28 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeader)); // Challenge with claims received - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithoutHeaderAndClaims); - // Challenge cache used alongside claims - policy.authorizeRequestSync(this.testContext); + assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, + this.unauthorizedHttpResponseWithHeaderAndClaims)); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); + + KeyVaultCredentialPolicy.clearCache(); + } + + @Test + public void onAuthorizeRequestChallengeNoCachePresentWithClaimsSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + // Challenge with claims received + assertFalse(policy.authorizeRequestOnChallengeSync(this.callContext, + this.unauthorizedHttpResponseWithHeaderAndClaims)); + assertNull(this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); } @SyncAsyncTest @@ -200,7 +281,7 @@ public void onAuthorizeRequestNoCache() { () -> policy.authorizeRequest(this.callContext) ); - assertNull(this.callContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION)); + assertNull(this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); } @SyncAsyncTest @@ -208,21 +289,21 @@ public void testSetContentLengthHeader() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.bodyContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.bodyFluxContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.bodyContext, this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.bodyFluxContext, this.unauthorizedHttpResponseWithHeader) ); // Validate that the onChallengeSync ran successfully. assertTrue(onChallenge); HttpHeaders headers = this.bodyFluxContext.getHttpRequest().getHeaders(); - String tokenValue = headers.getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = headers.getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); assertEquals("21", headers.getValue(HttpHeaderName.CONTENT_LENGTH)); HttpHeaders syncHeaders = this.bodyContext.getHttpRequest().getHeaders(); - String syncTokenValue = headers.getValue(HttpHeaderName.AUTHORIZATION); + String syncTokenValue = headers.getValue(AUTHORIZATION); assertFalse(syncTokenValue.isEmpty()); assertTrue(syncTokenValue.startsWith(BEARER)); assertEquals("21", syncHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); @@ -233,8 +314,8 @@ public void onAuthorizeRequestNoScope() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader), - () -> onChallenge(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader) + () -> onChallengeAndClearCacheSync(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader), + () -> onChallengeAndClearCache(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader) ); assertFalse(onChallenge); @@ -245,9 +326,11 @@ public void onAuthorizeRequestDifferentScope() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); assertThrows(RuntimeException.class, - () -> onChallengeSync(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader)); + () -> onChallengeAndClearCacheSync(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader)); - StepVerifier.create(onChallenge(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader)) + StepVerifier.create(onChallengeAndClearCache(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader)) .verifyErrorMessage("The challenge resource 'https://vault.azure.net/.default' does not match the " + "requested domain. If you wish to disable this check for your client, pass 'true' to the " + "SecretClientBuilder.disableChallengeResourceVerification() method when building it. See " @@ -259,22 +342,24 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, true); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader) ); assertTrue(onChallenge); } - private Mono onChallenge(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, - HttpResponse unauthorizedHttpResponse) { + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, + HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); KeyVaultCredentialPolicy.clearCache(); return onChallenge; } - private boolean onChallengeSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, - HttpResponse unauthorizedHttpResponse) { + private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, + HttpResponse unauthorizedHttpResponse) { boolean onChallengeSync = policy.authorizeRequestOnChallengeSync(callContext, unauthorizedHttpResponse); KeyVaultCredentialPolicy.clearCache(); return onChallengeSync; diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java index 74b10cae4dfed..2a40de36a4fc8 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java @@ -118,7 +118,8 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); return setAuthorizationHeader(context, tokenRequestContext); } @@ -208,7 +209,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); String error = challengeAttributes.get("error"); @@ -219,9 +221,7 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(new String(Base64Util.decodeString(claims))); + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); } } } @@ -244,7 +244,8 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -331,7 +332,8 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); String error = challengeAttributes.get("error"); @@ -342,9 +344,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(new String(Base64Util.decodeString(claims))); + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); } } } @@ -407,7 +407,7 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); if (newResponse.getStatusCode() == 401 && authHeader != null - && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { return handleChallenge(context, newResponse, nextPolicy); } else { @@ -431,7 +431,7 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); if (newResponse.getStatusCode() == 401 && authHeader != null - && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { return handleChallengeSync(context, newResponse, nextPolicy); } diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java index a6956b222b1e4..7f6f63f57c53b 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java @@ -32,8 +32,10 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -53,9 +55,10 @@ public class KeyVaultCredentialPolicyTest { private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); private BasicAuthenticationCredential credential; + private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; - private HttpResponse unauthorizedHttpResponseWithoutHeaderAndClaims; + private HttpResponse unauthorizedHttpResponseWithHeaderAndClaims; private HttpPipelineCallContext callContext; private HttpPipelineCallContext differentScopeContext; private HttpPipelineCallContext testContext; @@ -68,12 +71,13 @@ private static HttpPipelineCallContext createCallContext(HttpRequest request, Co HttpPipeline callContextCreator = new HttpPipelineBuilder() .policies((callContext, next) -> { callContextReference.set(callContext); + return next.process(); }) .httpClient(ignored -> Mono.empty()) .build(); - callContextCreator.send(request, context).block(); + callContextCreator.sendSync(request, context); return callContextReference.get(); } @@ -89,20 +93,24 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); - MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + MockHttpResponse unauthorizedResponseWithWrongStatusCode = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401); + MockHttpResponse unauthorizedResponseWithHeader = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER)); MockHttpResponse unauthorizedResponseWithHeaderAndClaims = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); + this.unauthorizedHttpResponseWithWrongStatusCode = unauthorizedResponseWithWrongStatusCode; this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; - this.unauthorizedHttpResponseWithoutHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; + this.unauthorizedHttpResponseWithHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); @@ -116,19 +124,57 @@ public void cleanup() { KeyVaultCredentialPolicy.clearCache(); } + @SyncAsyncTest + public void onNon401ErrorResponse() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(unauthorizedHttpResponseWithWrongStatusCode)) + .build(); + + SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext()) + ); + + assertNull(this.callContext.getHttpRequest().getHeaders().get(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); + } + + @SyncAsyncTest + public void on401UnauthorizedResponseWithHeader() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(unauthorizedHttpResponseWithHeader)) + .build(); + + SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext()) + ); + + assertNotNull(this.callContext.getHttpRequest().getHeaders().get(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); + } + @SyncAsyncTest public void onChallengeCredentialPolicy() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.callContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.callContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.callContext, this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.callContext, this.unauthorizedHttpResponseWithHeader) ); // Validate that the onChallengeSync ran successfully. assertTrue(onChallenge); - String tokenValue = this.callContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -137,11 +183,11 @@ public void onChallengeCredentialPolicy() { public void onAuthorizeRequestChallengeCachePresent() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + StepVerifier.create(onChallengeAndClearCache(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created .then(policy.authorizeRequest(this.testContext))) // Challenge cache used .verifyComplete(); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -151,11 +197,11 @@ public void onAuthorizeRequestChallengeCachePresentSync() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + onChallengeAndClearCacheSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); // Challenge cache used policy.authorizeRequestSync(this.testContext); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -164,14 +210,36 @@ public void onAuthorizeRequestChallengeCachePresentSync() { public void onAuthorizeRequestChallengeCachePresentWithClaims() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created - .then(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader)) // Challenge with claims received - .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created + this.unauthorizedHttpResponseWithHeader) + .flatMap(result -> result ? policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received + this.unauthorizedHttpResponseWithHeaderAndClaims) : Mono.just(false))) + .assertNext(result -> { + assertTrue(result); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + }) .verifyComplete(); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + KeyVaultCredentialPolicy.clearCache(); + } + + @Test + public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created + this.unauthorizedHttpResponseWithHeaderAndClaims)) + .assertNext(result -> { + assertFalse(result); + assertNull(this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); + }) + .verifyComplete(); + + KeyVaultCredentialPolicy.clearCache(); } @Test @@ -179,15 +247,28 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeader)); // Challenge with claims received - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithoutHeaderAndClaims); - // Challenge cache used alongside claims - policy.authorizeRequestSync(this.testContext); + assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, + this.unauthorizedHttpResponseWithHeaderAndClaims)); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); + + KeyVaultCredentialPolicy.clearCache(); + } + + @Test + public void onAuthorizeRequestChallengeNoCachePresentWithClaimsSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + // Challenge with claims received + assertFalse(policy.authorizeRequestOnChallengeSync(this.callContext, + this.unauthorizedHttpResponseWithHeaderAndClaims)); + assertNull(this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); } @SyncAsyncTest @@ -200,7 +281,7 @@ public void onAuthorizeRequestNoCache() { () -> policy.authorizeRequest(this.callContext) ); - assertNull(this.callContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION)); + assertNull(this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); } @SyncAsyncTest @@ -208,21 +289,21 @@ public void testSetContentLengthHeader() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.bodyContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.bodyFluxContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.bodyContext, this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.bodyFluxContext, this.unauthorizedHttpResponseWithHeader) ); // Validate that the onChallengeSync ran successfully. assertTrue(onChallenge); HttpHeaders headers = this.bodyFluxContext.getHttpRequest().getHeaders(); - String tokenValue = headers.getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = headers.getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); assertEquals("21", headers.getValue(HttpHeaderName.CONTENT_LENGTH)); HttpHeaders syncHeaders = this.bodyContext.getHttpRequest().getHeaders(); - String syncTokenValue = headers.getValue(HttpHeaderName.AUTHORIZATION); + String syncTokenValue = headers.getValue(AUTHORIZATION); assertFalse(syncTokenValue.isEmpty()); assertTrue(syncTokenValue.startsWith(BEARER)); assertEquals("21", syncHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); @@ -233,8 +314,8 @@ public void onAuthorizeRequestNoScope() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader), - () -> onChallenge(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader) + () -> onChallengeAndClearCacheSync(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader), + () -> onChallengeAndClearCache(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader) ); assertFalse(onChallenge); @@ -245,9 +326,11 @@ public void onAuthorizeRequestDifferentScope() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); assertThrows(RuntimeException.class, - () -> onChallengeSync(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader)); + () -> onChallengeAndClearCacheSync(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader)); - StepVerifier.create(onChallenge(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader)) + StepVerifier.create(onChallengeAndClearCache(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader)) .verifyErrorMessage("The challenge resource 'https://vault.azure.net/.default' does not match the " + "requested domain. If you wish to disable this check for your client, pass 'true' to the " + "SecretClientBuilder.disableChallengeResourceVerification() method when building it. See " @@ -259,22 +342,24 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, true); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader) ); assertTrue(onChallenge); } - private Mono onChallenge(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, - HttpResponse unauthorizedHttpResponse) { + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, + HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); KeyVaultCredentialPolicy.clearCache(); return onChallenge; } - private boolean onChallengeSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, - HttpResponse unauthorizedHttpResponse) { + private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, + HttpResponse unauthorizedHttpResponse) { boolean onChallengeSync = policy.authorizeRequestOnChallengeSync(callContext, unauthorizedHttpResponse); KeyVaultCredentialPolicy.clearCache(); return onChallengeSync; diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java index 76952b9e6cdb0..6a1ad5ab21a2c 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java @@ -118,7 +118,8 @@ public Mono authorizeRequest(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); return setAuthorizationHeader(context, tokenRequestContext); } @@ -208,7 +209,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); String error = challengeAttributes.get("error"); @@ -219,9 +221,7 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(new String(Base64Util.decodeString(claims))); + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); } } } @@ -244,7 +244,8 @@ public void authorizeRequestSync(HttpPipelineCallContext context) { // We fetched the challenge from the cache, but we have not initialized the scopes in the base yet. TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); setAuthorizationHeaderSync(context, tokenRequestContext); @@ -331,7 +332,8 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, TokenRequestContext tokenRequestContext = new TokenRequestContext() .addScopes(this.challenge.getScopes()) - .setTenantId(this.challenge.getTenantId()); + .setTenantId(this.challenge.getTenantId()) + .setCaeEnabled(true); String error = challengeAttributes.get("error"); @@ -342,9 +344,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext - .setCaeEnabled(true) - .setClaims(new String(Base64Util.decodeString(claims))); + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); } } } @@ -407,7 +407,7 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); if (newResponse.getStatusCode() == 401 && authHeader != null - && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { return handleChallenge(context, newResponse, nextPolicy); } else { @@ -431,7 +431,7 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); if (newResponse.getStatusCode() == 401 && authHeader != null - && !(isClaimsPresent(httpResponse) && isClaimsPresent(newResponse))) { + && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { return handleChallengeSync(context, newResponse, nextPolicy); } diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java index ae7fbf4586727..817f3a477b9e1 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java @@ -32,8 +32,10 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -53,9 +55,10 @@ public class KeyVaultCredentialPolicyTest { private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); private BasicAuthenticationCredential credential; + private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; - private HttpResponse unauthorizedHttpResponseWithoutHeaderAndClaims; + private HttpResponse unauthorizedHttpResponseWithHeaderAndClaims; private HttpPipelineCallContext callContext; private HttpPipelineCallContext differentScopeContext; private HttpPipelineCallContext testContext; @@ -68,12 +71,13 @@ private static HttpPipelineCallContext createCallContext(HttpRequest request, Co HttpPipeline callContextCreator = new HttpPipelineBuilder() .policies((callContext, next) -> { callContextReference.set(callContext); + return next.process(); }) .httpClient(ignored -> Mono.empty()) .build(); - callContextCreator.send(request, context).block(); + callContextCreator.sendSync(request, context); return callContextReference.get(); } @@ -89,20 +93,24 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); - MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + MockHttpResponse unauthorizedResponseWithWrongStatusCode = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); + MockHttpResponse unauthorizedResponseWithoutHeader = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401); + MockHttpResponse unauthorizedResponseWithHeader = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER)); MockHttpResponse unauthorizedResponseWithHeaderAndClaims = new MockHttpResponse( - new HttpRequest(HttpMethod.GET, "https://azure.com"), 500, + new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); + this.unauthorizedHttpResponseWithWrongStatusCode = unauthorizedResponseWithWrongStatusCode; this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; - this.unauthorizedHttpResponseWithoutHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; + this.unauthorizedHttpResponseWithHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); @@ -116,19 +124,57 @@ public void cleanup() { KeyVaultCredentialPolicy.clearCache(); } + @SyncAsyncTest + public void onNon401ErrorResponse() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(unauthorizedHttpResponseWithWrongStatusCode)) + .build(); + + SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext()) + ); + + assertNull(this.callContext.getHttpRequest().getHeaders().get(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); + } + + @SyncAsyncTest + public void on401UnauthorizedResponseWithHeader() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(unauthorizedHttpResponseWithHeader)) + .build(); + + SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext()) + ); + + assertNotNull(this.callContext.getHttpRequest().getHeaders().get(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); + } + @SyncAsyncTest public void onChallengeCredentialPolicy() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.callContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.callContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.callContext, this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.callContext, this.unauthorizedHttpResponseWithHeader) ); // Validate that the onChallengeSync ran successfully. assertTrue(onChallenge); - String tokenValue = this.callContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -137,11 +183,11 @@ public void onChallengeCredentialPolicy() { public void onAuthorizeRequestChallengeCachePresent() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created + StepVerifier.create(onChallengeAndClearCache(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created .then(policy.authorizeRequest(this.testContext))) // Challenge cache used .verifyComplete(); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -151,11 +197,11 @@ public void onAuthorizeRequestChallengeCachePresentSync() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + onChallengeAndClearCacheSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); // Challenge cache used policy.authorizeRequestSync(this.testContext); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); } @@ -164,14 +210,36 @@ public void onAuthorizeRequestChallengeCachePresentSync() { public void onAuthorizeRequestChallengeCachePresentWithClaims() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); - StepVerifier.create(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader) // Challenge cache created - .then(onChallenge(policy, this.callContext, unauthorizedHttpResponseWithHeader)) // Challenge with claims received - .then(policy.authorizeRequest(this.testContext))) // Challenge cache used + StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created + this.unauthorizedHttpResponseWithHeader) + .flatMap(result -> result ? policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received + this.unauthorizedHttpResponseWithHeaderAndClaims) : Mono.just(false))) + .assertNext(result -> { + assertTrue(result); + + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(tokenValue.isEmpty()); + assertTrue(tokenValue.startsWith(BEARER)); + }) .verifyComplete(); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + KeyVaultCredentialPolicy.clearCache(); + } + + @Test + public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created + this.unauthorizedHttpResponseWithHeaderAndClaims)) + .assertNext(result -> { + assertFalse(result); + assertNull(this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); + }) + .verifyComplete(); + + KeyVaultCredentialPolicy.clearCache(); } @Test @@ -179,15 +247,28 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); // Challenge cache created - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithHeader); + assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeader)); // Challenge with claims received - onChallengeSync(policy, this.callContext, unauthorizedHttpResponseWithoutHeaderAndClaims); - // Challenge cache used alongside claims - policy.authorizeRequestSync(this.testContext); + assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, + this.unauthorizedHttpResponseWithHeaderAndClaims)); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); + + KeyVaultCredentialPolicy.clearCache(); + } + + @Test + public void onAuthorizeRequestChallengeNoCachePresentWithClaimsSync() { + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + + // Challenge with claims received + assertFalse(policy.authorizeRequestOnChallengeSync(this.callContext, + this.unauthorizedHttpResponseWithHeaderAndClaims)); + assertNull(this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); + + KeyVaultCredentialPolicy.clearCache(); } @SyncAsyncTest @@ -200,7 +281,7 @@ public void onAuthorizeRequestNoCache() { () -> policy.authorizeRequest(this.callContext) ); - assertNull(this.callContext.getHttpRequest().getHeaders().getValue(HttpHeaderName.AUTHORIZATION)); + assertNull(this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION)); } @SyncAsyncTest @@ -208,21 +289,21 @@ public void testSetContentLengthHeader() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.bodyContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.bodyFluxContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.bodyContext, this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.bodyFluxContext, this.unauthorizedHttpResponseWithHeader) ); // Validate that the onChallengeSync ran successfully. assertTrue(onChallenge); HttpHeaders headers = this.bodyFluxContext.getHttpRequest().getHeaders(); - String tokenValue = headers.getValue(HttpHeaderName.AUTHORIZATION); + String tokenValue = headers.getValue(AUTHORIZATION); assertFalse(tokenValue.isEmpty()); assertTrue(tokenValue.startsWith(BEARER)); assertEquals("21", headers.getValue(HttpHeaderName.CONTENT_LENGTH)); HttpHeaders syncHeaders = this.bodyContext.getHttpRequest().getHeaders(); - String syncTokenValue = headers.getValue(HttpHeaderName.AUTHORIZATION); + String syncTokenValue = headers.getValue(AUTHORIZATION); assertFalse(syncTokenValue.isEmpty()); assertTrue(syncTokenValue.startsWith(BEARER)); assertEquals("21", syncHeaders.getValue(HttpHeaderName.CONTENT_LENGTH)); @@ -233,8 +314,8 @@ public void onAuthorizeRequestNoScope() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader), - () -> onChallenge(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader) + () -> onChallengeAndClearCacheSync(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader), + () -> onChallengeAndClearCache(policy, this.callContext, this.unauthorizedHttpResponseWithoutHeader) ); assertFalse(onChallenge); @@ -245,9 +326,11 @@ public void onAuthorizeRequestDifferentScope() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); assertThrows(RuntimeException.class, - () -> onChallengeSync(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader)); + () -> onChallengeAndClearCacheSync(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader)); - StepVerifier.create(onChallenge(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader)) + StepVerifier.create(onChallengeAndClearCache(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader)) .verifyErrorMessage("The challenge resource 'https://vault.azure.net/.default' does not match the " + "requested domain. If you wish to disable this check for your client, pass 'true' to the " + "SecretClientBuilder.disableChallengeResourceVerification() method when building it. See " @@ -259,22 +342,24 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, true); boolean onChallenge = SyncAsyncExtension.execute( - () -> onChallengeSync(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader), - () -> onChallenge(policy, this.differentScopeContext, this.unauthorizedHttpResponseWithHeader) + () -> onChallengeAndClearCacheSync(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader), + () -> onChallengeAndClearCache(policy, this.differentScopeContext, + this.unauthorizedHttpResponseWithHeader) ); assertTrue(onChallenge); } - private Mono onChallenge(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, - HttpResponse unauthorizedHttpResponse) { + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, + HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); KeyVaultCredentialPolicy.clearCache(); return onChallenge; } - private boolean onChallengeSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, - HttpResponse unauthorizedHttpResponse) { + private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, + HttpResponse unauthorizedHttpResponse) { boolean onChallengeSync = policy.authorizeRequestOnChallengeSync(callContext, unauthorizedHttpResponse); KeyVaultCredentialPolicy.clearCache(); return onChallengeSync; From a57cb96d89d9b07701e8cdd7dd66a14abd70dd60 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Tue, 8 Oct 2024 13:27:47 -0700 Subject: [PATCH 11/18] Removed unused imports. --- .../azure/security/keyvault/secrets/SecretClientTestBase.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTestBase.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTestBase.java index e57529c17573e..e395f303998b1 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTestBase.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/SecretClientTestBase.java @@ -8,8 +8,6 @@ import com.azure.core.http.HttpClient; import com.azure.core.http.policy.ExponentialBackoffOptions; import com.azure.core.http.policy.FixedDelayOptions; -import com.azure.core.http.policy.HttpLogDetailLevel; -import com.azure.core.http.policy.HttpLogOptions; import com.azure.core.http.policy.RetryOptions; import com.azure.core.test.TestProxyTestBase; import com.azure.core.test.models.BodilessMatcher; From 9d9d766b03e12b073dd6ee227e2e7b6435273f8e Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Tue, 8 Oct 2024 15:44:40 -0700 Subject: [PATCH 12/18] Optimized challenge processing. --- .../KeyVaultCredentialPolicy.java | 29 ++++++++++--------- .../KeyVaultCredentialPolicy.java | 29 ++++++++++--------- .../KeyVaultCredentialPolicy.java | 29 ++++++++++--------- .../KeyVaultCredentialPolicy.java | 29 ++++++++++--------- 4 files changed, 64 insertions(+), 52 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java index 42d318d8bda33..7891bcd99f06a 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java @@ -17,6 +17,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -72,20 +73,17 @@ private static Map extractChallengeAttributes(String authenticat return Collections.emptyMap(); } - String[] attributes = authenticateHeader.substring(authChallengePrefix.length()).split(", "); + String[] attributes = authenticateHeader + .replace("\"", "") + .substring(authChallengePrefix.length()) + .split(","); Map attributeMap = new HashMap<>(); for (String pair : attributes) { - // This is ugly, but we need to trim here because currently the 'claims' attribute comes after two spaces. - pair = pair.trim(); + // Using trim is ugly, but we need it here because currently the 'claims' attribute comes after two spaces. + String[] keyValue = pair.trim().split("=", 2); - if (pair.startsWith("claims=")) { - attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); - } else { - String[] keyValue = pair.split("="); - - attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); - } + attributeMap.put(keyValue[0], keyValue[1]); } return attributeMap; @@ -215,13 +213,18 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String error = challengeAttributes.get("error"); if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + LOGGER.verbose("The challenge response contained an error: {}", error); if ("insufficient_claims".equalsIgnoreCase(error)) { String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); + try { + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims), "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw LOGGER.logExceptionAsError( + new RuntimeException("Failed to decode the claims from the challenge.", e)); + } } } } @@ -338,7 +341,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String error = challengeAttributes.get("error"); if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + LOGGER.verbose("The challenge response contained an error: {}", error); if ("insufficient_claims".equalsIgnoreCase(error)) { String claims = challengeAttributes.get("claims"); diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java index 2a40de36a4fc8..af977255e8fd9 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java @@ -17,6 +17,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -72,20 +73,17 @@ private static Map extractChallengeAttributes(String authenticat return Collections.emptyMap(); } - String[] attributes = authenticateHeader.substring(authChallengePrefix.length()).split(", "); + String[] attributes = authenticateHeader + .replace("\"", "") + .substring(authChallengePrefix.length()) + .split(","); Map attributeMap = new HashMap<>(); for (String pair : attributes) { - // This is ugly, but we need to trim here because currently the 'claims' attribute comes after two spaces. - pair = pair.trim(); + // Using trim is ugly, but we need it here because currently the 'claims' attribute comes after two spaces. + String[] keyValue = pair.trim().split("=", 2); - if (pair.startsWith("claims=")) { - attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); - } else { - String[] keyValue = pair.split("="); - - attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); - } + attributeMap.put(keyValue[0], keyValue[1]); } return attributeMap; @@ -215,13 +213,18 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String error = challengeAttributes.get("error"); if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + LOGGER.verbose("The challenge response contained an error: {}", error); if ("insufficient_claims".equalsIgnoreCase(error)) { String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); + try { + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims), "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw LOGGER.logExceptionAsError( + new RuntimeException("Failed to decode the claims from the challenge.", e)); + } } } } @@ -338,7 +341,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String error = challengeAttributes.get("error"); if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + LOGGER.verbose("The challenge response contained an error: {}", error); if ("insufficient_claims".equalsIgnoreCase(error)) { String claims = challengeAttributes.get("claims"); diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java index 6a1ad5ab21a2c..c26674a9a7ad2 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java @@ -17,6 +17,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -72,20 +73,17 @@ private static Map extractChallengeAttributes(String authenticat return Collections.emptyMap(); } - String[] attributes = authenticateHeader.substring(authChallengePrefix.length()).split(", "); + String[] attributes = authenticateHeader + .replace("\"", "") + .substring(authChallengePrefix.length()) + .split(","); Map attributeMap = new HashMap<>(); for (String pair : attributes) { - // This is ugly, but we need to trim here because currently the 'claims' attribute comes after two spaces. - pair = pair.trim(); + // Using trim is ugly, but we need it here because currently the 'claims' attribute comes after two spaces. + String[] keyValue = pair.trim().split("=", 2); - if (pair.startsWith("claims=")) { - attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); - } else { - String[] keyValue = pair.split("="); - - attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); - } + attributeMap.put(keyValue[0], keyValue[1]); } return attributeMap; @@ -215,13 +213,18 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String error = challengeAttributes.get("error"); if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + LOGGER.verbose("The challenge response contained an error: {}", error); if ("insufficient_claims".equalsIgnoreCase(error)) { String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); + try { + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims), "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw LOGGER.logExceptionAsError( + new RuntimeException("Failed to decode the claims from the challenge.", e)); + } } } } @@ -338,7 +341,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String error = challengeAttributes.get("error"); if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + LOGGER.verbose("The challenge response contained an error: {}", error); if ("insufficient_claims".equalsIgnoreCase(error)) { String claims = challengeAttributes.get("claims"); diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index 67fef55a0ebad..fdff53103a8a4 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -17,6 +17,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -72,20 +73,17 @@ private static Map extractChallengeAttributes(String authenticat return Collections.emptyMap(); } - String[] attributes = authenticateHeader.substring(authChallengePrefix.length()).split(", "); + String[] attributes = authenticateHeader + .replace("\"", "") + .substring(authChallengePrefix.length()) + .split(","); Map attributeMap = new HashMap<>(); for (String pair : attributes) { - // This is ugly, but we need to trim here because currently the 'claims' attribute comes after two spaces. - pair = pair.trim(); + // Using trim is ugly, but we need it here because currently the 'claims' attribute comes after two spaces. + String[] keyValue = pair.trim().split("=", 2); - if (pair.startsWith("claims=")) { - attributeMap.put("claims", pair.substring("claims=".length()).replaceAll("\"", "")); - } else { - String[] keyValue = pair.split("="); - - attributeMap.put(keyValue[0].replaceAll("\"", ""), keyValue[1].replaceAll("\"", "")); - } + attributeMap.put(keyValue[0], keyValue[1]); } return attributeMap; @@ -215,13 +213,18 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String error = challengeAttributes.get("error"); if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + LOGGER.verbose("The challenge response contained an error: {}", error); if ("insufficient_claims".equalsIgnoreCase(error)) { String claims = challengeAttributes.get("claims"); if (claims != null) { - tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims))); + try { + tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims), "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw LOGGER.logExceptionAsError( + new RuntimeException("Failed to decode the claims from the challenge.", e)); + } } } } @@ -338,7 +341,7 @@ public boolean authorizeRequestOnChallengeSync(HttpPipelineCallContext context, String error = challengeAttributes.get("error"); if (error != null) { - LOGGER.verbose(String.format("The challenge response contained an error: %s", error)); + LOGGER.verbose("The challenge response contained an error: {}", error); if ("insufficient_claims".equalsIgnoreCase(error)) { String claims = challengeAttributes.get("claims"); From b4072e5919b9c675cddcadc9a3fe9e487d073830 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Tue, 8 Oct 2024 17:03:50 -0700 Subject: [PATCH 13/18] Applied PR feedback to tests. --- .../KeyVaultCredentialPolicy.java | 10 +-- .../KeyVaultCredentialPolicyTest.java | 90 +++++++++++++++---- .../KeyVaultCredentialPolicy.java | 10 +-- .../KeyVaultCredentialPolicyTest.java | 90 +++++++++++++++---- .../KeyVaultCredentialPolicy.java | 10 +-- .../keys/KeyVaultCredentialPolicyTest.java | 90 +++++++++++++++---- .../KeyVaultCredentialPolicy.java | 10 +-- .../secrets/KeyVaultCredentialPolicyTest.java | 90 +++++++++++++++---- 8 files changed, 304 insertions(+), 96 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java index 7891bcd99f06a..7d8345b46232e 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java @@ -17,11 +17,11 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.Locale; @@ -219,12 +219,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String claims = challengeAttributes.get("claims"); if (claims != null) { - try { - tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims), "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw LOGGER.logExceptionAsError( - new RuntimeException("Failed to decode the claims from the challenge.", e)); - } + tokenRequestContext.setClaims( + new String(Base64Util.decodeString(claims), StandardCharsets.UTF_8)); } } } diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java index c670eb92ca39c..468034e129e41 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java @@ -3,7 +3,10 @@ package com.azure.security.keyvault.administration; +import com.azure.core.credential.AccessToken; import com.azure.core.credential.BasicAuthenticationCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpHeaders; import com.azure.core.http.HttpMethod; @@ -15,6 +18,7 @@ import com.azure.core.test.SyncAsyncExtension; import com.azure.core.test.annotation.SyncAsyncTest; import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.util.Base64Util; import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.security.keyvault.administration.implementation.KeyVaultCredentialPolicy; @@ -29,12 +33,15 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Random; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -45,7 +52,6 @@ public class KeyVaultCredentialPolicyTest { private static final String AUTHENTICATE_HEADER = "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd022db57\", " + "resource=\"https://vault.azure.net\""; - private static final String AUTHENTICATE_HEADER_WITH_CLAIMS = "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", " + "error=\"insufficient_claims\", " @@ -54,7 +60,7 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); - private BasicAuthenticationCredential credential; + private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; @@ -64,6 +70,7 @@ public class KeyVaultCredentialPolicyTest { private HttpPipelineCallContext testContext; private HttpPipelineCallContext bodyContext; private HttpPipelineCallContext bodyFluxContext; + private BasicAuthenticationCredential credential; private static HttpPipelineCallContext createCallContext(HttpRequest request, Context context) { AtomicReference callContextReference = new AtomicReference<>(); @@ -113,10 +120,10 @@ public void setup() { this.unauthorizedHttpResponseWithHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); - this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); + this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); } @AfterEach @@ -208,19 +215,32 @@ public void onAuthorizeRequestChallengeCachePresentSync() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaims() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + MutableTestCredential testCredential = new MutableTestCredential(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created this.unauthorizedHttpResponseWithHeader) - .flatMap(result -> result ? policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received - this.unauthorizedHttpResponseWithHeaderAndClaims) : Mono.just(false))) - .assertNext(result -> { - assertTrue(result); - - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); - - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + .flatMap(authorized -> { + if (authorized) { + String firstToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(firstToken.isEmpty()); + assertTrue(firstToken.startsWith(BEARER)); + + return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received + this.unauthorizedHttpResponseWithHeaderAndClaims) + .map(ignored -> firstToken); + } else { + return Mono.just(""); + } + })) + .assertNext(firstToken -> { + String newToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(newToken.isEmpty()); + assertTrue(newToken.startsWith(BEARER)); + + assertNotEquals(firstToken, newToken); }) .verifyComplete(); @@ -244,17 +264,27 @@ public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + MutableTestCredential testCredential = new MutableTestCredential(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); // Challenge cache created assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeader)); + + String firstToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(firstToken.isEmpty()); + assertTrue(firstToken.startsWith(BEARER)); + // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeaderAndClaims)); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + String newToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(newToken.isEmpty()); + assertTrue(newToken.startsWith(BEARER)); + + assertNotEquals(firstToken, newToken); KeyVaultCredentialPolicy.clearCache(); } @@ -354,14 +384,40 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); + KeyVaultCredentialPolicy.clearCache(); + return onChallenge; } private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { boolean onChallengeSync = policy.authorizeRequestOnChallengeSync(callContext, unauthorizedHttpResponse); + KeyVaultCredentialPolicy.clearCache(); + return onChallengeSync; } + + private static class MutableTestCredential implements TokenCredential { + private String credential; + + public MutableTestCredential() { + this.credential = new Random().toString(); + } + + /** + * @throws RuntimeException If the UTF-8 encoding isn't supported. + */ + @Override + public Mono getToken(TokenRequestContext request) { + if (request.isCaeEnabled() && request.getClaims() != null) { + credential = new Random().toString(); + } + + String encodedCredential = Base64Util.encodeToString(credential.getBytes(StandardCharsets.UTF_8)); + + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); + } + } } diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java index af977255e8fd9..4a101637bd045 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java @@ -17,11 +17,11 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.Locale; @@ -219,12 +219,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String claims = challengeAttributes.get("claims"); if (claims != null) { - try { - tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims), "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw LOGGER.logExceptionAsError( - new RuntimeException("Failed to decode the claims from the challenge.", e)); - } + tokenRequestContext.setClaims( + new String(Base64Util.decodeString(claims), StandardCharsets.UTF_8)); } } } diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java index 7f6f63f57c53b..4e550a51b61f9 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java @@ -3,7 +3,10 @@ package com.azure.security.keyvault.certificates; +import com.azure.core.credential.AccessToken; import com.azure.core.credential.BasicAuthenticationCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpHeaders; import com.azure.core.http.HttpMethod; @@ -15,6 +18,7 @@ import com.azure.core.test.SyncAsyncExtension; import com.azure.core.test.annotation.SyncAsyncTest; import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.util.Base64Util; import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.security.keyvault.certificates.implementation.KeyVaultCredentialPolicy; @@ -29,12 +33,15 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Random; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -45,7 +52,6 @@ public class KeyVaultCredentialPolicyTest { private static final String AUTHENTICATE_HEADER = "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd022db57\", " + "resource=\"https://vault.azure.net\""; - private static final String AUTHENTICATE_HEADER_WITH_CLAIMS = "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", " + "error=\"insufficient_claims\", " @@ -54,7 +60,7 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); - private BasicAuthenticationCredential credential; + private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; @@ -64,6 +70,7 @@ public class KeyVaultCredentialPolicyTest { private HttpPipelineCallContext testContext; private HttpPipelineCallContext bodyContext; private HttpPipelineCallContext bodyFluxContext; + private BasicAuthenticationCredential credential; private static HttpPipelineCallContext createCallContext(HttpRequest request, Context context) { AtomicReference callContextReference = new AtomicReference<>(); @@ -113,10 +120,10 @@ public void setup() { this.unauthorizedHttpResponseWithHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); - this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); + this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); } @AfterEach @@ -208,19 +215,32 @@ public void onAuthorizeRequestChallengeCachePresentSync() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaims() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + MutableTestCredential testCredential = new MutableTestCredential(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created this.unauthorizedHttpResponseWithHeader) - .flatMap(result -> result ? policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received - this.unauthorizedHttpResponseWithHeaderAndClaims) : Mono.just(false))) - .assertNext(result -> { - assertTrue(result); - - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); - - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + .flatMap(authorized -> { + if (authorized) { + String firstToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(firstToken.isEmpty()); + assertTrue(firstToken.startsWith(BEARER)); + + return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received + this.unauthorizedHttpResponseWithHeaderAndClaims) + .map(ignored -> firstToken); + } else { + return Mono.just(""); + } + })) + .assertNext(firstToken -> { + String newToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(newToken.isEmpty()); + assertTrue(newToken.startsWith(BEARER)); + + assertNotEquals(firstToken, newToken); }) .verifyComplete(); @@ -244,17 +264,27 @@ public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + MutableTestCredential testCredential = new MutableTestCredential(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); // Challenge cache created assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeader)); + + String firstToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(firstToken.isEmpty()); + assertTrue(firstToken.startsWith(BEARER)); + // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeaderAndClaims)); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + String newToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(newToken.isEmpty()); + assertTrue(newToken.startsWith(BEARER)); + + assertNotEquals(firstToken, newToken); KeyVaultCredentialPolicy.clearCache(); } @@ -354,14 +384,40 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); + KeyVaultCredentialPolicy.clearCache(); + return onChallenge; } private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { boolean onChallengeSync = policy.authorizeRequestOnChallengeSync(callContext, unauthorizedHttpResponse); + KeyVaultCredentialPolicy.clearCache(); + return onChallengeSync; } + + private static class MutableTestCredential implements TokenCredential { + private String credential; + + public MutableTestCredential() { + this.credential = new Random().toString(); + } + + /** + * @throws RuntimeException If the UTF-8 encoding isn't supported. + */ + @Override + public Mono getToken(TokenRequestContext request) { + if (request.isCaeEnabled() && request.getClaims() != null) { + credential = new Random().toString(); + } + + String encodedCredential = Base64Util.encodeToString(credential.getBytes(StandardCharsets.UTF_8)); + + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); + } + } } diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java index c26674a9a7ad2..70ec56acf04a9 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java @@ -17,11 +17,11 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.Locale; @@ -219,12 +219,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String claims = challengeAttributes.get("claims"); if (claims != null) { - try { - tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims), "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw LOGGER.logExceptionAsError( - new RuntimeException("Failed to decode the claims from the challenge.", e)); - } + tokenRequestContext.setClaims( + new String(Base64Util.decodeString(claims), StandardCharsets.UTF_8)); } } } diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java index 817f3a477b9e1..060028d256341 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java @@ -3,7 +3,10 @@ package com.azure.security.keyvault.keys; +import com.azure.core.credential.AccessToken; import com.azure.core.credential.BasicAuthenticationCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpHeaders; import com.azure.core.http.HttpMethod; @@ -15,6 +18,7 @@ import com.azure.core.test.SyncAsyncExtension; import com.azure.core.test.annotation.SyncAsyncTest; import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.util.Base64Util; import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.security.keyvault.keys.implementation.KeyVaultCredentialPolicy; @@ -29,12 +33,15 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Random; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -45,7 +52,6 @@ public class KeyVaultCredentialPolicyTest { private static final String AUTHENTICATE_HEADER = "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd022db57\", " + "resource=\"https://vault.azure.net\""; - private static final String AUTHENTICATE_HEADER_WITH_CLAIMS = "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", " + "error=\"insufficient_claims\", " @@ -54,7 +60,7 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); - private BasicAuthenticationCredential credential; + private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; @@ -64,6 +70,7 @@ public class KeyVaultCredentialPolicyTest { private HttpPipelineCallContext testContext; private HttpPipelineCallContext bodyContext; private HttpPipelineCallContext bodyFluxContext; + private BasicAuthenticationCredential credential; private static HttpPipelineCallContext createCallContext(HttpRequest request, Context context) { AtomicReference callContextReference = new AtomicReference<>(); @@ -113,10 +120,10 @@ public void setup() { this.unauthorizedHttpResponseWithHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); - this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); + this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); } @AfterEach @@ -208,19 +215,32 @@ public void onAuthorizeRequestChallengeCachePresentSync() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaims() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + MutableTestCredential testCredential = new MutableTestCredential(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created this.unauthorizedHttpResponseWithHeader) - .flatMap(result -> result ? policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received - this.unauthorizedHttpResponseWithHeaderAndClaims) : Mono.just(false))) - .assertNext(result -> { - assertTrue(result); - - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); - - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + .flatMap(authorized -> { + if (authorized) { + String firstToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(firstToken.isEmpty()); + assertTrue(firstToken.startsWith(BEARER)); + + return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received + this.unauthorizedHttpResponseWithHeaderAndClaims) + .map(ignored -> firstToken); + } else { + return Mono.just(""); + } + })) + .assertNext(firstToken -> { + String newToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(newToken.isEmpty()); + assertTrue(newToken.startsWith(BEARER)); + + assertNotEquals(firstToken, newToken); }) .verifyComplete(); @@ -244,17 +264,27 @@ public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + MutableTestCredential testCredential = new MutableTestCredential(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); // Challenge cache created assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeader)); + + String firstToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(firstToken.isEmpty()); + assertTrue(firstToken.startsWith(BEARER)); + // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeaderAndClaims)); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + String newToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(newToken.isEmpty()); + assertTrue(newToken.startsWith(BEARER)); + + assertNotEquals(firstToken, newToken); KeyVaultCredentialPolicy.clearCache(); } @@ -354,14 +384,40 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); + KeyVaultCredentialPolicy.clearCache(); + return onChallenge; } private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { boolean onChallengeSync = policy.authorizeRequestOnChallengeSync(callContext, unauthorizedHttpResponse); + KeyVaultCredentialPolicy.clearCache(); + return onChallengeSync; } + + private static class MutableTestCredential implements TokenCredential { + private String credential; + + public MutableTestCredential() { + this.credential = new Random().toString(); + } + + /** + * @throws RuntimeException If the UTF-8 encoding isn't supported. + */ + @Override + public Mono getToken(TokenRequestContext request) { + if (request.isCaeEnabled() && request.getClaims() != null) { + credential = new Random().toString(); + } + + String encodedCredential = Base64Util.encodeToString(credential.getBytes(StandardCharsets.UTF_8)); + + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); + } + } } diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index fdff53103a8a4..5c7155e05632e 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -17,11 +17,11 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.Locale; @@ -219,12 +219,8 @@ public Mono authorizeRequestOnChallenge(HttpPipelineCallContext context String claims = challengeAttributes.get("claims"); if (claims != null) { - try { - tokenRequestContext.setClaims(new String(Base64Util.decodeString(claims), "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw LOGGER.logExceptionAsError( - new RuntimeException("Failed to decode the claims from the challenge.", e)); - } + tokenRequestContext.setClaims( + new String(Base64Util.decodeString(claims), StandardCharsets.UTF_8)); } } } diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java index 236fc67739efb..51877a27076ce 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java @@ -3,7 +3,10 @@ package com.azure.security.keyvault.secrets; +import com.azure.core.credential.AccessToken; import com.azure.core.credential.BasicAuthenticationCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpHeaderName; import com.azure.core.http.HttpHeaders; import com.azure.core.http.HttpMethod; @@ -15,6 +18,7 @@ import com.azure.core.test.SyncAsyncExtension; import com.azure.core.test.annotation.SyncAsyncTest; import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.util.Base64Util; import com.azure.core.util.BinaryData; import com.azure.core.util.Context; import com.azure.security.keyvault.secrets.implementation.KeyVaultCredentialPolicy; @@ -29,12 +33,15 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Random; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -45,7 +52,6 @@ public class KeyVaultCredentialPolicyTest { private static final String AUTHENTICATE_HEADER = "Bearer authorization=\"https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd022db57\", " + "resource=\"https://vault.azure.net\""; - private static final String AUTHENTICATE_HEADER_WITH_CLAIMS = "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", " + "error=\"insufficient_claims\", " @@ -54,7 +60,7 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); - private BasicAuthenticationCredential credential; + private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; @@ -64,6 +70,7 @@ public class KeyVaultCredentialPolicyTest { private HttpPipelineCallContext testContext; private HttpPipelineCallContext bodyContext; private HttpPipelineCallContext bodyFluxContext; + private BasicAuthenticationCredential credential; private static HttpPipelineCallContext createCallContext(HttpRequest request, Context context) { AtomicReference callContextReference = new AtomicReference<>(); @@ -113,10 +120,10 @@ public void setup() { this.unauthorizedHttpResponseWithHeaderAndClaims = unauthorizedResponseWithHeaderAndClaims; this.callContext = createCallContext(request, Context.NONE); this.differentScopeContext = createCallContext(requestWithDifferentScope, Context.NONE); - this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); + this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); } @AfterEach @@ -208,19 +215,32 @@ public void onAuthorizeRequestChallengeCachePresentSync() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaims() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + MutableTestCredential testCredential = new MutableTestCredential(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created this.unauthorizedHttpResponseWithHeader) - .flatMap(result -> result ? policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received - this.unauthorizedHttpResponseWithHeaderAndClaims) : Mono.just(false))) - .assertNext(result -> { - assertTrue(result); - - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); - - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + .flatMap(authorized -> { + if (authorized) { + String firstToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(firstToken.isEmpty()); + assertTrue(firstToken.startsWith(BEARER)); + + return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received + this.unauthorizedHttpResponseWithHeaderAndClaims) + .map(ignored -> firstToken); + } else { + return Mono.just(""); + } + })) + .assertNext(firstToken -> { + String newToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(newToken.isEmpty()); + assertTrue(newToken.startsWith(BEARER)); + + assertNotEquals(firstToken, newToken); }) .verifyComplete(); @@ -244,17 +264,27 @@ public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { - KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(this.credential, false); + MutableTestCredential testCredential = new MutableTestCredential(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); // Challenge cache created assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeader)); + + String firstToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(firstToken.isEmpty()); + assertTrue(firstToken.startsWith(BEARER)); + // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeaderAndClaims)); - String tokenValue = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); - assertFalse(tokenValue.isEmpty()); - assertTrue(tokenValue.startsWith(BEARER)); + String newToken = this.testContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + assertFalse(newToken.isEmpty()); + assertTrue(newToken.startsWith(BEARER)); + + assertNotEquals(firstToken, newToken); KeyVaultCredentialPolicy.clearCache(); } @@ -354,14 +384,40 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); + KeyVaultCredentialPolicy.clearCache(); + return onChallenge; } private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { boolean onChallengeSync = policy.authorizeRequestOnChallengeSync(callContext, unauthorizedHttpResponse); + KeyVaultCredentialPolicy.clearCache(); + return onChallengeSync; } + + private static class MutableTestCredential implements TokenCredential { + private String credential; + + public MutableTestCredential() { + this.credential = new Random().toString(); + } + + /** + * @throws RuntimeException If the UTF-8 encoding isn't supported. + */ + @Override + public Mono getToken(TokenRequestContext request) { + if (request.isCaeEnabled() && request.getClaims() != null) { + credential = new Random().toString(); + } + + String encodedCredential = Base64Util.encodeToString(credential.getBytes(StandardCharsets.UTF_8)); + + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); + } + } } From 01f0ded682c2c822b64b5eef396cd7fb3e396314 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Mon, 14 Oct 2024 12:08:57 -0700 Subject: [PATCH 14/18] Applied more feedback and added additional tests. --- .../KeyVaultCredentialPolicy.java | 14 +- .../KeyVaultCredentialPolicyTest.java | 216 +++++++++++++++++- .../KeyVaultCredentialPolicy.java | 14 +- .../KeyVaultCredentialPolicyTest.java | 216 +++++++++++++++++- .../KeyVaultCredentialPolicy.java | 14 +- .../keys/KeyVaultCredentialPolicyTest.java | 216 +++++++++++++++++- .../KeyVaultCredentialPolicy.java | 14 +- .../secrets/KeyVaultCredentialPolicyTest.java | 216 +++++++++++++++++- 8 files changed, 864 insertions(+), 56 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java index 7d8345b46232e..497d28689f7f5 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java @@ -405,10 +405,9 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http return next.process().flatMap(newResponse -> { String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (newResponse.getStatusCode() == 401 && authHeader != null - && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { - - return handleChallenge(context, newResponse, nextPolicy); + if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) + && !isClaimsPresent(httpResponse)) { + return handleChallenge(context, newResponse, nextPolicy); } else { return Mono.just(newResponse); } @@ -429,10 +428,9 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe HttpResponse newResponse = next.processSync(); String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (newResponse.getStatusCode() == 401 && authHeader != null - && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { - - return handleChallengeSync(context, newResponse, nextPolicy); + if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) + && !isClaimsPresent(httpResponse)) { + return handleChallengeSync(context, newResponse, nextPolicy); } return newResponse; diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java index 468034e129e41..113b13ad88fb3 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java @@ -34,11 +34,16 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.stream.Stream; import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; +import static com.azure.core.util.CoreUtils.isNullOrEmpty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -60,7 +65,12 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); + private static final List> BASE_ASSERTIONS = List.of( + tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), + tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), + TokenRequestContext::isCaeEnabled); + private HttpResponse simpleResponse; private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; @@ -100,6 +110,9 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); + MockHttpResponse simpleResponse = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 200); + MockHttpResponse unauthorizedResponseWithWrongStatusCode = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); @@ -114,6 +127,7 @@ public void setup() { new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); + this.simpleResponse = simpleResponse; this.unauthorizedHttpResponseWithWrongStatusCode = unauthorizedResponseWithWrongStatusCode; this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; @@ -215,7 +229,8 @@ public void onAuthorizeRequestChallengeCachePresentSync() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaims() { - MutableTestCredential testCredential = new MutableTestCredential(); + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created @@ -227,6 +242,9 @@ public void onAuthorizeRequestChallengeCachePresentWithClaims() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); + testCredential.replaceAssertion(tokenRequestContext -> + tokenRequestContext.getClaims() != null, 3); + return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received this.unauthorizedHttpResponseWithHeaderAndClaims) .map(ignored -> firstToken); @@ -264,7 +282,8 @@ public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { - MutableTestCredential testCredential = new MutableTestCredential(); + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); // Challenge cache created @@ -275,6 +294,8 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeaderAndClaims)); @@ -381,6 +402,160 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { assertTrue(onChallenge); } + // Normal flow: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 200 OK + @SyncAsyncTest + public void processMultipleResponses() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + simpleResponse + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request. + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + // On receiving an unauthorized response with claims, the token should be updated and a new attempt to make the + // original request should be made. + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the response with claims and set on the request. + assertNotNull(newToken); + // The token was updated. + assertNotEquals(firstToken, newToken); + // A subsequent request was successful. + assertEquals(simpleResponse, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + + // Edge case: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 401 Unauthorized with claims + @SyncAsyncTest + public void processConsecutiveResponsesWithClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + // If a second consecutive unauthorized response with claims is received, it shall be returned as is. + unauthorizedHttpResponseWithHeaderAndClaims, + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the first response with claims and set on the request + assertNotEquals(firstToken, newToken); + // A subsequent request was unsuccessful. + assertEquals(unauthorizedHttpResponseWithHeaderAndClaims, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + + // Edge case: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 401 Unauthorized + @SyncAsyncTest + public void process401WithoutClaimsAfter401WithClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + // If a second consecutive unauthorized response is received, it shall be returned as is. + unauthorizedHttpResponseWithHeader + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the first response with claims and set on the request + assertNotEquals(firstToken, newToken); + // A subsequent request was unsuccessful. + assertEquals(unauthorizedHttpResponseWithHeader, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); @@ -401,23 +576,52 @@ private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, Ht private static class MutableTestCredential implements TokenCredential { private String credential; + private List> assertions; - public MutableTestCredential() { + public MutableTestCredential(List> assertions) { this.credential = new Random().toString(); + this.assertions = assertions; } /** - * @throws RuntimeException If the UTF-8 encoding isn't supported. + * @throws RuntimeException if any of the assertions fail. */ @Override - public Mono getToken(TokenRequestContext request) { - if (request.isCaeEnabled() && request.getClaims() != null) { + public Mono getToken(TokenRequestContext requestContext) { + if (requestContext.isCaeEnabled() && requestContext.getClaims() != null) { + assertEquals("{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"cp1\"}}}", + requestContext.getClaims()); + credential = new Random().toString(); } String encodedCredential = Base64Util.encodeToString(credential.getBytes(StandardCharsets.UTF_8)); + for (int i = 0; i < assertions.size(); i++) { + if (!assertions.get(i).apply(requestContext)) { + return Mono.error(new RuntimeException(String.format("Assertion number %d failed", i))); + } + } + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); } + + public MutableTestCredential setAssertions(List> assertions) { + this.assertions = assertions; + + return this; + } + + public MutableTestCredential addAssertion(Function assertion) { + assertions.add(assertion); + + return this; + } + + public MutableTestCredential replaceAssertion(Function assertion, int index) { + assertions.set(index, assertion); + + return this; + } } } diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java index 4a101637bd045..328c1d8c30e1f 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java @@ -405,10 +405,9 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http return next.process().flatMap(newResponse -> { String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (newResponse.getStatusCode() == 401 && authHeader != null - && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { - - return handleChallenge(context, newResponse, nextPolicy); + if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) + && !isClaimsPresent(httpResponse)) { + return handleChallenge(context, newResponse, nextPolicy); } else { return Mono.just(newResponse); } @@ -429,10 +428,9 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe HttpResponse newResponse = next.processSync(); String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (newResponse.getStatusCode() == 401 && authHeader != null - && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { - - return handleChallengeSync(context, newResponse, nextPolicy); + if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) + && !isClaimsPresent(httpResponse)) { + return handleChallengeSync(context, newResponse, nextPolicy); } return newResponse; diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java index 4e550a51b61f9..ba12cfc7d0aea 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java @@ -34,11 +34,16 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.stream.Stream; import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; +import static com.azure.core.util.CoreUtils.isNullOrEmpty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -60,7 +65,12 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); + private static final List> BASE_ASSERTIONS = List.of( + tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), + tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), + TokenRequestContext::isCaeEnabled); + private HttpResponse simpleResponse; private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; @@ -100,6 +110,9 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); + MockHttpResponse simpleResponse = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 200); + MockHttpResponse unauthorizedResponseWithWrongStatusCode = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); @@ -114,6 +127,7 @@ public void setup() { new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); + this.simpleResponse = simpleResponse; this.unauthorizedHttpResponseWithWrongStatusCode = unauthorizedResponseWithWrongStatusCode; this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; @@ -215,7 +229,8 @@ public void onAuthorizeRequestChallengeCachePresentSync() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaims() { - MutableTestCredential testCredential = new MutableTestCredential(); + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created @@ -227,6 +242,9 @@ public void onAuthorizeRequestChallengeCachePresentWithClaims() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); + testCredential.replaceAssertion(tokenRequestContext -> + tokenRequestContext.getClaims() != null, 3); + return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received this.unauthorizedHttpResponseWithHeaderAndClaims) .map(ignored -> firstToken); @@ -264,7 +282,8 @@ public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { - MutableTestCredential testCredential = new MutableTestCredential(); + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); // Challenge cache created @@ -275,6 +294,8 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeaderAndClaims)); @@ -381,6 +402,160 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { assertTrue(onChallenge); } + // Normal flow: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 200 OK + @SyncAsyncTest + public void processMultipleResponses() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + simpleResponse + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request. + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + // On receiving an unauthorized response with claims, the token should be updated and a new attempt to make the + // original request should be made. + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the response with claims and set on the request. + assertNotNull(newToken); + // The token was updated. + assertNotEquals(firstToken, newToken); + // A subsequent request was successful. + assertEquals(simpleResponse, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + + // Edge case: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 401 Unauthorized with claims + @SyncAsyncTest + public void processConsecutiveResponsesWithClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + // If a second consecutive unauthorized response with claims is received, it shall be returned as is. + unauthorizedHttpResponseWithHeaderAndClaims, + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the first response with claims and set on the request + assertNotEquals(firstToken, newToken); + // A subsequent request was unsuccessful. + assertEquals(unauthorizedHttpResponseWithHeaderAndClaims, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + + // Edge case: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 401 Unauthorized + @SyncAsyncTest + public void process401WithoutClaimsAfter401WithClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + // If a second consecutive unauthorized response is received, it shall be returned as is. + unauthorizedHttpResponseWithHeader + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the first response with claims and set on the request + assertNotEquals(firstToken, newToken); + // A subsequent request was unsuccessful. + assertEquals(unauthorizedHttpResponseWithHeader, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); @@ -401,23 +576,52 @@ private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, Ht private static class MutableTestCredential implements TokenCredential { private String credential; + private List> assertions; - public MutableTestCredential() { + public MutableTestCredential(List> assertions) { this.credential = new Random().toString(); + this.assertions = assertions; } /** - * @throws RuntimeException If the UTF-8 encoding isn't supported. + * @throws RuntimeException if any of the assertions fail. */ @Override - public Mono getToken(TokenRequestContext request) { - if (request.isCaeEnabled() && request.getClaims() != null) { + public Mono getToken(TokenRequestContext requestContext) { + if (requestContext.isCaeEnabled() && requestContext.getClaims() != null) { + assertEquals("{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"cp1\"}}}", + requestContext.getClaims()); + credential = new Random().toString(); } String encodedCredential = Base64Util.encodeToString(credential.getBytes(StandardCharsets.UTF_8)); + for (int i = 0; i < assertions.size(); i++) { + if (!assertions.get(i).apply(requestContext)) { + return Mono.error(new RuntimeException(String.format("Assertion number %d failed", i))); + } + } + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); } + + public MutableTestCredential setAssertions(List> assertions) { + this.assertions = assertions; + + return this; + } + + public MutableTestCredential addAssertion(Function assertion) { + assertions.add(assertion); + + return this; + } + + public MutableTestCredential replaceAssertion(Function assertion, int index) { + assertions.set(index, assertion); + + return this; + } } } diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java index 70ec56acf04a9..165b8ec7a93e9 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java @@ -405,10 +405,9 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http return next.process().flatMap(newResponse -> { String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (newResponse.getStatusCode() == 401 && authHeader != null - && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { - - return handleChallenge(context, newResponse, nextPolicy); + if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) + && !isClaimsPresent(httpResponse)) { + return handleChallenge(context, newResponse, nextPolicy); } else { return Mono.just(newResponse); } @@ -429,10 +428,9 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe HttpResponse newResponse = next.processSync(); String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (newResponse.getStatusCode() == 401 && authHeader != null - && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { - - return handleChallengeSync(context, newResponse, nextPolicy); + if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) + && !isClaimsPresent(httpResponse)) { + return handleChallengeSync(context, newResponse, nextPolicy); } return newResponse; diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java index 060028d256341..98616f2249034 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java @@ -34,11 +34,16 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.stream.Stream; import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; +import static com.azure.core.util.CoreUtils.isNullOrEmpty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -60,7 +65,12 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); + private static final List> BASE_ASSERTIONS = List.of( + tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), + tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), + TokenRequestContext::isCaeEnabled); + private HttpResponse simpleResponse; private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; @@ -100,6 +110,9 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); + MockHttpResponse simpleResponse = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 200); + MockHttpResponse unauthorizedResponseWithWrongStatusCode = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); @@ -114,6 +127,7 @@ public void setup() { new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); + this.simpleResponse = simpleResponse; this.unauthorizedHttpResponseWithWrongStatusCode = unauthorizedResponseWithWrongStatusCode; this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; @@ -215,7 +229,8 @@ public void onAuthorizeRequestChallengeCachePresentSync() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaims() { - MutableTestCredential testCredential = new MutableTestCredential(); + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created @@ -227,6 +242,9 @@ public void onAuthorizeRequestChallengeCachePresentWithClaims() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); + testCredential.replaceAssertion(tokenRequestContext -> + tokenRequestContext.getClaims() != null, 3); + return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received this.unauthorizedHttpResponseWithHeaderAndClaims) .map(ignored -> firstToken); @@ -264,7 +282,8 @@ public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { - MutableTestCredential testCredential = new MutableTestCredential(); + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); // Challenge cache created @@ -275,6 +294,8 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeaderAndClaims)); @@ -381,6 +402,160 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { assertTrue(onChallenge); } + // Normal flow: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 200 OK + @SyncAsyncTest + public void processMultipleResponses() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + simpleResponse + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request. + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + // On receiving an unauthorized response with claims, the token should be updated and a new attempt to make the + // original request should be made. + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the response with claims and set on the request. + assertNotNull(newToken); + // The token was updated. + assertNotEquals(firstToken, newToken); + // A subsequent request was successful. + assertEquals(simpleResponse, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + + // Edge case: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 401 Unauthorized with claims + @SyncAsyncTest + public void processConsecutiveResponsesWithClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + // If a second consecutive unauthorized response with claims is received, it shall be returned as is. + unauthorizedHttpResponseWithHeaderAndClaims, + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the first response with claims and set on the request + assertNotEquals(firstToken, newToken); + // A subsequent request was unsuccessful. + assertEquals(unauthorizedHttpResponseWithHeaderAndClaims, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + + // Edge case: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 401 Unauthorized + @SyncAsyncTest + public void process401WithoutClaimsAfter401WithClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + // If a second consecutive unauthorized response is received, it shall be returned as is. + unauthorizedHttpResponseWithHeader + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the first response with claims and set on the request + assertNotEquals(firstToken, newToken); + // A subsequent request was unsuccessful. + assertEquals(unauthorizedHttpResponseWithHeader, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); @@ -401,23 +576,52 @@ private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, Ht private static class MutableTestCredential implements TokenCredential { private String credential; + private List> assertions; - public MutableTestCredential() { + public MutableTestCredential(List> assertions) { this.credential = new Random().toString(); + this.assertions = assertions; } /** - * @throws RuntimeException If the UTF-8 encoding isn't supported. + * @throws RuntimeException if any of the assertions fail. */ @Override - public Mono getToken(TokenRequestContext request) { - if (request.isCaeEnabled() && request.getClaims() != null) { + public Mono getToken(TokenRequestContext requestContext) { + if (requestContext.isCaeEnabled() && requestContext.getClaims() != null) { + assertEquals("{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"cp1\"}}}", + requestContext.getClaims()); + credential = new Random().toString(); } String encodedCredential = Base64Util.encodeToString(credential.getBytes(StandardCharsets.UTF_8)); + for (int i = 0; i < assertions.size(); i++) { + if (!assertions.get(i).apply(requestContext)) { + return Mono.error(new RuntimeException(String.format("Assertion number %d failed", i))); + } + } + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); } + + public MutableTestCredential setAssertions(List> assertions) { + this.assertions = assertions; + + return this; + } + + public MutableTestCredential addAssertion(Function assertion) { + assertions.add(assertion); + + return this; + } + + public MutableTestCredential replaceAssertion(Function assertion, int index) { + assertions.set(index, assertion); + + return this; + } } } diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index 5c7155e05632e..8ead5004cbade 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -405,10 +405,9 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http return next.process().flatMap(newResponse -> { String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (newResponse.getStatusCode() == 401 && authHeader != null - && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { - - return handleChallenge(context, newResponse, nextPolicy); + if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) + && !isClaimsPresent(httpResponse)) { + return handleChallenge(context, newResponse, nextPolicy); } else { return Mono.just(newResponse); } @@ -429,10 +428,9 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe HttpResponse newResponse = next.processSync(); String authHeader = newResponse.getHeaderValue(WWW_AUTHENTICATE); - if (newResponse.getStatusCode() == 401 && authHeader != null - && (isClaimsPresent(httpResponse) ^ isClaimsPresent(newResponse))) { - - return handleChallengeSync(context, newResponse, nextPolicy); + if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) + && !isClaimsPresent(httpResponse)) { + return handleChallengeSync(context, newResponse, nextPolicy); } return newResponse; diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java index 51877a27076ce..e71a3ffcb74eb 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java @@ -34,11 +34,16 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import java.util.stream.Stream; import static com.azure.core.http.HttpHeaderName.AUTHORIZATION; +import static com.azure.core.util.CoreUtils.isNullOrEmpty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -60,7 +65,12 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); + private static final List> BASE_ASSERTIONS = List.of( + tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), + tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), + TokenRequestContext::isCaeEnabled); + private HttpResponse simpleResponse; private HttpResponse unauthorizedHttpResponseWithWrongStatusCode; private HttpResponse unauthorizedHttpResponseWithHeader; private HttpResponse unauthorizedHttpResponseWithoutHeader; @@ -100,6 +110,9 @@ public void setup() { Context bodyFluxContextContext = new Context("KeyVaultCredentialPolicyStashedBody", BODY_FLUX) .addData("KeyVaultCredentialPolicyStashedContentLength", "21"); + MockHttpResponse simpleResponse = new MockHttpResponse( + new HttpRequest(HttpMethod.GET, "https://azure.com"), 200); + MockHttpResponse unauthorizedResponseWithWrongStatusCode = new MockHttpResponse( new HttpRequest(HttpMethod.GET, "https://azure.com"), 500); @@ -114,6 +127,7 @@ public void setup() { new HttpRequest(HttpMethod.GET, "https://azure.com"), 401, new HttpHeaders().set(HttpHeaderName.WWW_AUTHENTICATE, AUTHENTICATE_HEADER_WITH_CLAIMS)); + this.simpleResponse = simpleResponse; this.unauthorizedHttpResponseWithWrongStatusCode = unauthorizedResponseWithWrongStatusCode; this.unauthorizedHttpResponseWithHeader = unauthorizedResponseWithHeader; this.unauthorizedHttpResponseWithoutHeader = unauthorizedResponseWithoutHeader; @@ -215,7 +229,8 @@ public void onAuthorizeRequestChallengeCachePresentSync() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaims() { - MutableTestCredential testCredential = new MutableTestCredential(); + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); StepVerifier.create(policy.authorizeRequestOnChallenge(this.callContext, // Challenge cache created @@ -227,6 +242,9 @@ public void onAuthorizeRequestChallengeCachePresentWithClaims() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); + testCredential.replaceAssertion(tokenRequestContext -> + tokenRequestContext.getClaims() != null, 3); + return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received this.unauthorizedHttpResponseWithHeaderAndClaims) .map(ignored -> firstToken); @@ -264,7 +282,8 @@ public void onAuthorizeRequestChallengeNoCachePresentWithClaims() { @Test public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { - MutableTestCredential testCredential = new MutableTestCredential(); + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); // Challenge cache created @@ -275,6 +294,8 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, this.unauthorizedHttpResponseWithHeaderAndClaims)); @@ -381,6 +402,160 @@ public void onAuthorizeRequestDifferentScopeVerifyFalse() { assertTrue(onChallenge); } + // Normal flow: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 200 OK + @SyncAsyncTest + public void processMultipleResponses() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + simpleResponse + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request. + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + // On receiving an unauthorized response with claims, the token should be updated and a new attempt to make the + // original request should be made. + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the response with claims and set on the request. + assertNotNull(newToken); + // The token was updated. + assertNotEquals(firstToken, newToken); + // A subsequent request was successful. + assertEquals(simpleResponse, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + + // Edge case: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 401 Unauthorized with claims + @SyncAsyncTest + public void processConsecutiveResponsesWithClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + // If a second consecutive unauthorized response with claims is received, it shall be returned as is. + unauthorizedHttpResponseWithHeaderAndClaims, + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the first response with claims and set on the request + assertNotEquals(firstToken, newToken); + // A subsequent request was unsuccessful. + assertEquals(unauthorizedHttpResponseWithHeaderAndClaims, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + + // Edge case: 401 Unauthorized -> 200 OK -> 401 Unauthorized with claims -> 401 Unauthorized + @SyncAsyncTest + public void process401WithoutClaimsAfter401WithClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)) + .addAssertion(tokenRequestContext -> tokenRequestContext.getClaims() == null); + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + simpleResponse, + unauthorizedHttpResponseWithHeaderAndClaims, + // If a second consecutive unauthorized response is received, it shall be returned as is. + unauthorizedHttpResponseWithHeader + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String firstToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first response was unauthorized and a token was set on the request + assertNotNull(firstToken); + // On a second attempt, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + + HttpResponse newResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // A new token was fetched using the first response with claims and set on the request + assertNotEquals(firstToken, newToken); + // A subsequent request was unsuccessful. + assertEquals(unauthorizedHttpResponseWithHeader, newResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); @@ -401,23 +576,52 @@ private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, Ht private static class MutableTestCredential implements TokenCredential { private String credential; + private List> assertions; - public MutableTestCredential() { + public MutableTestCredential(List> assertions) { this.credential = new Random().toString(); + this.assertions = assertions; } /** - * @throws RuntimeException If the UTF-8 encoding isn't supported. + * @throws RuntimeException if any of the assertions fail. */ @Override - public Mono getToken(TokenRequestContext request) { - if (request.isCaeEnabled() && request.getClaims() != null) { + public Mono getToken(TokenRequestContext requestContext) { + if (requestContext.isCaeEnabled() && requestContext.getClaims() != null) { + assertEquals("{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"cp1\"}}}", + requestContext.getClaims()); + credential = new Random().toString(); } String encodedCredential = Base64Util.encodeToString(credential.getBytes(StandardCharsets.UTF_8)); + for (int i = 0; i < assertions.size(); i++) { + if (!assertions.get(i).apply(requestContext)) { + return Mono.error(new RuntimeException(String.format("Assertion number %d failed", i))); + } + } + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); } + + public MutableTestCredential setAssertions(List> assertions) { + this.assertions = assertions; + + return this; + } + + public MutableTestCredential addAssertion(Function assertion) { + assertions.add(assertion); + + return this; + } + + public MutableTestCredential replaceAssertion(Function assertion, int index) { + assertions.set(index, assertion); + + return this; + } } } From f11c43935c03d8733db46ea250034c8a45e0c1ee Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Mon, 14 Oct 2024 15:27:24 -0700 Subject: [PATCH 15/18] Added one more test. --- .../KeyVaultCredentialPolicyTest.java | 73 +++++++++++++++++-- .../KeyVaultCredentialPolicyTest.java | 73 +++++++++++++++++-- .../keys/KeyVaultCredentialPolicyTest.java | 73 +++++++++++++++++-- .../secrets/KeyVaultCredentialPolicyTest.java | 73 +++++++++++++++++-- 4 files changed, 260 insertions(+), 32 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java index 113b13ad88fb3..72f4bceaa4de8 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java @@ -61,6 +61,7 @@ public class KeyVaultCredentialPolicyTest { "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/authorize\", " + "error=\"insufficient_claims\", " + "claims=\"eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlIjoiY3AxIn19fQ==\""; + private static final String DECODED_CLAIMS = "{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"cp1\"}}}"; private static final String BEARER = "Bearer"; private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> @@ -243,7 +244,7 @@ public void onAuthorizeRequestChallengeCachePresentWithClaims() { assertTrue(firstToken.startsWith(BEARER)); testCredential.replaceAssertion(tokenRequestContext -> - tokenRequestContext.getClaims() != null, 3); + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received this.unauthorizedHttpResponseWithHeaderAndClaims) @@ -294,7 +295,8 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, @@ -435,7 +437,8 @@ public void processMultipleResponses() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); // On receiving an unauthorized response with claims, the token should be updated and a new attempt to make the // original request should be made. @@ -490,7 +493,8 @@ public void processConsecutiveResponsesWithClaims() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); HttpResponse newResponse = SyncAsyncExtension.execute( () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), @@ -540,7 +544,8 @@ public void process401WithoutClaimsAfter401WithClaims() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); HttpResponse newResponse = SyncAsyncExtension.execute( () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), @@ -556,6 +561,57 @@ public void process401WithoutClaimsAfter401WithClaims() { KeyVaultCredentialPolicy.clearCache(); } + // Edge case: 401 Unauthorized -> 401 Unauthorized with claims -> 200 OK + @SyncAsyncTest + public void process401WithClaimsAfter401WithoutClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)); + final String[] firstToken = new String[1]; + + testCredential.addAssertion(tokenRequestContext -> { + // This will ensure that that the first request does not contains claims, but the second does after + // receiving a 401 response with a challenge with claims. + testCredential.replaceAssertion(anotherTokenRequestContext -> + DECODED_CLAIMS.equals(anotherTokenRequestContext.getClaims()), 3); + + // We will also store the value of the first credential before it changes on a second call + firstToken[0] = Base64Util.encodeToString(testCredential.getCredential().getBytes(StandardCharsets.UTF_8)); + + assertNotNull(firstToken[0]); + + return tokenRequestContext.getClaims() == null; + }); + + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + unauthorizedHttpResponseWithHeaderAndClaims, + simpleResponse + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first unauthorized response caused a token to be set on the request, then the token was updated on a + // subsequent unauthorized response with claims. + assertNotEquals(firstToken[0], newToken); + // Finally, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); @@ -589,9 +645,6 @@ public MutableTestCredential(List> assert @Override public Mono getToken(TokenRequestContext requestContext) { if (requestContext.isCaeEnabled() && requestContext.getClaims() != null) { - assertEquals("{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"cp1\"}}}", - requestContext.getClaims()); - credential = new Random().toString(); } @@ -623,5 +676,9 @@ public MutableTestCredential replaceAssertion(Function BODY_FLUX = Flux.defer(() -> @@ -243,7 +244,7 @@ public void onAuthorizeRequestChallengeCachePresentWithClaims() { assertTrue(firstToken.startsWith(BEARER)); testCredential.replaceAssertion(tokenRequestContext -> - tokenRequestContext.getClaims() != null, 3); + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received this.unauthorizedHttpResponseWithHeaderAndClaims) @@ -294,7 +295,8 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, @@ -435,7 +437,8 @@ public void processMultipleResponses() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); // On receiving an unauthorized response with claims, the token should be updated and a new attempt to make the // original request should be made. @@ -490,7 +493,8 @@ public void processConsecutiveResponsesWithClaims() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); HttpResponse newResponse = SyncAsyncExtension.execute( () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), @@ -540,7 +544,8 @@ public void process401WithoutClaimsAfter401WithClaims() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); HttpResponse newResponse = SyncAsyncExtension.execute( () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), @@ -556,6 +561,57 @@ public void process401WithoutClaimsAfter401WithClaims() { KeyVaultCredentialPolicy.clearCache(); } + // Edge case: 401 Unauthorized -> 401 Unauthorized with claims -> 200 OK + @SyncAsyncTest + public void process401WithClaimsAfter401WithoutClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)); + final String[] firstToken = new String[1]; + + testCredential.addAssertion(tokenRequestContext -> { + // This will ensure that that the first request does not contains claims, but the second does after + // receiving a 401 response with a challenge with claims. + testCredential.replaceAssertion(anotherTokenRequestContext -> + DECODED_CLAIMS.equals(anotherTokenRequestContext.getClaims()), 3); + + // We will also store the value of the first credential before it changes on a second call + firstToken[0] = Base64Util.encodeToString(testCredential.getCredential().getBytes(StandardCharsets.UTF_8)); + + assertNotNull(firstToken[0]); + + return tokenRequestContext.getClaims() == null; + }); + + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + unauthorizedHttpResponseWithHeaderAndClaims, + simpleResponse + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first unauthorized response caused a token to be set on the request, then the token was updated on a + // subsequent unauthorized response with claims. + assertNotEquals(firstToken[0], newToken); + // Finally, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); @@ -589,9 +645,6 @@ public MutableTestCredential(List> assert @Override public Mono getToken(TokenRequestContext requestContext) { if (requestContext.isCaeEnabled() && requestContext.getClaims() != null) { - assertEquals("{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"cp1\"}}}", - requestContext.getClaims()); - credential = new Random().toString(); } @@ -623,5 +676,9 @@ public MutableTestCredential replaceAssertion(Function BODY_FLUX = Flux.defer(() -> @@ -243,7 +244,7 @@ public void onAuthorizeRequestChallengeCachePresentWithClaims() { assertTrue(firstToken.startsWith(BEARER)); testCredential.replaceAssertion(tokenRequestContext -> - tokenRequestContext.getClaims() != null, 3); + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received this.unauthorizedHttpResponseWithHeaderAndClaims) @@ -294,7 +295,8 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, @@ -435,7 +437,8 @@ public void processMultipleResponses() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); // On receiving an unauthorized response with claims, the token should be updated and a new attempt to make the // original request should be made. @@ -490,7 +493,8 @@ public void processConsecutiveResponsesWithClaims() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); HttpResponse newResponse = SyncAsyncExtension.execute( () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), @@ -540,7 +544,8 @@ public void process401WithoutClaimsAfter401WithClaims() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); HttpResponse newResponse = SyncAsyncExtension.execute( () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), @@ -556,6 +561,57 @@ public void process401WithoutClaimsAfter401WithClaims() { KeyVaultCredentialPolicy.clearCache(); } + // Edge case: 401 Unauthorized -> 401 Unauthorized with claims -> 200 OK + @SyncAsyncTest + public void process401WithClaimsAfter401WithoutClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)); + final String[] firstToken = new String[1]; + + testCredential.addAssertion(tokenRequestContext -> { + // This will ensure that that the first request does not contains claims, but the second does after + // receiving a 401 response with a challenge with claims. + testCredential.replaceAssertion(anotherTokenRequestContext -> + DECODED_CLAIMS.equals(anotherTokenRequestContext.getClaims()), 3); + + // We will also store the value of the first credential before it changes on a second call + firstToken[0] = Base64Util.encodeToString(testCredential.getCredential().getBytes(StandardCharsets.UTF_8)); + + assertNotNull(firstToken[0]); + + return tokenRequestContext.getClaims() == null; + }); + + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + unauthorizedHttpResponseWithHeaderAndClaims, + simpleResponse + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first unauthorized response caused a token to be set on the request, then the token was updated on a + // subsequent unauthorized response with claims. + assertNotEquals(firstToken[0], newToken); + // Finally, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); @@ -589,9 +645,6 @@ public MutableTestCredential(List> assert @Override public Mono getToken(TokenRequestContext requestContext) { if (requestContext.isCaeEnabled() && requestContext.getClaims() != null) { - assertEquals("{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"cp1\"}}}", - requestContext.getClaims()); - credential = new Random().toString(); } @@ -623,5 +676,9 @@ public MutableTestCredential replaceAssertion(Function BODY_FLUX = Flux.defer(() -> @@ -243,7 +244,7 @@ public void onAuthorizeRequestChallengeCachePresentWithClaims() { assertTrue(firstToken.startsWith(BEARER)); testCredential.replaceAssertion(tokenRequestContext -> - tokenRequestContext.getClaims() != null, 3); + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); return policy.authorizeRequestOnChallenge(this.callContext, // Challenge with claims received this.unauthorizedHttpResponseWithHeaderAndClaims) @@ -294,7 +295,8 @@ public void onAuthorizeRequestChallengeCachePresentWithClaimsSync() { assertFalse(firstToken.isEmpty()); assertTrue(firstToken.startsWith(BEARER)); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); // Challenge with claims received assertTrue(policy.authorizeRequestOnChallengeSync(this.callContext, @@ -435,7 +437,8 @@ public void processMultipleResponses() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); // On receiving an unauthorized response with claims, the token should be updated and a new attempt to make the // original request should be made. @@ -490,7 +493,8 @@ public void processConsecutiveResponsesWithClaims() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); HttpResponse newResponse = SyncAsyncExtension.execute( () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), @@ -540,7 +544,8 @@ public void process401WithoutClaimsAfter401WithClaims() { // On a second attempt, a successful response was received. assertEquals(simpleResponse, firstResponse); - testCredential.replaceAssertion(tokenRequestContext -> tokenRequestContext.getClaims() != null, 3); + testCredential.replaceAssertion(tokenRequestContext -> + DECODED_CLAIMS.equals(tokenRequestContext.getClaims()), 3); HttpResponse newResponse = SyncAsyncExtension.execute( () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), @@ -556,6 +561,57 @@ public void process401WithoutClaimsAfter401WithClaims() { KeyVaultCredentialPolicy.clearCache(); } + // Edge case: 401 Unauthorized -> 401 Unauthorized with claims -> 200 OK + @SyncAsyncTest + public void process401WithClaimsAfter401WithoutClaims() { + MutableTestCredential testCredential = new MutableTestCredential(new ArrayList<>(BASE_ASSERTIONS)); + final String[] firstToken = new String[1]; + + testCredential.addAssertion(tokenRequestContext -> { + // This will ensure that that the first request does not contains claims, but the second does after + // receiving a 401 response with a challenge with claims. + testCredential.replaceAssertion(anotherTokenRequestContext -> + DECODED_CLAIMS.equals(anotherTokenRequestContext.getClaims()), 3); + + // We will also store the value of the first credential before it changes on a second call + firstToken[0] = Base64Util.encodeToString(testCredential.getCredential().getBytes(StandardCharsets.UTF_8)); + + assertNotNull(firstToken[0]); + + return tokenRequestContext.getClaims() == null; + }); + + HttpResponse[] responses = new HttpResponse[] { + unauthorizedHttpResponseWithHeader, + unauthorizedHttpResponseWithHeaderAndClaims, + simpleResponse + }; + AtomicInteger currentResponse = new AtomicInteger(); + KeyVaultCredentialPolicy policy = new KeyVaultCredentialPolicy(testCredential, false); + + HttpPipeline pipeline = new HttpPipelineBuilder() + .policies(policy) + .httpClient(ignored -> Mono.just(responses[currentResponse.getAndIncrement()])) + .build(); + + // The first request to a Key Vault endpoint without an access token will always return a 401 Unauthorized + // response with a WWW-Authenticate header containing an authentication challenge. + + HttpResponse firstResponse = SyncAsyncExtension.execute( + () -> pipeline.sendSync(this.callContext.getHttpRequest(), this.callContext.getContext()), + () -> pipeline.send(this.callContext.getHttpRequest(), this.callContext.getContext())); + + String newToken = this.callContext.getHttpRequest().getHeaders().getValue(AUTHORIZATION); + + // The first unauthorized response caused a token to be set on the request, then the token was updated on a + // subsequent unauthorized response with claims. + assertNotEquals(firstToken[0], newToken); + // Finally, a successful response was received. + assertEquals(simpleResponse, firstResponse); + + KeyVaultCredentialPolicy.clearCache(); + } + private Mono onChallengeAndClearCache(KeyVaultCredentialPolicy policy, HttpPipelineCallContext callContext, HttpResponse unauthorizedHttpResponse) { Mono onChallenge = policy.authorizeRequestOnChallenge(callContext, unauthorizedHttpResponse); @@ -589,9 +645,6 @@ public MutableTestCredential(List> assert @Override public Mono getToken(TokenRequestContext requestContext) { if (requestContext.isCaeEnabled() && requestContext.getClaims() != null) { - assertEquals("{\"access_token\":{\"acrs\":{\"essential\":true,\"value\":\"cp1\"}}}", - requestContext.getClaims()); - credential = new Random().toString(); } @@ -623,5 +676,9 @@ public MutableTestCredential replaceAssertion(Function Date: Mon, 14 Oct 2024 15:35:20 -0700 Subject: [PATCH 16/18] Fixed compilation error. --- .../keyvault/administration/KeyVaultCredentialPolicyTest.java | 3 ++- .../keyvault/certificates/KeyVaultCredentialPolicyTest.java | 3 ++- .../security/keyvault/keys/KeyVaultCredentialPolicyTest.java | 3 ++- .../keyvault/secrets/KeyVaultCredentialPolicyTest.java | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java index 72f4bceaa4de8..164998021a711 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java @@ -35,6 +35,7 @@ import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; @@ -66,7 +67,7 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); - private static final List> BASE_ASSERTIONS = List.of( + private static final List> BASE_ASSERTIONS = Arrays.asList( tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), TokenRequestContext::isCaeEnabled); diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java index 6ba6c438bb71d..bc6dc6ecdd6ce 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java @@ -35,6 +35,7 @@ import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; @@ -66,7 +67,7 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); - private static final List> BASE_ASSERTIONS = List.of( + private static final List> BASE_ASSERTIONS = Arrays.asList( tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), TokenRequestContext::isCaeEnabled); diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java index 104a6e124f7e3..d14cb19561380 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java @@ -35,6 +35,7 @@ import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; @@ -66,7 +67,7 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); - private static final List> BASE_ASSERTIONS = List.of( + private static final List> BASE_ASSERTIONS = Arrays.asList( tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), TokenRequestContext::isCaeEnabled); diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java index 355182d4eee70..ca25aa04a1ffa 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java @@ -35,6 +35,7 @@ import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; @@ -66,7 +67,7 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); - private static final List> BASE_ASSERTIONS = List.of( + private static final List> BASE_ASSERTIONS = Arrays.asList( tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), TokenRequestContext::isCaeEnabled); From bfa88a7714a9f8cc776f1a7b3edf7e6eeab19c57 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Mon, 14 Oct 2024 15:59:06 -0700 Subject: [PATCH 17/18] Fixed issue with BasicAuthenticationCredential. --- .../administration/KeyVaultCredentialPolicyTest.java | 12 ++++++++---- .../certificates/KeyVaultCredentialPolicyTest.java | 12 ++++++++---- .../keyvault/keys/KeyVaultCredentialPolicyTest.java | 12 ++++++++---- .../secrets/KeyVaultCredentialPolicyTest.java | 12 ++++++++---- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java index 164998021a711..55ade51099554 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java @@ -4,7 +4,6 @@ package com.azure.security.keyvault.administration; import com.azure.core.credential.AccessToken; -import com.azure.core.credential.BasicAuthenticationCredential; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpHeaderName; @@ -67,6 +66,8 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); + private static final String FAKE_ENCODED_CREDENTIAL = + Base64Util.encodeToString("user:fakePasswordPlaceholder".getBytes(StandardCharsets.UTF_8)); private static final List> BASE_ASSERTIONS = Arrays.asList( tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), @@ -82,7 +83,7 @@ public class KeyVaultCredentialPolicyTest { private HttpPipelineCallContext testContext; private HttpPipelineCallContext bodyContext; private HttpPipelineCallContext bodyFluxContext; - private BasicAuthenticationCredential credential; + private TokenCredential credential; private static HttpPipelineCallContext createCallContext(HttpRequest request, Context context) { AtomicReference callContextReference = new AtomicReference<>(); @@ -139,7 +140,10 @@ public void setup() { this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); - this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); + // Can't use BasicAuthenticationCredential until the following PR is merged: + // https://github.com/Azure/azure-sdk-for-java/pull/42238 + this.credential = tokenRequestContext -> + Mono.fromCallable(() -> new AccessToken(FAKE_ENCODED_CREDENTIAL, OffsetDateTime.MAX.minusYears(1))); } @AfterEach @@ -657,7 +661,7 @@ public Mono getToken(TokenRequestContext requestContext) { } } - return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX.minusYears(1))); } public MutableTestCredential setAssertions(List> assertions) { diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java index bc6dc6ecdd6ce..8893ffdff2101 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java @@ -4,7 +4,6 @@ package com.azure.security.keyvault.certificates; import com.azure.core.credential.AccessToken; -import com.azure.core.credential.BasicAuthenticationCredential; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpHeaderName; @@ -67,6 +66,8 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); + private static final String FAKE_ENCODED_CREDENTIAL = + Base64Util.encodeToString("user:fakePasswordPlaceholder".getBytes(StandardCharsets.UTF_8)); private static final List> BASE_ASSERTIONS = Arrays.asList( tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), @@ -82,7 +83,7 @@ public class KeyVaultCredentialPolicyTest { private HttpPipelineCallContext testContext; private HttpPipelineCallContext bodyContext; private HttpPipelineCallContext bodyFluxContext; - private BasicAuthenticationCredential credential; + private TokenCredential credential; private static HttpPipelineCallContext createCallContext(HttpRequest request, Context context) { AtomicReference callContextReference = new AtomicReference<>(); @@ -139,7 +140,10 @@ public void setup() { this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); - this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); + // Can't use BasicAuthenticationCredential until the following PR is merged: + // https://github.com/Azure/azure-sdk-for-java/pull/42238 + this.credential = tokenRequestContext -> + Mono.fromCallable(() -> new AccessToken(FAKE_ENCODED_CREDENTIAL, OffsetDateTime.MAX.minusYears(1))); } @AfterEach @@ -657,7 +661,7 @@ public Mono getToken(TokenRequestContext requestContext) { } } - return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX.minusYears(1))); } public MutableTestCredential setAssertions(List> assertions) { diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java index d14cb19561380..9894f91bdb74b 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java @@ -4,7 +4,6 @@ package com.azure.security.keyvault.keys; import com.azure.core.credential.AccessToken; -import com.azure.core.credential.BasicAuthenticationCredential; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpHeaderName; @@ -67,6 +66,8 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); + private static final String FAKE_ENCODED_CREDENTIAL = + Base64Util.encodeToString("user:fakePasswordPlaceholder".getBytes(StandardCharsets.UTF_8)); private static final List> BASE_ASSERTIONS = Arrays.asList( tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), @@ -82,7 +83,7 @@ public class KeyVaultCredentialPolicyTest { private HttpPipelineCallContext testContext; private HttpPipelineCallContext bodyContext; private HttpPipelineCallContext bodyFluxContext; - private BasicAuthenticationCredential credential; + private TokenCredential credential; private static HttpPipelineCallContext createCallContext(HttpRequest request, Context context) { AtomicReference callContextReference = new AtomicReference<>(); @@ -139,7 +140,10 @@ public void setup() { this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); - this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); + // Can't use BasicAuthenticationCredential until the following PR is merged: + // https://github.com/Azure/azure-sdk-for-java/pull/42238 + this.credential = tokenRequestContext -> + Mono.fromCallable(() -> new AccessToken(FAKE_ENCODED_CREDENTIAL, OffsetDateTime.MAX.minusYears(1))); } @AfterEach @@ -657,7 +661,7 @@ public Mono getToken(TokenRequestContext requestContext) { } } - return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX.minusYears(1))); } public MutableTestCredential setAssertions(List> assertions) { diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java index ca25aa04a1ffa..fe1ed793933c2 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java @@ -4,7 +4,6 @@ package com.azure.security.keyvault.secrets; import com.azure.core.credential.AccessToken; -import com.azure.core.credential.BasicAuthenticationCredential; import com.azure.core.credential.TokenCredential; import com.azure.core.credential.TokenRequestContext; import com.azure.core.http.HttpHeaderName; @@ -67,6 +66,8 @@ public class KeyVaultCredentialPolicyTest { private static final String BODY = "this is a sample body"; private static final Flux BODY_FLUX = Flux.defer(() -> Flux.fromStream(Stream.of(BODY.split("")).map(s -> ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8))))); + private static final String FAKE_ENCODED_CREDENTIAL = + Base64Util.encodeToString("user:fakePasswordPlaceholder".getBytes(StandardCharsets.UTF_8)); private static final List> BASE_ASSERTIONS = Arrays.asList( tokenRequestContext -> !tokenRequestContext.getScopes().isEmpty(), tokenRequestContext -> !isNullOrEmpty(tokenRequestContext.getTenantId()), @@ -82,7 +83,7 @@ public class KeyVaultCredentialPolicyTest { private HttpPipelineCallContext testContext; private HttpPipelineCallContext bodyContext; private HttpPipelineCallContext bodyFluxContext; - private BasicAuthenticationCredential credential; + private TokenCredential credential; private static HttpPipelineCallContext createCallContext(HttpRequest request, Context context) { AtomicReference callContextReference = new AtomicReference<>(); @@ -139,7 +140,10 @@ public void setup() { this.testContext = createCallContext(request, Context.NONE); this.bodyContext = createCallContext(request, bodyContextContext); this.bodyFluxContext = createCallContext(request, bodyFluxContextContext); - this.credential = new BasicAuthenticationCredential("user", "fakePasswordPlaceholder"); + // Can't use BasicAuthenticationCredential until the following PR is merged: + // https://github.com/Azure/azure-sdk-for-java/pull/42238 + this.credential = tokenRequestContext -> + Mono.fromCallable(() -> new AccessToken(FAKE_ENCODED_CREDENTIAL, OffsetDateTime.MAX.minusYears(1))); } @AfterEach @@ -657,7 +661,7 @@ public Mono getToken(TokenRequestContext requestContext) { } } - return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX)); + return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX.minusYears(1))); } public MutableTestCredential setAssertions(List> assertions) { From 76cb15746baa0e1cac3f2bd6eaf37c0ba281c4a9 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Mon, 14 Oct 2024 16:19:00 -0700 Subject: [PATCH 18/18] Fixed CheckStyle issues. --- .../implementation/KeyVaultCredentialPolicy.java | 6 ++++-- .../administration/KeyVaultCredentialPolicyTest.java | 12 ++++++------ .../implementation/KeyVaultCredentialPolicy.java | 6 ++++-- .../certificates/KeyVaultCredentialPolicyTest.java | 12 ++++++------ .../implementation/KeyVaultCredentialPolicy.java | 6 ++++-- .../keyvault/keys/KeyVaultCredentialPolicyTest.java | 12 ++++++------ .../implementation/KeyVaultCredentialPolicy.java | 6 ++++-- .../secrets/KeyVaultCredentialPolicyTest.java | 12 ++++++------ 8 files changed, 40 insertions(+), 32 deletions(-) diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java index 497d28689f7f5..8043cdc730710 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/main/java/com/azure/security/keyvault/administration/implementation/KeyVaultCredentialPolicy.java @@ -407,7 +407,8 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) && !isClaimsPresent(httpResponse)) { - return handleChallenge(context, newResponse, nextPolicy); + + return handleChallenge(context, newResponse, nextPolicy); } else { return Mono.just(newResponse); } @@ -430,7 +431,8 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) && !isClaimsPresent(httpResponse)) { - return handleChallengeSync(context, newResponse, nextPolicy); + + return handleChallengeSync(context, newResponse, nextPolicy); } return newResponse; diff --git a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java index 55ade51099554..b5caeb277a755 100644 --- a/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-administration/src/test/java/com/azure/security/keyvault/administration/KeyVaultCredentialPolicyTest.java @@ -635,11 +635,11 @@ private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, Ht return onChallengeSync; } - private static class MutableTestCredential implements TokenCredential { + private static final class MutableTestCredential implements TokenCredential { private String credential; private List> assertions; - public MutableTestCredential(List> assertions) { + private MutableTestCredential(List> assertions) { this.credential = new Random().toString(); this.assertions = assertions; } @@ -664,25 +664,25 @@ public Mono getToken(TokenRequestContext requestContext) { return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX.minusYears(1))); } - public MutableTestCredential setAssertions(List> assertions) { + private MutableTestCredential setAssertions(List> assertions) { this.assertions = assertions; return this; } - public MutableTestCredential addAssertion(Function assertion) { + private MutableTestCredential addAssertion(Function assertion) { assertions.add(assertion); return this; } - public MutableTestCredential replaceAssertion(Function assertion, int index) { + private MutableTestCredential replaceAssertion(Function assertion, int index) { assertions.set(index, assertion); return this; } - public String getCredential() { + private String getCredential() { return this.credential; } } diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java index 328c1d8c30e1f..a978e2a585f8f 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/main/java/com/azure/security/keyvault/certificates/implementation/KeyVaultCredentialPolicy.java @@ -407,7 +407,8 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) && !isClaimsPresent(httpResponse)) { - return handleChallenge(context, newResponse, nextPolicy); + + return handleChallenge(context, newResponse, nextPolicy); } else { return Mono.just(newResponse); } @@ -430,7 +431,8 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) && !isClaimsPresent(httpResponse)) { - return handleChallengeSync(context, newResponse, nextPolicy); + + return handleChallengeSync(context, newResponse, nextPolicy); } return newResponse; diff --git a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java index 8893ffdff2101..0dfbedd591fb5 100644 --- a/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-certificates/src/test/java/com/azure/security/keyvault/certificates/KeyVaultCredentialPolicyTest.java @@ -635,11 +635,11 @@ private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, Ht return onChallengeSync; } - private static class MutableTestCredential implements TokenCredential { + private static final class MutableTestCredential implements TokenCredential { private String credential; private List> assertions; - public MutableTestCredential(List> assertions) { + private MutableTestCredential(List> assertions) { this.credential = new Random().toString(); this.assertions = assertions; } @@ -664,25 +664,25 @@ public Mono getToken(TokenRequestContext requestContext) { return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX.minusYears(1))); } - public MutableTestCredential setAssertions(List> assertions) { + private MutableTestCredential setAssertions(List> assertions) { this.assertions = assertions; return this; } - public MutableTestCredential addAssertion(Function assertion) { + private MutableTestCredential addAssertion(Function assertion) { assertions.add(assertion); return this; } - public MutableTestCredential replaceAssertion(Function assertion, int index) { + private MutableTestCredential replaceAssertion(Function assertion, int index) { assertions.set(index, assertion); return this; } - public String getCredential() { + private String getCredential() { return this.credential; } } diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java index 165b8ec7a93e9..608a4521d2459 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/main/java/com/azure/security/keyvault/keys/implementation/KeyVaultCredentialPolicy.java @@ -407,7 +407,8 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) && !isClaimsPresent(httpResponse)) { - return handleChallenge(context, newResponse, nextPolicy); + + return handleChallenge(context, newResponse, nextPolicy); } else { return Mono.just(newResponse); } @@ -430,7 +431,8 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) && !isClaimsPresent(httpResponse)) { - return handleChallengeSync(context, newResponse, nextPolicy); + + return handleChallengeSync(context, newResponse, nextPolicy); } return newResponse; diff --git a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java index 9894f91bdb74b..b7fdc8d2600a9 100644 --- a/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-keys/src/test/java/com/azure/security/keyvault/keys/KeyVaultCredentialPolicyTest.java @@ -635,11 +635,11 @@ private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, Ht return onChallengeSync; } - private static class MutableTestCredential implements TokenCredential { + private static final class MutableTestCredential implements TokenCredential { private String credential; private List> assertions; - public MutableTestCredential(List> assertions) { + private MutableTestCredential(List> assertions) { this.credential = new Random().toString(); this.assertions = assertions; } @@ -664,25 +664,25 @@ public Mono getToken(TokenRequestContext requestContext) { return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX.minusYears(1))); } - public MutableTestCredential setAssertions(List> assertions) { + private MutableTestCredential setAssertions(List> assertions) { this.assertions = assertions; return this; } - public MutableTestCredential addAssertion(Function assertion) { + private MutableTestCredential addAssertion(Function assertion) { assertions.add(assertion); return this; } - public MutableTestCredential replaceAssertion(Function assertion, int index) { + private MutableTestCredential replaceAssertion(Function assertion, int index) { assertions.set(index, assertion); return this; } - public String getCredential() { + private String getCredential() { return this.credential; } } diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java index 8ead5004cbade..804564bc040e0 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/main/java/com/azure/security/keyvault/secrets/implementation/KeyVaultCredentialPolicy.java @@ -407,7 +407,8 @@ private Mono handleChallenge(HttpPipelineCallContext context, Http if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) && !isClaimsPresent(httpResponse)) { - return handleChallenge(context, newResponse, nextPolicy); + + return handleChallenge(context, newResponse, nextPolicy); } else { return Mono.just(newResponse); } @@ -430,7 +431,8 @@ private HttpResponse handleChallengeSync(HttpPipelineCallContext context, HttpRe if (newResponse.getStatusCode() == 401 && authHeader != null && isClaimsPresent(newResponse) && !isClaimsPresent(httpResponse)) { - return handleChallengeSync(context, newResponse, nextPolicy); + + return handleChallengeSync(context, newResponse, nextPolicy); } return newResponse; diff --git a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java index fe1ed793933c2..2ec6839db94e5 100644 --- a/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java +++ b/sdk/keyvault/azure-security-keyvault-secrets/src/test/java/com/azure/security/keyvault/secrets/KeyVaultCredentialPolicyTest.java @@ -635,11 +635,11 @@ private boolean onChallengeAndClearCacheSync(KeyVaultCredentialPolicy policy, Ht return onChallengeSync; } - private static class MutableTestCredential implements TokenCredential { + private static final class MutableTestCredential implements TokenCredential { private String credential; private List> assertions; - public MutableTestCredential(List> assertions) { + private MutableTestCredential(List> assertions) { this.credential = new Random().toString(); this.assertions = assertions; } @@ -664,25 +664,25 @@ public Mono getToken(TokenRequestContext requestContext) { return Mono.fromCallable(() -> new AccessToken(encodedCredential, OffsetDateTime.MAX.minusYears(1))); } - public MutableTestCredential setAssertions(List> assertions) { + private MutableTestCredential setAssertions(List> assertions) { this.assertions = assertions; return this; } - public MutableTestCredential addAssertion(Function assertion) { + private MutableTestCredential addAssertion(Function assertion) { assertions.add(assertion); return this; } - public MutableTestCredential replaceAssertion(Function assertion, int index) { + private MutableTestCredential replaceAssertion(Function assertion, int index) { assertions.set(index, assertion); return this; } - public String getCredential() { + private String getCredential() { return this.credential; } }