diff --git a/docs/esignet-openapi.yaml b/docs/esignet-openapi.yaml index b3fef01b1..9f396450a 100644 --- a/docs/esignet-openapi.yaml +++ b/docs/esignet-openapi.yaml @@ -1986,6 +1986,7 @@ paths: - invalid_prompt - unsupported_pkce_challenge_method - invalid_pkce_challenge + - use_pkce errorMessage: type: string examples: @@ -2233,6 +2234,7 @@ paths: - invalid_pkce_challenge - invalid_request - invalid_id_token_hint + - use_pkce errorMessage: type: string examples: diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/constants/ErrorConstants.java b/esignet-core/src/main/java/io/mosip/esignet/core/constants/ErrorConstants.java index 14f2de7e4..ca35bfd34 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/constants/ErrorConstants.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/constants/ErrorConstants.java @@ -87,4 +87,5 @@ public class ErrorConstants { public static final String KBI_SPEC_NOT_FOUND= "kbi_spec_not_found"; public static final String INVALID_DPOP_PROOF = "invalid_dpop_proof"; public static final String USE_DPOP_NONCE = "use_dpop_nonce"; + public static final String USE_PKCE = "use_pkce"; } diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/OIDCTransaction.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/OIDCTransaction.java index d106598ed..ae743e02d 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/dto/OIDCTransaction.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/OIDCTransaction.java @@ -72,18 +72,20 @@ public class OIDCTransaction implements Serializable { Map> claimMetadata; Map requestedClaimDetails; + //Shared flags between signup and eSignet services String verificationStatus; String verificationErrorCode; - String userInfoResponseType; String[] prompt; int consentExpireMinutes; - boolean requirePushedAuthorizationRequests; - boolean dpopBoundAccessToken; - boolean requirePKCE; - Map additionalConfigMap; String dpopJkt; String dpopServerNonce; Long dpopServerNonceTTL; + + //Feature flags + boolean requirePushedAuthorizationRequests; + boolean dpopBoundAccessToken; + boolean requirePKCE; + String userInfoResponseType; } diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/dto/ServerProfile.java b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ServerProfile.java new file mode 100644 index 000000000..c1858d9e1 --- /dev/null +++ b/esignet-core/src/main/java/io/mosip/esignet/core/dto/ServerProfile.java @@ -0,0 +1,17 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package io.mosip.esignet.core.dto; + +import lombok.Data; + +import java.util.Map; + +@Data +public class ServerProfile { + + private String name; + private Map featureMap; +} diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/util/IdentityProviderUtil.java b/esignet-core/src/main/java/io/mosip/esignet/core/util/IdentityProviderUtil.java index 830a19c61..93af195dc 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/util/IdentityProviderUtil.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/util/IdentityProviderUtil.java @@ -223,7 +223,7 @@ public static String getJWKString(Map jwk) throws EsignetExcepti String use = (String) jwk.get("use"); String alg = (String) jwk.get("alg"); - if (keyType == null || alg==null || use==null) { + if (keyType == null) { throw new EsignetException(ErrorConstants.INVALID_PUBLIC_KEY); } @@ -231,13 +231,10 @@ public static String getJWKString(Map jwk) throws EsignetExcepti String jwkString = switch (keyType) { case "RSA" -> new RsaJsonWebKey(jwk).toJson(); case "EC" -> new EllipticCurveJsonWebKey(jwk).toJson(); - default -> { - log.error("Unsupported key type '{}' in JWK", keyType); - throw new EsignetException(ErrorConstants.INVALID_PUBLIC_KEY); - } + default -> throw new EsignetException(ErrorConstants.INVALID_PUBLIC_KEY); }; // Validate alg and use fields as RSAPublicKey and ECPublicKey classes do not validate these fields - if (alg.isEmpty() || use.isEmpty()) { + if ((alg != null && alg.isBlank()) || (use != null && use.isBlank())) { throw new EsignetException(ErrorConstants.INVALID_PUBLIC_KEY); } return jwkString; diff --git a/esignet-core/src/test/java/io/mosip/esignet/core/IdentityProviderUtilTest.java b/esignet-core/src/test/java/io/mosip/esignet/core/IdentityProviderUtilTest.java index 263b3e888..325cce131 100644 --- a/esignet-core/src/test/java/io/mosip/esignet/core/IdentityProviderUtilTest.java +++ b/esignet-core/src/test/java/io/mosip/esignet/core/IdentityProviderUtilTest.java @@ -233,7 +233,7 @@ public void getJWKString_withInvalidAlgForRSA_thenFail() { jwkMap.put("kty", "RSA"); jwkMap.put("n", "oahUIzUup5kqncCkHk5Zb1pRrLx7e6YtM-9jX1f5e6mHnZFkC2LJUZ0sEh0n5Y5KnQfW9s7d7gK2b8P0EEl0h3ZyHkWzA3YbsgzB4pDxP4RxMZ1I8xD2z3UvfA1zjvKDHz6wEweq4hVJ8nS8GzZJ2E_vb3s"); jwkMap.put("e", "AQAB"); - jwkMap.put("alg", "ES256"); + jwkMap.put("alg", ""); EsignetException ex = Assertions.assertThrows(EsignetException.class, () -> IdentityProviderUtil.getJWKString(jwkMap)); @@ -246,7 +246,7 @@ public void getJWKString_withInvalidUse_thenFail() { jwkMap.put("kty", "RSA"); jwkMap.put("n", "oahUIzUup5kqncCkHk5Zb1pRrLx7e6YtM-9jX1f5e6mHnZFkC2LJUZ0sEh0n5Y5KnQfW9s7d7gK2b8P0EEl0h3ZyHkWzA3YbsgzB4pDxP4RxMZ1I8xD2z3UvfA1zjvKDHz6wEweq4hVJ8nS8GzZJ2E_vb3s"); jwkMap.put("e", "AQAB"); - jwkMap.put("use", "enc"); + jwkMap.put("use", " "); EsignetException ex = Assertions.assertThrows(EsignetException.class, () -> IdentityProviderUtil.getJWKString(jwkMap)); diff --git a/esignet-service/src/main/java/io/mosip/esignet/config/AppConfig.java b/esignet-service/src/main/java/io/mosip/esignet/config/AppConfig.java index 546375fd7..a6e3b9745 100644 --- a/esignet-service/src/main/java/io/mosip/esignet/config/AppConfig.java +++ b/esignet-service/src/main/java/io/mosip/esignet/config/AppConfig.java @@ -9,6 +9,9 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.mosip.esignet.core.constants.Constants; +import io.mosip.esignet.core.dto.ServerProfile; +import io.mosip.esignet.core.exception.EsignetException; +import io.mosip.esignet.repository.ServerProfileRepository; import io.mosip.kernel.keymanagerservice.dto.KeyPairGenerateRequestDto; import io.mosip.kernel.keymanagerservice.dto.SymmetricKeyGenerateRequestDto; import io.mosip.kernel.keymanagerservice.service.KeymanagerService; @@ -26,6 +29,10 @@ import org.springframework.util.ObjectUtils; import org.springframework.web.client.RestTemplate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + @Configuration @Slf4j public class AppConfig implements ApplicationRunner { @@ -40,9 +47,15 @@ public class AppConfig implements ApplicationRunner { @Value("${mosip.esignet.cache.security.secretkey.reference-id}") private String cacheSecretKeyRefId; + @Value("${mosip.esignet.server.profile:none}") + private String serverProfile; + @Autowired private KeymanagerService keymanagerService; + @Autowired + private ServerProfileRepository serverProfileRepository; + @Bean public ObjectMapper objectMapper() { return JsonMapper.builder() @@ -63,6 +76,34 @@ public RestTemplate restTemplate() { return new RestTemplate(requestFactory); } + + /** + * Get the features associated with the profile + * name of the profile - fapi2.0. nisdsp, gov, none, etc. + */ + @Bean + public ServerProfile serverProfile() throws EsignetException { + ServerProfile profile = new ServerProfile(); + profile.setName(serverProfile); + final Map profileDataMap = new HashMap<>(); + profile.setFeatureMap(profileDataMap); + + if("none".equalsIgnoreCase(serverProfile)) { + return profile; + } + + List profiles = serverProfileRepository.findByProfileName(serverProfile); + if (profiles == null || profiles.isEmpty()) { + log.error("**** No features found for the configured server profile: {} ****", serverProfile); + throw new EsignetException("INVALID_SERVER_PROFILE"); + } + + for (io.mosip.esignet.entity.ServerProfile serverProfileEntity : profiles) { + profileDataMap.put(serverProfileEntity.getAdditionalConfigKey(), serverProfileEntity.getFeature()); + } + return profile; + } + @Override public void run(ApplicationArguments args) throws Exception { log.info("===================== IDP_SERVICE ROOT KEY CHECK ========================"); diff --git a/esignet-service/src/main/resources/application-default.properties b/esignet-service/src/main/resources/application-default.properties index 98b8a5db3..92317aa58 100644 --- a/esignet-service/src/main/resources/application-default.properties +++ b/esignet-service/src/main/resources/application-default.properties @@ -35,9 +35,6 @@ mosip.esignet.server.profile=none ## Time(in seconds) to keep the KBI spec in cache mosip.esignet.kbispec.ttl.seconds=18000 -## Time(in seconds) to keep the server profile in cache -mosip.esignet.server.profile.cache.ttl.seconds=18000 - ## Auth challenge type & format mapping. Auth challenge length validations for each auth factor type. mosip.esignet.auth-challenge.OTP.format=alpha-numeric mosip.esignet.auth-challenge.OTP.min-length=6 @@ -191,7 +188,7 @@ mosip.esignet.cache.security.algorithm-name=AES/ECB/PKCS5Padding mosip.esignet.cache.key.hash.algorithm=SHA3-256 mosip.esignet.cache.keyprefix=${mosip.esignet.namespace} -mosip.esignet.cache.names=clientdetails,preauth,authenticated,authcodegenerated,userinfo,linkcodegenerated,linked,linkedcode,linkedauth,consented,authtokens,bindingtransaction,apiratelimit,blocked,halted,nonce,par,jti,kbispec,serverprofile +mosip.esignet.cache.names=clientdetails,preauth,authenticated,authcodegenerated,userinfo,linkcodegenerated,linked,linkedcode,linkedauth,consented,authtokens,bindingtransaction,apiratelimit,blocked,halted,nonce,par,jti,kbispec # 'simple' cache type is only applicable only for Non-Production setup spring.cache.type=redis @@ -221,8 +218,7 @@ mosip.esignet.cache.size={'clientdetails' : 200, \ 'nonce' : 500, \ 'par' : 200, \ 'jti' : 200, \ -'kbispec': 1 ,\ -'serverprofile': 5} +'kbispec': 1 } # Cache expire in seconds is applicable for both 'simple' and 'Redis' cache type # TTL of 'authtokens' cache depends on the auth token expire time acquired from IAM / MOSIP authmanager. @@ -244,8 +240,7 @@ mosip.esignet.cache.expire-in-seconds={'clientdetails' : 86400, \ 'nonce' : 86400, \ 'par' : ${mosip.esignet.par.expire-seconds},\ 'jti' : 86400 , \ -'kbispec': ${mosip.esignet.kbispec.ttl.seconds},\ -'serverprofile': ${mosip.esignet.server.profile.cache.ttl.seconds}} +'kbispec': ${mosip.esignet.kbispec.ttl.seconds}} ## ------------------------------------------ Discovery openid-configuration ------------------------------------------- diff --git a/esignet-service/src/test/java/io/mosip/esignet/AppConfigTest.java b/esignet-service/src/test/java/io/mosip/esignet/AppConfigTest.java new file mode 100644 index 000000000..a2b3f0fc4 --- /dev/null +++ b/esignet-service/src/test/java/io/mosip/esignet/AppConfigTest.java @@ -0,0 +1,82 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package io.mosip.esignet; + +import io.mosip.esignet.config.AppConfig; +import io.mosip.esignet.core.dto.ServerProfile; +import io.mosip.esignet.core.exception.EsignetException; +import io.mosip.esignet.repository.ServerProfileRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AppConfigTest { + + @Mock + private ServerProfileRepository serverProfileRepository; + + @InjectMocks + private AppConfig appConfig; + + @Value("${mosip.esignet.server.profile:none}") + private String serverProfile; + + @Test + void serverProfile_ServerProfileIsNone_returnsWithNoFeatures() throws EsignetException { + ReflectionTestUtils.setField(appConfig, "serverProfile", "none"); + + ServerProfile result = appConfig.serverProfile(); + + assertNotNull(result); + assertEquals("none", result.getName()); + assertTrue(result.getFeatureMap().isEmpty()); + } + + @Test + void serverProfile_NoProfilesFound_ThrowsException() { + ReflectionTestUtils.setField(appConfig, "serverProfile", "invalidProfile"); + when(serverProfileRepository.findByProfileName("invalidProfile")).thenReturn(Collections.emptyList()); + + EsignetException exception = assertThrows(EsignetException.class, () -> appConfig.serverProfile()); + assertEquals("INVALID_SERVER_PROFILE", exception.getMessage()); + } + + @Test + void serverProfile_NullProfile_ThrowsException() { + ReflectionTestUtils.setField(appConfig, "serverProfile", null); + + EsignetException exception = assertThrows(EsignetException.class, () -> appConfig.serverProfile()); + assertEquals("INVALID_SERVER_PROFILE", exception.getMessage()); + } + + @Test + void serverProfile_ProfilesExist_ReturnsProfileWithFeatures() throws EsignetException { + ReflectionTestUtils.setField(appConfig, "serverProfile", "gov"); + io.mosip.esignet.entity.ServerProfile profileEntity = new io.mosip.esignet.entity.ServerProfile(); + profileEntity.setAdditionalConfigKey("require_pkce"); + profileEntity.setFeature("PKCE"); + when(serverProfileRepository.findByProfileName("gov")) + .thenReturn(Collections.singletonList(profileEntity)); + + ServerProfile result = appConfig.serverProfile(); + + assertNotNull(result); + assertEquals("gov", result.getName()); + assertEquals(1, result.getFeatureMap().size()); + assertEquals("PKCE", result.getFeatureMap().get("require_pkce")); + } +} + diff --git a/oidc-service-impl/src/main/java/io/mosip/esignet/repository/ServerProfileRepository.java b/oidc-service-impl/src/main/java/io/mosip/esignet/repository/ServerProfileRepository.java index c881bc75f..1de9ab807 100644 --- a/oidc-service-impl/src/main/java/io/mosip/esignet/repository/ServerProfileRepository.java +++ b/oidc-service-impl/src/main/java/io/mosip/esignet/repository/ServerProfileRepository.java @@ -7,12 +7,10 @@ import io.mosip.esignet.entity.ServerProfile; import io.mosip.esignet.entity.ServerProfileId; -import org.springframework.cache.annotation.Cacheable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface ServerProfileRepository extends JpaRepository { - @Cacheable(value = "serverprofile", key = "#profileName") List findByProfileName(String profileName); } diff --git a/oidc-service-impl/src/main/java/io/mosip/esignet/services/AuthorizationHelperService.java b/oidc-service-impl/src/main/java/io/mosip/esignet/services/AuthorizationHelperService.java index df829ec7a..010b80c8f 100644 --- a/oidc-service-impl/src/main/java/io/mosip/esignet/services/AuthorizationHelperService.java +++ b/oidc-service-impl/src/main/java/io/mosip/esignet/services/AuthorizationHelperService.java @@ -24,8 +24,6 @@ import io.mosip.esignet.core.exception.InvalidTransactionException; import io.mosip.esignet.core.spi.TokenService; import io.mosip.esignet.core.util.*; -import io.mosip.esignet.entity.ServerProfile; -import io.mosip.esignet.repository.ServerProfileRepository; import io.mosip.kernel.core.keymanager.spi.KeyStore; import io.mosip.kernel.keymanagerservice.constant.KeymanagerConstant; import io.mosip.kernel.keymanagerservice.entity.KeyAlias; @@ -131,8 +129,6 @@ public class AuthorizationHelperService { @Value("${mosip.esignet.signup-id-token-audience}") private String signupIDTokenAudience; - @Autowired - ServerProfileRepository serverProfileRepository; protected void validateSendOtpCaptchaToken(String captchaToken) { if(!captchaRequired.contains("send-otp")) { @@ -453,18 +449,4 @@ protected void validateNonce(String nonce) { private boolean isLocalEnvironment() { return Arrays.stream(environment.getActiveProfiles()).anyMatch(env -> env.equalsIgnoreCase("local")); } - - /** - * Get the features associated with the profile - * @param profileName name of the profile - fapi2.0. nisdsp, gov, none etc - * @return map of features associated with the profile - */ - public Map getFeaturesByProfileName(String profileName) { - List profiles = serverProfileRepository.findByProfileName(profileName); - if (profiles == null || profiles.isEmpty()) { - throw new EsignetException("No features found for openid profile: " + profileName); - } - return profiles.stream() - .collect(Collectors.toMap(ServerProfile::getAdditionalConfigKey, ServerProfile::getFeature)); - } } diff --git a/oidc-service-impl/src/main/java/io/mosip/esignet/services/AuthorizationServiceImpl.java b/oidc-service-impl/src/main/java/io/mosip/esignet/services/AuthorizationServiceImpl.java index 3c02d7e6d..fb95493e7 100644 --- a/oidc-service-impl/src/main/java/io/mosip/esignet/services/AuthorizationServiceImpl.java +++ b/oidc-service-impl/src/main/java/io/mosip/esignet/services/AuthorizationServiceImpl.java @@ -6,7 +6,6 @@ package io.mosip.esignet.services; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.mosip.esignet.api.dto.claim.*; @@ -60,33 +59,6 @@ public class AuthorizationServiceImpl implements AuthorizationService { private static final String KBI_FIELD_DETAILS_CONFIG_KEY = "auth.factor.kbi.field-details"; - @Autowired - private ClientManagementService clientManagementService; - - @Autowired - private CacheUtilService cacheUtilService; - - @Autowired - private TokenService tokenService; - - @Autowired - private AuthenticationContextClassRefUtil authenticationContextClassRefUtil; - - @Autowired - private AuthorizationHelperService authorizationHelperService; - - @Autowired - private AuditPlugin auditWrapper; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private ConsentHelperService consentHelperService; - - @Autowired - private ClaimsHelperService claimsHelperService; - @Value("#{${mosip.esignet.ui.config.key-values}}") private HashMap uiConfigMap; @@ -100,9 +72,6 @@ public class AuthorizationServiceImpl implements AuthorizationService { @Value("${mosip.esignet.credential.scope.auto-permit:true}") private boolean autoPermitCredentialScopes; - @Value("${mosip.esignet.credential.mandate-pkce:true}") - private boolean mandatePKCEForVC; - @Value("#{'${mosip.esignet.captcha.required}'.split(',')}") private List captchaRequired; @@ -118,26 +87,50 @@ public class AuthorizationServiceImpl implements AuthorizationService { @Value("#{${mosip.esignet.authenticator.default.auth-factor.kbi.field-details}}") private List> fieldDetailList; + @Autowired + private ClientManagementService clientManagementService; + + @Autowired + private CacheUtilService cacheUtilService; + + @Autowired + private TokenService tokenService; + + @Autowired + private AuthenticationContextClassRefUtil authenticationContextClassRefUtil; + + @Autowired + private AuthorizationHelperService authorizationHelperService; + + @Autowired + private AuditPlugin auditWrapper; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ConsentHelperService consentHelperService; + + @Autowired + private ClaimsHelperService claimsHelperService; + @Autowired private ResourceLoader resourceLoader; @Autowired private KBIFormHelperService kbiFormHelperService; - @Value("${mosip.esignet.server.profile:none}") - private String serverProfile; + @Autowired + private ServerProfile serverProfile; + @Override public OAuthDetailResponseV1 getOauthDetails(OAuthDetailRequest oauthDetailReqDto) throws EsignetException { ClientDetail clientDetailDto = clientManagementService.getClientDetails(oauthDetailReqDto.getClientId()); - Map features = null; - if (serverProfile != null && !NONE.equalsIgnoreCase(serverProfile)) { - features = authorizationHelperService.getFeaturesByProfileName(serverProfile); - } - assertPARRequiredIsFalse(clientDetailDto, features); + assertPARRequiredIsFalse(clientDetailDto); validateRedirectURIAndNonce(oauthDetailReqDto, clientDetailDto); OAuthDetailResponseV1 oAuthDetailResponseV1 = new OAuthDetailResponseV1(); - Pair pair = checkAndBuildOIDCTransaction(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV1, features); + Pair pair = checkAndBuildOIDCTransaction(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV1); oAuthDetailResponseV1 = (OAuthDetailResponseV1) pair.getFirst(); oAuthDetailResponseV1.setClientName(clientDetailDto.getName().get(NONE_LANG_KEY)); pair.getSecond().setOauthDetailsHash(getOauthDetailsResponseHash(oAuthDetailResponseV1)); @@ -150,14 +143,10 @@ public OAuthDetailResponseV1 getOauthDetails(OAuthDetailRequest oauthDetailReqDt @Override public OAuthDetailResponseV2 getOauthDetailsV2(OAuthDetailRequestV2 oauthDetailReqDto) throws EsignetException { ClientDetail clientDetailDto = clientManagementService.getClientDetails(oauthDetailReqDto.getClientId()); - Map features = null; - if (serverProfile != null && !NONE.equalsIgnoreCase(serverProfile)) { - features = authorizationHelperService.getFeaturesByProfileName(serverProfile); - } - assertPARRequiredIsFalse(clientDetailDto, features); + assertPARRequiredIsFalse(clientDetailDto); validateRedirectURIAndNonce(oauthDetailReqDto, clientDetailDto); OAuthDetailResponseV2 oAuthDetailResponseV2 = new OAuthDetailResponseV2(); - return buildTransactionAndOAuthDetailResponse(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV2, features); + return buildTransactionAndOAuthDetailResponse(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV2); } @Override @@ -182,12 +171,8 @@ public OAuthDetailResponseV2 getPAROAuthDetails(PushedOAuthDetailRequest pushedO OAuthDetailRequestV3 oAuthDetailRequestV3 = mapPushedAuthorizationRequestToOAuthDetailsRequest(pushedAuthorizationRequest); handleIdTokenHint(oAuthDetailRequestV3, httpServletRequest); ClientDetail clientDetailDto = clientManagementService.getClientDetails(oAuthDetailRequestV3.getClientId()); - Map features = null; - if (serverProfile != null && !NONE.equalsIgnoreCase(serverProfile)) { - features = authorizationHelperService.getFeaturesByProfileName(serverProfile); - } OAuthDetailResponseV2 oAuthDetailResponseV2 = new OAuthDetailResponseV2(); - return buildTransactionAndOAuthDetailResponse(oAuthDetailRequestV3, clientDetailDto, oAuthDetailResponseV2, features); + return buildTransactionAndOAuthDetailResponse(oAuthDetailRequestV3, clientDetailDto, oAuthDetailResponseV2); } @Override @@ -425,18 +410,15 @@ private void validateRedirectURIAndNonce(OAuthDetailRequest oAuthDetailRequest, authorizationHelperService.validateNonce(oAuthDetailRequest.getNonce()); } - private void assertPARRequiredIsFalse(ClientDetail clientDetail, Map features) throws EsignetException { - boolean isParRequired = (serverProfile == null || NONE.equalsIgnoreCase(serverProfile)) - ? clientDetail.getAdditionalConfig(REQUIRE_PAR, false) - : (features!=null && features.containsKey(REQUIRE_PAR)); - if (isParRequired) { + private void assertPARRequiredIsFalse(ClientDetail clientDetail) throws EsignetException { + if (serverProfile.getFeatureMap().containsKey(REQUIRE_PAR) || clientDetail.getAdditionalConfig(REQUIRE_PAR, false)) { log.error("Pushed Authorization Request (PAR) flow is mandated for clientId: {}", clientDetail.getId()); throw new EsignetException(ErrorConstants.INVALID_REQUEST); } } private Pair checkAndBuildOIDCTransaction(OAuthDetailRequest oauthDetailReqDto, - ClientDetail clientDetailDto, OAuthDetailResponse oAuthDetailResponse, Map features) { + ClientDetail clientDetailDto, OAuthDetailResponse oAuthDetailResponse) { //Resolve the final set of claims based on registered and request parameter. Claims resolvedClaims = claimsHelperService.resolveRequestedClaims(oauthDetailReqDto, clientDetailDto); //Resolve and set ACR claim @@ -488,7 +470,7 @@ private Pair checkAndBuildOIDCTransaction( oidcTransaction.setPrompt(IdentityProviderUtil.splitAndTrimValue(oauthDetailReqDto.getPrompt(), Constants.SPACE)); oidcTransaction.setConsentExpireMinutes(clientDetailDto.getAdditionalConfig(CONSENT_EXPIRE_IN_MINS, 0)); oidcTransaction.setDpopJkt(oauthDetailReqDto.getDpopJkt()); - setAdditionalConfigInOidcTransaction(oidcTransaction, clientDetailDto, features); + setFeatureFlags(oidcTransaction, clientDetailDto); return Pair.of(oAuthDetailResponse, oidcTransaction); } @@ -504,9 +486,9 @@ private HashMap getUIConfig() { } private OAuthDetailResponseV2 buildTransactionAndOAuthDetailResponse(OAuthDetailRequestV2 oauthDetailReqDto, - ClientDetail clientDetailDto, OAuthDetailResponseV2 oAuthDetailResponseV2, Map features) { + ClientDetail clientDetailDto, OAuthDetailResponseV2 oAuthDetailResponseV2) { - Pair pair = checkAndBuildOIDCTransaction(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV2, features); + Pair pair = checkAndBuildOIDCTransaction(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV2); oAuthDetailResponseV2 = (OAuthDetailResponseV2) pair.getFirst(); oAuthDetailResponseV2.setClientName(clientDetailDto.getName()); @@ -521,10 +503,9 @@ private OAuthDetailResponseV2 buildTransactionAndOAuthDetailResponse(OAuthDetail oidcTransaction.setProofKeyCodeExchange(ProofKeyCodeExchange.getInstance(oauthDetailReqDto.getCodeChallenge(), oauthDetailReqDto.getCodeChallengeMethod())); - if(mandatePKCEForVC && CollectionUtils.isNotEmpty(oidcTransaction.getRequestedCredentialScopes()) && - oidcTransaction.getProofKeyCodeExchange() == null) { - log.error("PKCE is mandated for VC scoped transactions"); - throw new EsignetException(ErrorConstants.INVALID_PKCE_CHALLENGE); + if(oidcTransaction.isRequirePKCE() && oidcTransaction.getProofKeyCodeExchange() == null) { + log.error("PKCE is mandated for {}, but code challenge is not present in the request", clientDetailDto.getId()); + throw new EsignetException(ErrorConstants.USE_PKCE); } cacheUtilService.setTransaction(oAuthDetailResponseV2.getTransactionId(), pair.getSecond()); @@ -647,39 +628,29 @@ private void handleIdTokenHint(OAuthDetailRequestV3 oauthDetailReqDto, HttpServl } /** - * Set additional config in OIDC transaction based on openid profile and client additional config + * Set additional config in OIDC transaction based on configured server profile and client additional config. + * Priority is given to server profile config + * Second priority is given to client additional config + * Otherwise the default value is set to all the available features * @param oidcTransaction {@link OIDCTransaction} - * @param clientDetailDto {@link ClientDetail} - * @param features {@link List} + * @param clientDetail {@link ClientDetail} */ - private void setAdditionalConfigInOidcTransaction(OIDCTransaction oidcTransaction, ClientDetail clientDetailDto, Map features) { - final Map featureMap = (features == null) ? Map.of() : features; - - Map existingAdditionalConfigs = objectMapper.convertValue(clientDetailDto.getAdditionalConfig(), new TypeReference<>() {}); - Map resultMap = new HashMap<>(); - if( existingAdditionalConfigs != null) { - existingAdditionalConfigs.forEach((key, value) -> resultMap.put(key, value.toString())); - } - oidcTransaction.setAdditionalConfigMap(resultMap); - Map additionalConfigMap = oidcTransaction.getAdditionalConfigMap(); - if(features!=null && !features.isEmpty()) { - log.info("Setting additional config in OIDC transaction based on openid profile features: {}", featureMap); - for (String key : featureMap.keySet()) { - if (USERINFO_RESPONSE_TYPE.equals(key)) { - additionalConfigMap.put(key, featureMap.get(key)); - } else { - additionalConfigMap.put(key, "true"); - } - } - oidcTransaction.setAdditionalConfigMap(additionalConfigMap); - } - // if profile is set get it from overridden map or get it from existing configs - if(additionalConfigMap!=null && !additionalConfigMap.isEmpty()) { - oidcTransaction.setRequirePushedAuthorizationRequests(additionalConfigMap.containsKey(REQUIRE_PAR)); - oidcTransaction.setDpopBoundAccessToken(additionalConfigMap.containsKey(DPOP_BOUND_ACCESS_TOKENS)); - oidcTransaction.setRequirePKCE(additionalConfigMap.containsKey(REQUIRE_PKCE)); - oidcTransaction.setUserInfoResponseType(additionalConfigMap.get(USERINFO_RESPONSE_TYPE)); - } + void setFeatureFlags(OIDCTransaction oidcTransaction, ClientDetail clientDetail) { + oidcTransaction.setRequirePushedAuthorizationRequests(serverProfile.getFeatureMap().containsKey(REQUIRE_PAR) + || clientDetail.getAdditionalConfig(REQUIRE_PAR, false)); + oidcTransaction.setDpopBoundAccessToken(serverProfile.getFeatureMap().containsKey(DPOP_BOUND_ACCESS_TOKENS) + || clientDetail.getAdditionalConfig(DPOP_BOUND_ACCESS_TOKENS, false)); + oidcTransaction.setRequirePKCE(serverProfile.getFeatureMap().containsKey(REQUIRE_PKCE) + || clientDetail.getAdditionalConfig(REQUIRE_PKCE, false)); + oidcTransaction.setUserInfoResponseType(serverProfile.getFeatureMap().containsKey(USERINFO_RESPONSE_TYPE) ? + serverProfile.getFeatureMap().get(USERINFO_RESPONSE_TYPE) : + clientDetail.getAdditionalConfig(USERINFO_RESPONSE_TYPE, "JWS")); + + log.debug("Feature flags set in OIDC transaction -> PAR : {}, DPoP : {}, PKCE : {}, RespType : {}", + oidcTransaction.isRequirePushedAuthorizationRequests(), + oidcTransaction.isDpopBoundAccessToken(), + oidcTransaction.isRequirePKCE(), + oidcTransaction.getUserInfoResponseType()); } } diff --git a/oidc-service-impl/src/test/java/io/mosip/esignet/services/AuthorizationHelperServiceTest.java b/oidc-service-impl/src/test/java/io/mosip/esignet/services/AuthorizationHelperServiceTest.java index b099159b1..ec4e72bea 100644 --- a/oidc-service-impl/src/test/java/io/mosip/esignet/services/AuthorizationHelperServiceTest.java +++ b/oidc-service-impl/src/test/java/io/mosip/esignet/services/AuthorizationHelperServiceTest.java @@ -20,8 +20,6 @@ import io.mosip.esignet.core.spi.TokenService; import io.mosip.esignet.core.util.AuthenticationContextClassRefUtil; import io.mosip.esignet.core.util.CaptchaHelper; -import io.mosip.esignet.entity.ServerProfile; -import io.mosip.esignet.repository.ServerProfileRepository; import io.mosip.kernel.core.keymanager.spi.KeyStore; import io.mosip.kernel.keymanagerservice.entity.KeyAlias; import io.mosip.kernel.keymanagerservice.helper.KeymanagerDBHelper; @@ -90,9 +88,6 @@ public class AuthorizationHelperServiceTest { @Mock private HttpServletRequest httpServletRequest; - @Mock - private ServerProfileRepository serverProfileRepository; - ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach @@ -581,43 +576,4 @@ public void testHandleInternalAuthenticateRequest_NoHaltedTransaction_thenFail() Assertions.assertEquals("auth_failed", e.getErrorCode()); } } - - @Test - void getFeaturesByProfileName_thenPass() { - ServerProfile profile1 = mock(ServerProfile.class); - ServerProfile profile2 = mock(ServerProfile.class); - when(profile1.getFeature()).thenReturn("feature1"); - when(profile1.getAdditionalConfigKey()).thenReturn("configKey1"); - when(profile2.getFeature()).thenReturn("feature2"); - when(profile2.getAdditionalConfigKey()).thenReturn("configKey2"); - when(serverProfileRepository.findByProfileName("profileA")) - .thenReturn(Arrays.asList(profile1, profile2)); - - Map features = authorizationHelperService.getFeaturesByProfileName("profileA"); - assertEquals(2, features.size()); - assertTrue(features.containsKey("configKey1")); - assertTrue(features.containsKey("configKey2")); - assertEquals("feature1", features.get("configKey1")); - assertEquals("feature2", features.get("configKey2")); - } - - @Test - void getFeaturesByProfileName_thenThrowException() { - when(serverProfileRepository.findByProfileName("nonexistent")) - .thenReturn(Collections.emptyList()); - - EsignetException ex = assertThrows(EsignetException.class, () -> - authorizationHelperService.getFeaturesByProfileName("nonexistent")); - assertTrue(ex.getMessage().contains("No features found for openid profile: nonexistent")); - } - - @Test - void getFeaturesByProfileName_whenNull_thenThrowException() { - when(serverProfileRepository.findByProfileName(null)) - .thenReturn(Collections.emptyList()); - - EsignetException ex = assertThrows(EsignetException.class, () -> - authorizationHelperService.getFeaturesByProfileName(null)); - assertTrue(ex.getMessage().contains("No features found for openid profile: null")); - } } diff --git a/oidc-service-impl/src/test/java/io/mosip/esignet/services/AuthorizationServiceTest.java b/oidc-service-impl/src/test/java/io/mosip/esignet/services/AuthorizationServiceTest.java index 061b74c35..68a407e80 100644 --- a/oidc-service-impl/src/test/java/io/mosip/esignet/services/AuthorizationServiceTest.java +++ b/oidc-service-impl/src/test/java/io/mosip/esignet/services/AuthorizationServiceTest.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import io.mosip.esignet.api.dto.AuthChallenge; @@ -43,6 +44,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.mockito.verification.VerificationMode; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.mock.web.MockHttpServletRequest; @@ -110,6 +112,9 @@ public class AuthorizationServiceTest { @Mock KBIFormHelperService kbiFormHelperService; + @Mock + private ServerProfile serverProfile; + private final ObjectMapper objectMapper = new ObjectMapper(); private static final String CONFIG_KEY = "auth.factor.kbi.field-details"; @@ -919,6 +924,9 @@ public void getOauthDetailsV2_withoutPKCE_thenFail() { clientDetail.setRedirectUris(Arrays.asList("https://localshot:3044/logo.png", "http://localhost:8088/v1/idp", "/v1/idp")); clientDetail.setClaims(Arrays.asList("email", "given_name")); clientDetail.setAcrValues(Arrays.asList("mosip:idp:acr:generated-code", "mosip:idp:acr:wallet")); + ObjectNode additionalConfig = objectMapper.createObjectNode(); + additionalConfig.put(REQUIRE_PKCE, true); + clientDetail.setAdditionalConfig(additionalConfig); OAuthDetailRequestV2 oauthDetailRequest = new OAuthDetailRequestV2(); oauthDetailRequest.setClientId("34567"); @@ -941,11 +949,10 @@ public void getOauthDetailsV2_withoutPKCE_thenFail() { when(authenticationContextClassRefUtil.getAuthFactors(new String[]{"mosip:idp:acr:wallet"})).thenReturn(authFactors); try { - ReflectionTestUtils.setField(authorizationServiceImpl, "mandatePKCEForVC", true); authorizationServiceImpl.getOauthDetailsV2(oauthDetailRequest); Assertions.fail(); } catch (EsignetException e) { - Assertions.assertEquals(ErrorConstants.INVALID_PKCE_CHALLENGE, e.getErrorCode()); + Assertions.assertEquals(ErrorConstants.USE_PKCE, e.getErrorCode()); } } @@ -2219,6 +2226,131 @@ public void getOauthDetails_migrateKBIFieldDetails_withUIConfig_thenPass() throw verify(kbiFormHelperService).migrateKBIFieldDetails(any()); } + @Test + void setFeatureFlags_withServerProfileFeaturesEnabled_shouldSetFlagsCorrectly() { + Map featureMap = Map.of( + REQUIRE_PAR, "PAR", + DPOP_BOUND_ACCESS_TOKENS, "DPOP", + REQUIRE_PKCE, "PKCE", + USERINFO_RESPONSE_TYPE, "JWE" + ); + when(serverProfile.getFeatureMap()).thenReturn(featureMap); + + ClientDetail clientDetail = getClientDetail(); + ObjectNode clientConfig = JsonNodeFactory.instance.objectNode(); + clientConfig.put(REQUIRE_PAR, true); + clientConfig.put(DPOP_BOUND_ACCESS_TOKENS, true); + clientConfig.put(REQUIRE_PKCE, true); + clientConfig.put(USERINFO_RESPONSE_TYPE, "JWT"); + clientDetail.setAdditionalConfig(clientConfig); + when(clientManagementService.getClientDetails(anyString())).thenReturn(clientDetail); + + OIDCTransaction oidcTransaction = new OIDCTransaction(); + authorizationServiceImpl.setFeatureFlags(oidcTransaction, clientDetail); + + Assertions.assertTrue(oidcTransaction.isRequirePushedAuthorizationRequests()); + Assertions.assertTrue(oidcTransaction.isDpopBoundAccessToken()); + Assertions.assertTrue(oidcTransaction.isRequirePKCE()); + Assertions.assertEquals("JWE", oidcTransaction.getUserInfoResponseType()); + } + + @Test + void setFeatureFlags_withClientAdditionalConfigEnabled_shouldOverrideServerProfile() { + Map featureMap = Map.of(); + when(serverProfile.getFeatureMap()).thenReturn(featureMap); + + ClientDetail clientDetail = getClientDetail(); + ObjectNode clientConfig = JsonNodeFactory.instance.objectNode(); + clientConfig.put(REQUIRE_PAR, true); + clientConfig.put(DPOP_BOUND_ACCESS_TOKENS, true); + clientConfig.put(REQUIRE_PKCE, true); + clientConfig.put(USERINFO_RESPONSE_TYPE, "JWT"); + clientDetail.setAdditionalConfig(clientConfig); + when(clientManagementService.getClientDetails(anyString())).thenReturn(clientDetail); + + OIDCTransaction oidcTransaction = new OIDCTransaction(); + authorizationServiceImpl.setFeatureFlags(oidcTransaction, clientDetail); + + Assertions.assertTrue(oidcTransaction.isRequirePushedAuthorizationRequests()); + Assertions.assertTrue(oidcTransaction.isDpopBoundAccessToken()); + Assertions.assertTrue(oidcTransaction.isRequirePKCE()); + Assertions.assertEquals("JWT", oidcTransaction.getUserInfoResponseType()); + } + + @Test + void setFeatureFlags_withNoFeaturesEnabled_shouldSetDefaultValues() { + Map featureMap = Map.of(); + when(serverProfile.getFeatureMap()).thenReturn(featureMap); + + OIDCTransaction oidcTransaction = new OIDCTransaction(); + + ClientDetail clientDetail = getClientDetail(); + ObjectNode clientConfig = JsonNodeFactory.instance.objectNode(); + clientConfig.put(REQUIRE_PAR, false); + clientConfig.put(REQUIRE_PKCE, false); + clientDetail.setAdditionalConfig(clientConfig); + when(clientManagementService.getClientDetails(anyString())).thenReturn(clientDetail); + + authorizationServiceImpl.setFeatureFlags(oidcTransaction, clientDetail); + + Assertions.assertFalse(oidcTransaction.isRequirePushedAuthorizationRequests()); + Assertions.assertFalse(oidcTransaction.isDpopBoundAccessToken()); + Assertions.assertFalse(oidcTransaction.isRequirePKCE()); + Assertions.assertEquals("JWS", oidcTransaction.getUserInfoResponseType()); + } + + @Test + void getOauthDetails_withPAREnabledInProfile_shouldMandatePAR() { + OAuthDetailRequest request = new OAuthDetailRequest(); + request.setClientId("client123"); + request.setRedirectUri("https://callback.com"); + request.setAcrValues("mosip:idp:acr:biometrics mosip:idp:acr:generated-code"); + + ClientDetail clientDetail = mock(ClientDetail.class); + ObjectNode clientConfig = mock(ObjectNode.class); + when(clientDetail.getAdditionalConfig()).thenReturn(clientConfig); + when(clientManagementService.getClientDetails("client123")).thenReturn(clientDetail); + when(authenticationContextClassRefUtil.getAuthFactors(any())).thenReturn(Collections.emptyList()); + + Map featureMap = Map.of( + REQUIRE_PAR, "PAR" + ); + when(serverProfile.getFeatureMap()).thenReturn(featureMap); + + try { + authorizationServiceImpl.getOauthDetails(request); + } catch (EsignetException e) { + Assertions.assertEquals(ErrorConstants.INVALID_REQUEST, e.getErrorCode()); + } + + verify(serverProfile, atMostOnce()).getFeatureMap(); + verify(clientDetail, never()).getAdditionalConfig(); + } + + @Test + void getOauthDetails_withPARDisabledInProfileAndEnabledInClientConfig_shouldMandatePAR() { + OAuthDetailRequest request = new OAuthDetailRequest(); + request.setClientId("client123"); + request.setRedirectUri("https://callback.com"); + request.setAcrValues("mosip:idp:acr:biometrics mosip:idp:acr:generated-code"); + + ClientDetail clientDetail = mock(ClientDetail.class); + when(clientDetail.getAdditionalConfig(REQUIRE_PAR, false)).thenReturn(true); + when(clientManagementService.getClientDetails("client123")).thenReturn(clientDetail); + + Map featureMap = Map.of(); + when(serverProfile.getFeatureMap()).thenReturn(featureMap); + + try { + authorizationServiceImpl.getOauthDetails(request); + } catch (EsignetException e) { + Assertions.assertEquals(ErrorConstants.INVALID_REQUEST, e.getErrorCode()); + } + + verify(serverProfile, atMostOnce()).getFeatureMap(); + verify(clientDetail, atMostOnce()).getAdditionalConfig(); + } + private ClientDetail getClientDetail() { ClientDetail clientDetail = new ClientDetail(); clientDetail.setId("client123"); diff --git a/postman-collection/eSignet.postman_collection.json b/postman-collection/eSignet.postman_collection.json index 97ffe5b22..1508ece77 100644 --- a/postman-collection/eSignet.postman_collection.json +++ b/postman-collection/eSignet.postman_collection.json @@ -271,6 +271,8 @@ "kp = pmlib.rs.KEYUTIL.generateKeypair(\"RSA\", 2048);", "privateKey_jwk = pmlib.rs.KEYUTIL.getJWK(kp.prvKeyObj);", "publicKey_jwk = pmlib.rs.KEYUTIL.getJWK(kp.pubKeyObj);", + "publicKey_jwk.alg = \"RS256\"; // Or your preferred algorithm", + "publicKey_jwk.use = \"sig\"; // Usually 'sig' for JWTs", "", "// Get the public key in PEM format", "const publicKeyPem = pmlib.rs.KEYUTIL.getPEM(kp.pubKeyObj, \"PKCS8PUB\");",