diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/AadJwtBearerGrantRequestEntityConverter.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/AadJwtBearerGrantRequestEntityConverter.java
index 378b13cfc165..7870cba0b78b 100644
--- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/AadJwtBearerGrantRequestEntityConverter.java
+++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/AadJwtBearerGrantRequestEntityConverter.java
@@ -5,10 +5,16 @@
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequestEntityConverter;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
/**
- * This is a special JWT Bearer flow implementation for Microsoft identify platform.
+ * This is a special JWT Bearer flow implementation for Microsoft identity platform.
*
* @since 4.3.0
* @see OAuth 2.0 On-Behalf-Of
@@ -18,8 +24,25 @@ public class AadJwtBearerGrantRequestEntityConverter extends JwtBearerGrantReque
@Override
protected MultiValueMap createParameters(JwtBearerGrantRequest jwtBearerGrantRequest) {
- MultiValueMap parameters = super.createParameters(jwtBearerGrantRequest);
- parameters.add("requested_token_use", "on_behalf_of");
+ ClientRegistration clientRegistration = jwtBearerGrantRequest.getClientRegistration();
+ MultiValueMap parameters = new LinkedMultiValueMap<>();
+ parameters.set(OAuth2ParameterNames.GRANT_TYPE, jwtBearerGrantRequest.getGrantType().getValue());
+ parameters.set(OAuth2ParameterNames.ASSERTION, jwtBearerGrantRequest.getJwt().getTokenValue());
+ if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
+ parameters.set(OAuth2ParameterNames.SCOPE,
+ StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " "));
+ }
+ // For CLIENT_SECRET_BASIC: credentials go in Authorization header, not in request parameters
+ // For CLIENT_SECRET_POST and other methods: client_id goes in request parameters
+ if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
+ parameters.set(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
+ }
+ // For CLIENT_SECRET_POST: client_secret goes in request parameters
+ // For CLIENT_SECRET_BASIC and other methods: client_secret is handled separately (e.g., in Authorization header)
+ if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())) {
+ parameters.set(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
+ }
+ parameters.set("requested_token_use", "on_behalf_of");
return parameters;
}
}
diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/AadJwtBearerGrantRequestEntityConverterTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/AadJwtBearerGrantRequestEntityConverterTests.java
index ce771c846038..d11e6ed70ab4 100644
--- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/AadJwtBearerGrantRequestEntityConverterTests.java
+++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/security/AadJwtBearerGrantRequestEntityConverterTests.java
@@ -8,13 +8,19 @@
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.time.Instant;
+import java.util.List;
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.assertTrue;
class AadJwtBearerGrantRequestEntityConverterTests {
@@ -31,8 +37,8 @@ void requestedTokenUseParameter() {
Jwt jwt = Jwt.withTokenValue("jwt-token-value")
.header("alg", JwsAlgorithms.RS256)
.claim("sub", "test")
- .issuedAt(Instant.ofEpochMilli(Instant.now().toEpochMilli()))
- .expiresAt(Instant.ofEpochMilli(Instant.now().plusSeconds(60).toEpochMilli()))
+ .issuedAt(Instant.now())
+ .expiresAt(Instant.now().plusSeconds(60))
.build();
JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, jwt);
AadJwtBearerGrantRequestEntityConverter converter =
@@ -43,4 +49,134 @@ void requestedTokenUseParameter() {
assertTrue(parameters.containsKey("requested_token_use"));
assertEquals("on_behalf_of", parameters.getFirst("requested_token_use"));
}
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void grantTypeIsNotDuplicatedWhenParametersConverterIsAdded() {
+ ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("test")
+ .clientId("test")
+ .clientSecret("test-secret")
+ .authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
+ .tokenUri("http://localhost/token")
+ .build();
+ Jwt jwt = Jwt.withTokenValue("jwt-token-value")
+ .header("alg", JwsAlgorithms.RS256)
+ .claim("sub", "test")
+ .issuedAt(Instant.now())
+ .expiresAt(Instant.now().plusSeconds(60))
+ .build();
+ JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, jwt);
+
+ // Create converter and add a parameters converter that returns additional parameters
+ AadJwtBearerGrantRequestEntityConverter converter = new AadJwtBearerGrantRequestEntityConverter();
+ converter.addParametersConverter((grantRequest) -> {
+ MultiValueMap additionalParams = new LinkedMultiValueMap<>();
+ additionalParams.set("custom_param", "custom_value");
+ return additionalParams;
+ });
+
+ RequestEntity> entity =
+ (RequestEntity>) converter.convert(request);
+ MultiValueMap parameters = entity.getBody();
+
+ // Verify that grant_type exists
+ assertTrue(parameters.containsKey(OAuth2ParameterNames.GRANT_TYPE));
+
+ // Verify that grant_type is a single value, not a list
+ List grantTypeValues = parameters.get(OAuth2ParameterNames.GRANT_TYPE);
+ assertNotNull(grantTypeValues);
+ assertEquals(1, grantTypeValues.size(),
+ "Grant type should be a single value, not duplicated. Found: " + grantTypeValues.size() + " values");
+ assertEquals("urn:ietf:params:oauth:grant-type:jwt-bearer", grantTypeValues.get(0));
+
+ // Verify the custom parameter was added
+ assertTrue(parameters.containsKey("custom_param"));
+ assertEquals("custom_value", parameters.getFirst("custom_param"));
+
+ // Verify requested_token_use is present
+ assertTrue(parameters.containsKey("requested_token_use"));
+ assertEquals("on_behalf_of", parameters.getFirst("requested_token_use"));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void allRequiredParametersArePresent() {
+ ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("test")
+ .clientId("test")
+ .clientSecret("test-secret")
+ .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
+ .authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
+ .tokenUri("http://localhost/token")
+ .scope("openid", "profile")
+ .build();
+ Jwt jwt = Jwt.withTokenValue("jwt-token-value")
+ .header("alg", JwsAlgorithms.RS256)
+ .claim("sub", "test")
+ .issuedAt(Instant.now())
+ .expiresAt(Instant.now().plusSeconds(60))
+ .build();
+ JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, jwt);
+ AadJwtBearerGrantRequestEntityConverter converter =
+ new AadJwtBearerGrantRequestEntityConverter();
+ RequestEntity> entity =
+ (RequestEntity>) converter.convert(request);
+ MultiValueMap parameters = entity.getBody();
+
+ // Verify all required parameters
+ assertTrue(parameters.containsKey(OAuth2ParameterNames.GRANT_TYPE));
+ assertEquals("urn:ietf:params:oauth:grant-type:jwt-bearer",
+ parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE));
+
+ assertTrue(parameters.containsKey(OAuth2ParameterNames.ASSERTION));
+ assertEquals("jwt-token-value", parameters.getFirst(OAuth2ParameterNames.ASSERTION));
+
+ assertTrue(parameters.containsKey(OAuth2ParameterNames.SCOPE));
+ assertEquals("openid profile", parameters.getFirst(OAuth2ParameterNames.SCOPE));
+
+ // For CLIENT_SECRET_POST, both client_id and client_secret should be in parameters
+ assertTrue(parameters.containsKey(OAuth2ParameterNames.CLIENT_ID));
+ assertEquals("test", parameters.getFirst(OAuth2ParameterNames.CLIENT_ID));
+
+ assertTrue(parameters.containsKey(OAuth2ParameterNames.CLIENT_SECRET));
+ assertEquals("test-secret", parameters.getFirst(OAuth2ParameterNames.CLIENT_SECRET));
+
+ assertTrue(parameters.containsKey("requested_token_use"));
+ assertEquals("on_behalf_of", parameters.getFirst("requested_token_use"));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void clientSecretBasicAuthenticationMethod() {
+ ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("test")
+ .clientId("test")
+ .clientSecret("test-secret")
+ .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
+ .authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
+ .tokenUri("http://localhost/token")
+ .scope("openid", "profile")
+ .build();
+ Jwt jwt = Jwt.withTokenValue("jwt-token-value")
+ .header("alg", JwsAlgorithms.RS256)
+ .claim("sub", "test")
+ .issuedAt(Instant.now())
+ .expiresAt(Instant.now().plusSeconds(60))
+ .build();
+ JwtBearerGrantRequest request = new JwtBearerGrantRequest(clientRegistration, jwt);
+ AadJwtBearerGrantRequestEntityConverter converter =
+ new AadJwtBearerGrantRequestEntityConverter();
+ RequestEntity> entity =
+ (RequestEntity>) converter.convert(request);
+ MultiValueMap parameters = entity.getBody();
+
+ // For CLIENT_SECRET_BASIC, credentials should NOT be in the parameters (they go in the Authorization header)
+ assertFalse(parameters.containsKey(OAuth2ParameterNames.CLIENT_ID),
+ "CLIENT_ID should not be in parameters for CLIENT_SECRET_BASIC");
+ assertFalse(parameters.containsKey(OAuth2ParameterNames.CLIENT_SECRET),
+ "CLIENT_SECRET should not be in parameters for CLIENT_SECRET_BASIC");
+
+ // But other parameters should still be present
+ assertTrue(parameters.containsKey(OAuth2ParameterNames.GRANT_TYPE));
+ assertTrue(parameters.containsKey(OAuth2ParameterNames.ASSERTION));
+ assertTrue(parameters.containsKey("requested_token_use"));
+ }
}