Skip to content

Commit d057d0e

Browse files
authored
chore(deps): upgrade to Spring Framework 7 generation (PR-MAJOR) (#24475)
* chore(deps): upgrade to Spring Framework 7 generation (PR-MAJOR) Atomic generation bump after Phase 0 prep PRs A-G: - Spring Framework 6.2.18 -> 7.0.8 - Spring Security 6.5.10 -> 7.0.6 - Spring Authorization Server 1.5.2 -> 7.0.6 - Spring Data Redis 2.7.18 -> 4.1.0 - Spring Session Data Redis 2.7.4 -> 4.1.0 (aligned with session-core 4.1.0) - JUnit 5.12.2 -> 6.0.3 - surefire: spring.test.extension.context.scope=test_class Spring-7-only API migrations: - HttpHeaders no longer implements Map (AuthScheme, RouteService, advice, tests) - OAuth2 RestClientAuthorizationCodeTokenResponseClient (private_key_jwt kept) - Authorization Server package moves + OAuth2AuthorizationServerConfigurer ctor - ObjectPostProcessor package move; AbstractAuthenticationToken(null) cast - Hibernate SpringSessionContext FQCN -> org.springframework.orm.jpa.hibernate - RedisTemplate<String,Object> for session - setPatternParser(null) keep AntPathMatcher until PathPattern migration - Temporary requireProofKey(false) bridge (SAS 7 PKCE-by-default); PR-H to adopt PKCE Validated: clean reactor test-compile + unit-test profile green. Based on spike PR #24087; Phase 0 prep already on master. * fix: declare spring-web on dhis-api for HttpHeaders AuthScheme API AuthScheme.apply now takes org.springframework.http.HttpHeaders (Spring 7 no longer implements Map). dependency:analyze requires an explicit spring-web compile dependency on dhis-api. * fix: use Spring Data Redis 4.0.6 + Lettuce 6.8.2 (not SDR 4.1) SDR 4.1.0 requires Lettuce 7.5+ (DriverInfo). That blew up app boot in api-test with ClassNotFoundException: io.lettuce.core.DriverInfo. Stay on the 4.0.x line for MAJOR (aligned with spike PR #24087): - spring-data-redis 4.0.6 - spring-session-core / spring-session-data-redis 4.0.4 - lettuce 6.8.2.RELEASE Lettuce 7 remains PR-L after deliberate SDR 4.1+ adoption. * fix: SAS 7 PKCE/DCR/auth_time + data-exchange dependency analyze - dhis-service-data-exchange: drop unused spring-core (analyze fail) - Force requireProofKey(false) on client create/toEntity/DCR converter (SAS 7 PKCE-by-default temporary bridge; PR-H removes this) - DCR tests: omit scope (SAS 7 rejects scope on registration) - Federated OIDC test: clientSettings.requireProofKey(false) + Factor auth - TwoFactorAuthenticationProvider + OAuth2Authorization load path: ensure FactorGrantedAuthority so JwtGenerator can set OIDC auth_time when SessionRegistry is present (fixes OAuth2Test 500 authenticationTime) * fix: DCR default scopes; stop forcing requireProofKey on RegisteredClient save - DCR without scope (SAS 7) gets openid/profile/username/email defaults so client_credentials token requests still work - Do not rewrite requireProofKey in toEntity (round-trip fidelity for Dhis2OAuth2ClientServiceIntegrationTest); keep false only for CRUD applyCreateDefaults when clientSettings is null + DCR converter - Authorization integration test: allow FACTOR_* authorities after load * style: clear Sonar new-code smells on Spring 7 PR - avoid null credentials in UsernamePasswordAuthenticationToken.authenticated - SessionFixationConfigurer method reference; drop unused throws Exception - wrap AS filter-chain config Exception as IllegalStateException - TestBase clearSecurityContext; package-private test methods * style: clear remaining Sonar smells on Spring 7 PR Wrap filterChain Exception; requireNonNull principals; package-private RouteControllerTest beforeEach. * style: null-check principal before AuthenticationToken.authenticated (S4449) * fix: cast OAuth2User when rebuilding OAuth2AuthenticationToken * style: spotless import order for OAuth2User * chore: remove local plan and changelog docs from PR-MAJOR These markdown planning/changelog files must not ship in product PRs. * refactor: keep spring-web out of dhis-api Reverts the AuthScheme signature change back to the JDK-typed Map<String, List<String>> from master, removing the spring-web dependency that was added to dhis-api. Spring 7 HttpHeaders no longer implements MultiValueMap, so the two call sites (RouteService, WebhookHandler) now collect auth headers in a plain map and merge them into HttpHeaders afterwards, keeping the web type in the web-aware modules where spring-web is available. Keeps the Spring Security 7 constructor disambiguation cast in OAuth2ClientCredentialsAuthScheme.
1 parent e4a9688 commit d057d0e

30 files changed

Lines changed: 252 additions & 221 deletions

File tree

dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/auth/OAuth2ClientCredentialsAuthScheme.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
package org.hisp.dhis.common.auth;
3131

3232
import com.fasterxml.jackson.annotation.JsonProperty;
33+
import java.util.Collection;
3334
import java.util.List;
3435
import java.util.Map;
3536
import java.util.function.UnaryOperator;
@@ -44,6 +45,7 @@
4445
import org.springframework.context.ApplicationContext;
4546
import org.springframework.security.authentication.AbstractAuthenticationToken;
4647
import org.springframework.security.core.Authentication;
48+
import org.springframework.security.core.GrantedAuthority;
4749
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
4850
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
4951
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
@@ -64,7 +66,7 @@ public class OAuth2ClientCredentialsAuthScheme implements AuthScheme {
6466
public static final String OAUTH2_CLIENT_CREDENTIALS_TYPE = "oauth2-client-credentials";
6567

6668
public static final Authentication ANONYMOUS_AUTHENTICATION =
67-
new AbstractAuthenticationToken(null) {
69+
new AbstractAuthenticationToken((Collection<? extends GrantedAuthority>) null) {
6870
@Override
6971
public Object getCredentials() {
7072
return "";

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/route/RouteService.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import java.util.ArrayList;
4848
import java.util.Collection;
4949
import java.util.Collections;
50+
import java.util.LinkedHashMap;
5051
import java.util.List;
5152
import java.util.Map;
5253
import java.util.Objects;
@@ -480,7 +481,7 @@ protected WebClient.RequestHeadersSpec<?> buildRequestSpec(
480481
requestHeadersSpec = buildUpstreamRequestHeaderSpec(request, requestBodySpec);
481482
}
482483

483-
for (Map.Entry<String, List<String>> header : headers.entrySet()) {
484+
for (Map.Entry<String, List<String>> header : headers.headerSet()) {
484485
requestHeadersSpec =
485486
requestHeadersSpec.header(header.getKey(), header.getValue().toArray(new String[0]));
486487
}
@@ -586,14 +587,16 @@ protected ResponseBodyEmitter emitResponseBody(
586587
}
587588

588589
protected void applyAuthScheme(
589-
Route route, Map<String, List<String>> headers, Map<String, List<String>> queryParameters)
590+
Route route, HttpHeaders headers, Map<String, List<String>> queryParameters)
590591
throws BadGatewayException {
591592
if (route.getAuth() != null) {
592593
try {
594+
Map<String, List<String>> authHeaders = new LinkedHashMap<>();
593595
route
594596
.getAuth()
595597
.decrypt(encryptor::decrypt)
596-
.apply(applicationContext, headers, queryParameters);
598+
.apply(applicationContext, authHeaders, queryParameters);
599+
authHeaders.forEach((name, values) -> values.forEach(value -> headers.add(name, value)));
597600
} catch (Exception e) {
598601
log.error(e.getMessage(), e);
599602
throw new BadGatewayException("An error occurred during authentication");
@@ -646,7 +649,8 @@ private HttpHeaders filterRequestHeaders(HttpServletRequest request) {
646649
* @return an {@link HttpHeaders}.
647650
*/
648651
private HttpHeaders filterResponseHeaders(HttpHeaders responseHeaders) {
649-
return filterHeaders(responseHeaders.keySet(), ALLOWED_RESPONSE_HEADERS, responseHeaders::get);
652+
return filterHeaders(
653+
responseHeaders.headerNames(), ALLOWED_RESPONSE_HEADERS, responseHeaders::get);
650654
}
651655

652656
/**

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/oauth2/authorization/Dhis2OAuth2AuthorizationServiceImpl.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@
5252
import org.springframework.dao.DataRetrievalFailureException;
5353
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
5454
import org.springframework.security.core.Authentication;
55+
import org.springframework.security.core.GrantedAuthority;
56+
import org.springframework.security.core.authority.FactorGrantedAuthority;
5557
import org.springframework.security.jackson2.SecurityJackson2Modules;
58+
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
5659
import org.springframework.security.oauth2.core.OAuth2AccessToken;
5760
import org.springframework.security.oauth2.core.OAuth2DeviceCode;
5861
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
@@ -61,6 +64,7 @@
6164
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
6265
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
6366
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
67+
import org.springframework.security.oauth2.core.user.OAuth2User;
6468
import org.springframework.security.oauth2.jwt.Jwt;
6569
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
6670
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
@@ -255,7 +259,11 @@ private OAuth2Authorization toObject(Dhis2OAuth2Authorization entity) {
255259
.principalName(entity.getPrincipalName())
256260
.authorizationGrantType(OAuth2GrantTypes.resolve(entity.getAuthorizationGrantType()))
257261
.authorizedScopes(StringUtils.commaDelimitedListToSet(entity.getAuthorizedScopes()))
258-
.attributes(attributes -> attributes.putAll(parseMap(entity.getAttributes())));
262+
.attributes(
263+
attributes -> {
264+
attributes.putAll(parseMap(entity.getAttributes()));
265+
ensureFactorGrantedAuthority(attributes);
266+
});
259267

260268
if (entity.getState() != null) {
261269
builder.attribute(OAuth2ParameterNames.STATE, entity.getState());
@@ -474,6 +482,45 @@ private <T extends OAuth2Token> void setTokenValues(
474482
* @param data The JSON string
475483
* @return The parsed Map
476484
*/
485+
486+
/**
487+
* SAS 7 JwtGenerator requires a {@link FactorGrantedAuthority} to derive OIDC {@code auth_time}
488+
* when a SessionRegistry is present. Older persisted principals (and some login paths) may lack
489+
* one after JSON round-trip; re-attach a synthetic password factor so token exchange does not
490+
* fail with "authenticationTime cannot be null".
491+
*/
492+
private static void ensureFactorGrantedAuthority(Map<String, Object> attributes) {
493+
Object principal = attributes.get(java.security.Principal.class.getName());
494+
if (!(principal instanceof Authentication authentication)) {
495+
return;
496+
}
497+
if (authentication.getAuthorities().stream()
498+
.anyMatch(FactorGrantedAuthority.class::isInstance)) {
499+
return;
500+
}
501+
java.util.List<GrantedAuthority> authorities =
502+
new java.util.ArrayList<>(authentication.getAuthorities());
503+
authorities.add(
504+
FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY));
505+
Object principalObj = authentication.getPrincipal();
506+
if (principalObj == null) {
507+
return;
508+
}
509+
Authentication enriched;
510+
if (authentication instanceof OAuth2AuthenticationToken oauth
511+
&& principalObj instanceof OAuth2User oauth2User) {
512+
enriched =
513+
new OAuth2AuthenticationToken(
514+
oauth2User, authorities, oauth.getAuthorizedClientRegistrationId());
515+
} else {
516+
Object credentials = authentication.getCredentials();
517+
enriched =
518+
UsernamePasswordAuthenticationToken.authenticated(
519+
principalObj, credentials != null ? credentials : "", authorities);
520+
}
521+
attributes.put(java.security.Principal.class.getName(), enriched);
522+
}
523+
477524
private Map<String, Object> parseMap(String data) {
478525
if (data == null || data.isBlank()) {
479526
return Map.of();

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/oauth2/client/Dhis2OAuth2ClientServiceImpl.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,11 @@ public void applyCreateDefaults(Dhis2OAuth2Client entity) {
388388
if (entity.getRawName() == null || entity.getRawName().isEmpty()) {
389389
entity.setName(truncateName(entity.getClientId()));
390390
}
391+
// SAS 7 flips requireProofKey default false->true. Temporary bridge for existing non-PKCE
392+
// clients/e2e (CRUD/API create without clientSettings) until PR-H adopts PKCE-by-default.
391393
if (entity.getClientSettings() == null) {
392-
ClientSettings defaults = ClientSettings.builder().requireAuthorizationConsent(true).build();
394+
ClientSettings defaults =
395+
ClientSettings.builder().requireAuthorizationConsent(true).requireProofKey(false).build();
393396
entity.setClientSettings(writeMap(defaults.getSettings()));
394397
}
395398
if (entity.getTokenSettings() == null) {

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/oidc/DhisAuthorizationCodeTokenResponseClient.java

Lines changed: 19 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -31,71 +31,49 @@
3131

3232
import com.nimbusds.jose.jwk.JWK;
3333
import jakarta.annotation.PostConstruct;
34-
import java.util.List;
3534
import java.util.function.Consumer;
3635
import java.util.function.Function;
3736
import javax.annotation.Nonnull;
3837
import lombok.RequiredArgsConstructor;
39-
import org.springframework.core.convert.converter.Converter;
40-
import org.springframework.http.RequestEntity;
41-
import org.springframework.http.ResponseEntity;
42-
import org.springframework.http.converter.FormHttpMessageConverter;
43-
import org.springframework.http.converter.HttpMessageConverter;
4438
import org.springframework.security.oauth2.client.endpoint.NimbusJwtClientAuthenticationParametersConverter;
4539
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
4640
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
47-
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;
48-
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
41+
import org.springframework.security.oauth2.client.endpoint.RestClientAuthorizationCodeTokenResponseClient;
4942
import org.springframework.security.oauth2.client.registration.ClientRegistration;
5043
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
51-
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
52-
import org.springframework.security.oauth2.core.OAuth2Error;
5344
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
54-
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
5545
import org.springframework.stereotype.Service;
56-
import org.springframework.web.client.ResponseErrorHandler;
57-
import org.springframework.web.client.RestClientException;
58-
import org.springframework.web.client.RestOperations;
59-
import org.springframework.web.client.RestTemplate;
6046

6147
/**
6248
* Spring {@link OAuth2AccessTokenResponseClient} used by the DHIS2 Relying Party during the
6349
* authorization-code token exchange against an external OIDC Identity Provider.
6450
*
6551
* <p>This client supports both client authentication modes: standard {@code client_secret_*}
6652
* (basic, post, JWT) and {@code private_key_jwt}. When the matched {@link ClientRegistration}
67-
* declares {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}, the client builds a JWT client
68-
* assertion signed with the per-provider key loaded from {@link DhisOidcClientRegistration} ({@code
69-
* jwk}, {@code jwkSetUrl}), using Spring's {@link
70-
* NimbusJwtClientAuthenticationParametersConverter}; for all other methods it falls back to the
71-
* standard {@link OAuth2AuthorizationCodeGrantRequestEntityConverter}.
53+
* declares {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}, the {@link
54+
* NimbusJwtClientAuthenticationParametersConverter} adds a JWT client assertion signed with the
55+
* per-provider key loaded from {@link DhisOidcClientRegistration} ({@code jwk}, {@code jwkSetUrl});
56+
* for all other methods the converter contributes nothing and the delegate falls back to the
57+
* standard client-credentials parameters.
7258
*
73-
* <p>HTTP exchange is performed with a {@link RestTemplate} configured with {@link
74-
* FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}, and {@link
75-
* OAuth2ErrorResponseErrorHandler} mapping upstream HTTP errors to {@link
76-
* OAuth2AuthorizationException} with the {@code invalid_token_response} error code.
59+
* <p>Since Spring Security 7.0 removed the {@code RequestEntity}-converter /{@code RestOperations}
60+
* token-client infrastructure, the HTTP exchange is delegated to {@link
61+
* RestClientAuthorizationCodeTokenResponseClient}, which provides the default {@code RestClient},
62+
* message converters and OAuth2 error handling.
7763
*
7864
* @author Morten Svanæs <msvanaes@dhis2.org>
7965
*/
8066
@Service
8167
@RequiredArgsConstructor
8268
public class DhisAuthorizationCodeTokenResponseClient
8369
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
84-
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
85-
8670
private final DhisOidcProviderRepository clientRegistrations;
8771

88-
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter =
89-
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
90-
91-
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>>
92-
jwtRequestEntityConverter;
93-
94-
private RestOperations restOperations;
72+
private RestClientAuthorizationCodeTokenResponseClient delegate;
9573

9674
/**
97-
* Builds the two request-entity converters (standard and {@code private_key_jwt}) and the {@link
98-
* RestTemplate} used to call the IdP's token endpoint.
75+
* Builds the delegate token-response client and registers the {@code private_key_jwt} parameters
76+
* converter (which self-gates on the client registration's authentication method).
9977
*/
10078
@PostConstruct
10179
public void init() {
@@ -129,86 +107,22 @@ public void init() {
129107
parametersConverter = new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver);
130108
parametersConverter.setJwtClientAssertionCustomizer(jwtClientAssertionCustomizer);
131109

132-
OAuth2AuthorizationCodeGrantRequestEntityConverter jwtReqConverter =
133-
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
134-
jwtReqConverter.addParametersConverter(parametersConverter);
135-
this.jwtRequestEntityConverter = jwtReqConverter;
136-
137-
RestTemplate restTemplate =
138-
new RestTemplate(
139-
List.of(
140-
new FormHttpMessageConverter(),
141-
new OAuth2AccessTokenResponseHttpMessageConverter()));
142-
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
143-
this.restOperations = restTemplate;
110+
RestClientAuthorizationCodeTokenResponseClient tokenResponseClient =
111+
new RestClientAuthorizationCodeTokenResponseClient();
112+
tokenResponseClient.addParametersConverter(parametersConverter);
113+
this.delegate = tokenResponseClient;
144114
}
145115

146116
/**
147117
* Exchanges an authorization code for an access token (and an ID token) at the IdP's token
148-
* endpoint. Picks the {@code private_key_jwt} converter when the client registration declares
149-
* that authentication method, otherwise uses the standard converter.
118+
* endpoint.
150119
*
151120
* @param authorizationCodeGrantRequest the authorization-code grant request
152121
* @return the parsed token response from the IdP
153-
* @throws OAuth2AuthorizationException if the HTTP exchange fails or the response cannot be
154-
* parsed
155122
*/
156123
@Override
157124
public OAuth2AccessTokenResponse getTokenResponse(
158125
@Nonnull OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
159-
Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> converter =
160-
ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(
161-
authorizationCodeGrantRequest
162-
.getClientRegistration()
163-
.getClientAuthenticationMethod())
164-
? this.jwtRequestEntityConverter
165-
: this.requestEntityConverter;
166-
167-
return getResponse(converter.convert(authorizationCodeGrantRequest)).getBody();
168-
}
169-
170-
private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
171-
try {
172-
return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
173-
} catch (RestClientException ex) {
174-
OAuth2Error oauth2Error =
175-
new OAuth2Error(
176-
INVALID_TOKEN_RESPONSE_ERROR_CODE,
177-
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
178-
+ ex.getMessage(),
179-
null);
180-
throw new OAuth2AuthorizationException(oauth2Error, ex);
181-
}
182-
}
183-
184-
/**
185-
* Sets the {@link Converter} used for converting the {@link OAuth2AuthorizationCodeGrantRequest}
186-
* to a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
187-
*
188-
* @param requestEntityConverter the {@link Converter} used for converting to a {@link
189-
* RequestEntity} representation of the Access Token Request
190-
*/
191-
public void setRequestEntityConverter(
192-
@Nonnull
193-
Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter) {
194-
this.requestEntityConverter = requestEntityConverter;
195-
}
196-
197-
/**
198-
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token Response.
199-
*
200-
* <p><b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the
201-
* following:
202-
*
203-
* <ol>
204-
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link
205-
* OAuth2AccessTokenResponseHttpMessageConverter}
206-
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}
207-
* </ol>
208-
*
209-
* @param restOperations the {@link RestOperations} used when requesting the Access Token Response
210-
*/
211-
public void setRestOperations(@Nonnull RestOperations restOperations) {
212-
this.restOperations = restOperations;
126+
return delegate.getTokenResponse(authorizationCodeGrantRequest);
213127
}
214128
}

dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/spring2fa/TwoFactorAuthenticationProvider.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
5656
import org.springframework.security.core.Authentication;
5757
import org.springframework.security.core.AuthenticationException;
58+
import org.springframework.security.core.GrantedAuthority;
59+
import org.springframework.security.core.authority.FactorGrantedAuthority;
5860
import org.springframework.security.core.userdetails.UserDetailsService;
5961
import org.springframework.security.crypto.password.PasswordEncoder;
6062
import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException;
@@ -113,8 +115,16 @@ public Authentication authenticate(Authentication auth) throws AuthenticationExc
113115
checkTwoFactorAuthentication(auth, userDetails);
114116

115117
// Return a new authentication token with the user details.
118+
// Spring Security 7 JwtGenerator requires FactorGrantedAuthority for OIDC auth_time when a
119+
// SessionRegistry is active (Authorization Server always installs one).
120+
java.util.List<GrantedAuthority> authorities =
121+
new java.util.ArrayList<>(result.getAuthorities());
122+
if (authorities.stream().noneMatch(FactorGrantedAuthority.class::isInstance)) {
123+
authorities.add(
124+
FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY));
125+
}
116126
return new UsernamePasswordAuthenticationToken(
117-
userDetails, result.getCredentials(), result.getAuthorities());
127+
userDetails, result.getCredentials(), authorities);
118128
}
119129

120130
private void checkLockout(String username, String ip) {

dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/sms/GenericSmsGatewayTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class GenericSmsGatewayTest {
116116
private Map<String, String> valueStore = new HashMap<>();
117117

118118
@BeforeEach
119-
public void setUp() {
119+
void setUp() {
120120
subject.setRestTemplate(restTemplate);
121121

122122
gatewayConfig = new GenericHttpGatewayConfig();
@@ -188,8 +188,8 @@ void testSendSms_Json() {
188188
.filter(p -> p.isEncode() && p.isConfidential() && p.isHeader())
189189
.forEach(
190190
p -> {
191-
assertTrue(httpHeaders.containsKey(p.getKey()));
192-
assertEquals(" Basic ZGVjcnlwdGVkVGV4dA==", httpHeaders.get(p.getKey()).get(0));
191+
assertTrue(httpHeaders.containsHeader(p.getKey()));
192+
assertEquals(" Basic ZGVjcnlwdGVkVGV4dA==", httpHeaders.getFirst(p.getKey()));
193193
});
194194
}
195195

dhis-2/dhis-services/dhis-service-data-exchange/pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@
4949
</dependency>
5050

5151
<!-- Application -->
52-
<dependency>
53-
<groupId>org.springframework</groupId>
54-
<artifactId>spring-core</artifactId>
55-
</dependency>
5652
<dependency>
5753
<groupId>org.springframework</groupId>
5854
<artifactId>spring-context</artifactId>

0 commit comments

Comments
 (0)