Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow">OAuth 2.0 On-Behalf-Of</a>
Expand All @@ -18,8 +24,25 @@ public class AadJwtBearerGrantRequestEntityConverter extends JwtBearerGrantReque

@Override
protected MultiValueMap<String, String> createParameters(JwtBearerGrantRequest jwtBearerGrantRequest) {
MultiValueMap<String, String> parameters = super.createParameters(jwtBearerGrantRequest);
parameters.add("requested_token_use", "on_behalf_of");
ClientRegistration clientRegistration = jwtBearerGrantRequest.getClientRegistration();
MultiValueMap<String, String> 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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 =
Expand All @@ -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<String, String> additionalParams = new LinkedMultiValueMap<>();
additionalParams.set("custom_param", "custom_value");
return additionalParams;
});

RequestEntity<MultiValueMap<String, String>> entity =
(RequestEntity<MultiValueMap<String, String>>) converter.convert(request);
MultiValueMap<String, String> 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<String> 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<MultiValueMap<String, String>> entity =
(RequestEntity<MultiValueMap<String, String>>) converter.convert(request);
MultiValueMap<String, String> 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<MultiValueMap<String, String>> entity =
(RequestEntity<MultiValueMap<String, String>>) converter.convert(request);
MultiValueMap<String, String> 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"));
}
}
Loading