diff --git a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java index 1b94e7d8c..8ac61f164 100644 --- a/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java +++ b/client-management-service-impl/src/main/java/io/mosip/esignet/services/ClientManagementServiceImpl.java @@ -37,7 +37,7 @@ import java.time.ZoneId; import java.util.*; -import static io.mosip.esignet.core.constants.Constants.CLIENT_ACTIVE_STATUS; +import static io.mosip.esignet.core.constants.Constants.*; @Slf4j @Service @@ -321,5 +321,4 @@ public ClientDetail buildClient(String clientId, ClientDetailUpdateRequestV3 cli clientDetail.setAdditionalConfig(clientDetailUpdateRequestV3.getAdditionalConfig()); return clientDetail; } - } diff --git a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java index 5cfe740e1..4bd5e36e4 100644 --- a/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java +++ b/client-management-service-impl/src/test/java/io/mosip/esignet/ClientManagementServiceTest.java @@ -6,6 +6,7 @@ package io.mosip.esignet; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.KeyUse; @@ -23,11 +24,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.Spy; +import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -356,5 +355,4 @@ public static JWK generateJWK_RSA() { } return null; } - } \ No newline at end of file diff --git a/db_scripts/mosip_esignet/ddl.sql b/db_scripts/mosip_esignet/ddl.sql index 9abca2dd6..ce44c54b4 100644 --- a/db_scripts/mosip_esignet/ddl.sql +++ b/db_scripts/mosip_esignet/ddl.sql @@ -8,3 +8,4 @@ \ir ddl/esignet-consent.sql \ir ddl/esignet-consent_history.sql \ir ddl/esignet-ca_cert_store.sql +\ir ddl/esignet-openid_profile.sql \ No newline at end of file diff --git a/db_scripts/mosip_esignet/ddl/esignet-openid_profile.sql b/db_scripts/mosip_esignet/ddl/esignet-openid_profile.sql new file mode 100644 index 000000000..5a32085ab --- /dev/null +++ b/db_scripts/mosip_esignet/ddl/esignet-openid_profile.sql @@ -0,0 +1,27 @@ +-- 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/. +-- ------------------------------------------------------------------------------------------------- +-- Database Name: mosip_esignet +-- Table Name : openid_profile +-- Purpose : Openid profile: static table to store the profile and feature(as part of profile) mapping +-- +-- Create By : Md Humair K +-- Created Date : Nov-2025 +-- +-- Modified Date Modified By Comments / Remarks +-- ------------------------------------------------------------------------------------------ +-- ------------------------------------------------------------------------------------------ + +-- Table: openid_profile +CREATE TABLE IF NOT EXISTS openid_profile ( + profile_name VARCHAR(100) NOT NULL, + feature VARCHAR(100) NOT NULL, + additional_config_key VARCHAR(200) NOT NULL, + CONSTRAINT pk_openid_profile PRIMARY KEY (profile_name, feature) +); + +-- COMMENT ON TABLE openid_profile IS 'Static table for global configuration: profile name and feature mapping.'; +-- COMMENT ON COLUMN openid_profile.profile_name IS 'Profile name for configuration.'; +-- COMMENT ON COLUMN openid_profile.feature IS 'Feature enabled for the profile.'; +-- COMMENT ON COLUMN openid_profile.additional_config_key IS 'Additional config key name for the feature.'; diff --git a/db_scripts/mosip_esignet/dml.sql b/db_scripts/mosip_esignet/dml.sql index f894a3139..e9b858151 100644 --- a/db_scripts/mosip_esignet/dml.sql +++ b/db_scripts/mosip_esignet/dml.sql @@ -2,5 +2,8 @@ ----- TRUNCATE esignet.client_detail TABLE Data and It's reference Data and insert data from sql file ----- TRUNCATE TABLE esignet.client_detail cascade ; +TRUNCATE TABLE esignet.openid_profile CASCADE; -\ir dml/esignet-key_policy_def.sql \ No newline at end of file +\ir dml/esignet-key_policy_def.sql + +\ir dml/esignet-openid_profile.sql diff --git a/db_scripts/mosip_esignet/dml/esignet-openid_profile.sql b/db_scripts/mosip_esignet/dml/esignet-openid_profile.sql new file mode 100644 index 000000000..9232bc7ea --- /dev/null +++ b/db_scripts/mosip_esignet/dml/esignet-openid_profile.sql @@ -0,0 +1,4 @@ +INSERT INTO openid_profile (profile_name, feature, additional_config_key) VALUES +('fapi2.0', 'PAR', 'require_pushed_authorization_requests'), +('fapi2.0', 'DPOP', 'dpop_bound_access_tokens'), +('fapi2.0', 'JWE', 'userinfo_response_type'); \ No newline at end of file diff --git a/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_rollback.sql b/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_rollback.sql index 9aff7c3bd..23f2a7111 100644 --- a/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_rollback.sql +++ b/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_rollback.sql @@ -66,3 +66,5 @@ ALTER TABLE public_key_registry ALTER COLUMN thumbprint TYPE varchar; END; $$; + +DROP TABLE IF EXISTS openid_profile; \ No newline at end of file diff --git a/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_upgrade.sql b/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_upgrade.sql index 84dc9ea67..4ee4a232d 100644 --- a/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_upgrade.sql +++ b/db_upgrade_script/mosip_esignet/sql/1.7.1_to_1.8.0_upgrade.sql @@ -139,5 +139,16 @@ ALTER TABLE public_key_registry ALTER COLUMN public_key TYPE varchar(2500); ALTER TABLE public_key_registry ALTER COLUMN certificate TYPE varchar(4000); ALTER TABLE public_key_registry ALTER COLUMN thumbprint TYPE varchar(128); +CREATE TABLE IF NOT EXISTS openid_profile ( + profile_name VARCHAR(100) NOT NULL, + feature VARCHAR(100) NOT NULL, + additional_config_key VARCHAR(200) NOT NULL, + CONSTRAINT pk_openid_profile PRIMARY KEY (profile_name, feature) +); + +INSERT INTO esignet.openid_profile(profile_name, feature, additional_config_key) VALUES ('fapi2.0', 'PAR', 'require_pushed_authorization_requests'); +INSERT INTO esignet.openid_profile(profile_name, feature, additional_config_key) VALUES ('fapi2.0', 'DPOP', 'dpop_bound_access_tokens'); +INSERT INTO esignet.openid_profile(profile_name, feature, additional_config_key) VALUES ('fapi2.0', 'JWE', 'userinfo_response_type'); + END; -$$; +$$; \ No newline at end of file diff --git a/docker-compose/init.sql b/docker-compose/init.sql index 041977bbd..4550fdb94 100644 --- a/docker-compose/init.sql +++ b/docker-compose/init.sql @@ -165,6 +165,12 @@ CREATE TABLE esignet.ca_cert_store( CONSTRAINT cert_thumbprint_unique UNIQUE (cert_thumbprint,partner_domain) ); +CREATE TABLE IF NOT EXISTS esignet.openid_profile ( + profile_name VARCHAR(100) NOT NULL, + feature VARCHAR(100) NOT NULL, + additional_config_key VARCHAR(200) NOT NULL, + CONSTRAINT pk_openid_profile PRIMARY KEY (profile_name, feature) +); INSERT INTO esignet.KEY_POLICY_DEF(APP_ID,KEY_VALIDITY_DURATION,PRE_EXPIRE_DAYS,ACCESS_ALLOWED,IS_ACTIVE,CR_BY,CR_DTIMES) VALUES('ROOT', 2920, 1125, 'NA', true, 'mosipadmin', now()); INSERT INTO esignet.KEY_POLICY_DEF(APP_ID,KEY_VALIDITY_DURATION,PRE_EXPIRE_DAYS,ACCESS_ALLOWED,IS_ACTIVE,CR_BY,CR_DTIMES) VALUES('OIDC_SERVICE', 1095, 50, 'NA', true, 'mosipadmin', now()); @@ -172,6 +178,10 @@ INSERT INTO esignet.KEY_POLICY_DEF(APP_ID,KEY_VALIDITY_DURATION,PRE_EXPIRE_DAYS, INSERT INTO esignet.KEY_POLICY_DEF(APP_ID,KEY_VALIDITY_DURATION,PRE_EXPIRE_DAYS,ACCESS_ALLOWED,IS_ACTIVE,CR_BY,CR_DTIMES) VALUES('BINDING_SERVICE', 1095, 50, 'NA', true, 'mosipadmin', now()); INSERT INTO esignet.KEY_POLICY_DEF(APP_ID,KEY_VALIDITY_DURATION,PRE_EXPIRE_DAYS,ACCESS_ALLOWED,IS_ACTIVE,CR_BY,CR_DTIMES) VALUES('MOCK_BINDING_SERVICE', 1095, 50, 'NA', true, 'mosipadmin', now()); +INSERT INTO esignet.openid_profile(profile_name, feature, additional_config_key) VALUES ('fapi2.0', 'PAR', 'require_pushed_authorization_requests'); +INSERT INTO esignet.openid_profile(profile_name, feature, additional_config_key) VALUES ('fapi2.0', 'DPOP', 'dpop_bound_access_tokens'); +INSERT INTO esignet.openid_profile(profile_name, feature, additional_config_key) VALUES ('fapi2.0', 'JWE', 'userinfo_response_type'); + \c mosip_mockidentitysystem postgres diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/constants/Constants.java b/esignet-core/src/main/java/io/mosip/esignet/core/constants/Constants.java index b2bbc2fbe..ac78eb947 100644 --- a/esignet-core/src/main/java/io/mosip/esignet/core/constants/Constants.java +++ b/esignet-core/src/main/java/io/mosip/esignet/core/constants/Constants.java @@ -62,4 +62,8 @@ public class Constants { public static final String PAR_REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:"; public static final String JTI_CACHE = "jti"; + + public static final String REQUIRE_PAR= "require_pushed_authorization_requests"; + + public static final String NONE= "none"; } 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 ee6c75c8b..d106598ed 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 @@ -79,7 +79,10 @@ public class OIDCTransaction implements Serializable { String[] prompt; int consentExpireMinutes; + boolean requirePushedAuthorizationRequests; boolean dpopBoundAccessToken; + boolean requirePKCE; + Map additionalConfigMap; String dpopJkt; String dpopServerNonce; Long dpopServerNonceTTL; diff --git a/esignet-core/src/main/java/io/mosip/esignet/core/spi/ServerProfileService.java b/esignet-core/src/main/java/io/mosip/esignet/core/spi/ServerProfileService.java new file mode 100644 index 000000000..3d83857d5 --- /dev/null +++ b/esignet-core/src/main/java/io/mosip/esignet/core/spi/ServerProfileService.java @@ -0,0 +1,19 @@ +/* + * 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.spi; + +import io.mosip.esignet.core.exception.EsignetException; +import java.util.List; + +public interface ServerProfileService { + + /** + * Get the features associated with the profile + * @param profileName name of the profile - fapi2.0. nisdsp, gov, none etc + * @return list of features associated with the profile + */ + List getFeaturesByProfileName(String profileName) throws EsignetException; +} diff --git a/esignet-core/src/test/resources/additional_config_request_schema.json b/esignet-core/src/test/resources/additional_config_request_schema.json index 704c7ef6c..66b41139c 100644 --- a/esignet-core/src/test/resources/additional_config_request_schema.json +++ b/esignet-core/src/test/resources/additional_config_request_schema.json @@ -65,6 +65,9 @@ }, "dpop_bound_access_tokens": { "type": "boolean" + }, + "require_pkce": { + "type": "boolean" } }, "additionalProperties": false diff --git a/esignet-service/pom.xml b/esignet-service/pom.xml index 54a9129cf..ca484bf14 100644 --- a/esignet-service/pom.xml +++ b/esignet-service/pom.xml @@ -75,7 +75,7 @@ consent-service-impl ${project.version} - + diff --git a/esignet-service/src/main/resources/additional_config_request_schema.json b/esignet-service/src/main/resources/additional_config_request_schema.json index 704c7ef6c..66b41139c 100644 --- a/esignet-service/src/main/resources/additional_config_request_schema.json +++ b/esignet-service/src/main/resources/additional_config_request_schema.json @@ -65,6 +65,9 @@ }, "dpop_bound_access_tokens": { "type": "boolean" + }, + "require_pkce": { + "type": "boolean" } }, "additionalProperties": false diff --git a/esignet-service/src/main/resources/application-default.properties b/esignet-service/src/main/resources/application-default.properties index 6230edb75..98b8a5db3 100644 --- a/esignet-service/src/main/resources/application-default.properties +++ b/esignet-service/src/main/resources/application-default.properties @@ -29,9 +29,15 @@ mosip.esignet.dpop.header-filter.paths-to-validate={'${server.servlet.path}/oaut '${server.servlet.path}/oauth/v2/token', \ '${server.servlet.path}/oidc/userinfo' } +# Server profile can be either of fapi2.0, nisdsp, gov, none etc +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 @@ -185,7 +191,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 +mosip.esignet.cache.names=clientdetails,preauth,authenticated,authcodegenerated,userinfo,linkcodegenerated,linked,linkedcode,linkedauth,consented,authtokens,bindingtransaction,apiratelimit,blocked,halted,nonce,par,jti,kbispec,serverprofile # 'simple' cache type is only applicable only for Non-Production setup spring.cache.type=redis @@ -215,7 +221,8 @@ mosip.esignet.cache.size={'clientdetails' : 200, \ 'nonce' : 500, \ 'par' : 200, \ 'jti' : 200, \ -'kbispec': 1 } +'kbispec': 1 ,\ +'serverprofile': 5} # 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. @@ -237,7 +244,8 @@ mosip.esignet.cache.expire-in-seconds={'clientdetails' : 86400, \ 'nonce' : 86400, \ 'par' : ${mosip.esignet.par.expire-seconds},\ 'jti' : 86400 , \ -'kbispec': ${mosip.esignet.kbispec.ttl.seconds}} +'kbispec': ${mosip.esignet.kbispec.ttl.seconds},\ +'serverprofile': ${mosip.esignet.server.profile.cache.ttl.seconds}} ## ------------------------------------------ Discovery openid-configuration ------------------------------------------- diff --git a/esignet-service/src/main/resources/application-local.properties b/esignet-service/src/main/resources/application-local.properties index 0cdc00f56..8cf73e16f 100644 --- a/esignet-service/src/main/resources/application-local.properties +++ b/esignet-service/src/main/resources/application-local.properties @@ -27,14 +27,14 @@ kafka.enabled=true spring.kafka.bootstrap-servers=localhost:9093 ## Redis configuration -spring.cache.type=redis +spring.cache.type=simple spring.data.redis.host=localhost spring.data.redis.port=6379 spring.data.redis.password= ## Database configuration mosip.esignet.database.hostname=localhost -mosip.esignet.database.port=5455 +mosip.esignet.database.port=5432 mosip.esignet.database.name=mosip_esignet mosip.esignet.database.username=postgres mosip.esignet.database.password=postgres diff --git a/esignet-service/src/test/resources/additional_config_request_schema.json b/esignet-service/src/test/resources/additional_config_request_schema.json index 704c7ef6c..66b41139c 100644 --- a/esignet-service/src/test/resources/additional_config_request_schema.json +++ b/esignet-service/src/test/resources/additional_config_request_schema.json @@ -65,6 +65,9 @@ }, "dpop_bound_access_tokens": { "type": "boolean" + }, + "require_pkce": { + "type": "boolean" } }, "additionalProperties": false diff --git a/oidc-service-impl/src/main/java/io/mosip/esignet/entity/ServerProfile.java b/oidc-service-impl/src/main/java/io/mosip/esignet/entity/ServerProfile.java new file mode 100644 index 000000000..ac8b99be9 --- /dev/null +++ b/oidc-service-impl/src/main/java/io/mosip/esignet/entity/ServerProfile.java @@ -0,0 +1,32 @@ +/* + * 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.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.*; + +@Data +@Entity +@Table(name = "openid_profile") +@NoArgsConstructor +@AllArgsConstructor +@IdClass(ServerProfileId.class) +public class ServerProfile { + @Id + @Column(name = "profile_name", length = 100, nullable = false) + private String profileName; + + @Id + @Column(name = "feature", length = 100, nullable = false) + private String feature; + + @Column(name = "additional_config_key", length = 200, nullable = false) + private String additionalConfigKey; + +} diff --git a/oidc-service-impl/src/main/java/io/mosip/esignet/entity/ServerProfileId.java b/oidc-service-impl/src/main/java/io/mosip/esignet/entity/ServerProfileId.java new file mode 100644 index 000000000..2ce8b80e5 --- /dev/null +++ b/oidc-service-impl/src/main/java/io/mosip/esignet/entity/ServerProfileId.java @@ -0,0 +1,43 @@ +/* + * 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.entity; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +@Setter +@Getter +public class ServerProfileId implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private String profileName; + private String feature; + + public ServerProfileId() {} + + public ServerProfileId(String profileName, String feature) { + this.profileName = profileName; + this.feature = feature; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ServerProfileId)) return false; + ServerProfileId that = (ServerProfileId) o; + return Objects.equals(profileName, that.profileName) && + Objects.equals(feature, that.feature); + } + + @Override + public int hashCode() { + return Objects.hash(profileName, feature); + } +} 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 new file mode 100644 index 000000000..c881bc75f --- /dev/null +++ b/oidc-service-impl/src/main/java/io/mosip/esignet/repository/ServerProfileRepository.java @@ -0,0 +1,18 @@ +/* + * 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.repository; + +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 19b32b43e..df829ec7a 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,6 +24,8 @@ 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; @@ -129,6 +131,9 @@ 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")) { log.warn("captcha validation is disabled for send-otp request!"); @@ -448,4 +453,18 @@ 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 ce973c3df..122975791 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,6 +6,7 @@ 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.*; @@ -41,7 +42,6 @@ import java.util.*; import java.util.stream.Collectors; -import jakarta.annotation.PostConstruct; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -49,6 +49,7 @@ import java.util.UUID; import static io.mosip.esignet.core.constants.Constants.*; +import static io.mosip.esignet.core.constants.Constants.REQUIRE_PAR; import static io.mosip.esignet.core.spi.TokenService.ACR; import static io.mosip.esignet.core.util.IdentityProviderUtil.ALGO_SHA3_256; import static io.mosip.esignet.core.util.IdentityProviderUtil.ALGO_SHA_256; @@ -59,8 +60,6 @@ public class AuthorizationServiceImpl implements AuthorizationService { private static final String KBI_FIELD_DETAILS_CONFIG_KEY = "auth.factor.kbi.field-details"; - public static final String REQUIRE_PAR= "require_pushed_authorization_requests"; - @Autowired private ClientManagementService clientManagementService; @@ -125,15 +124,22 @@ public class AuthorizationServiceImpl implements AuthorizationService { @Autowired private KBIFormHelperService kbiFormHelperService; + @Value("${mosip.esignet.server.profile:none}") + private String openidProfile; + @Override public OAuthDetailResponseV1 getOauthDetails(OAuthDetailRequest oauthDetailReqDto) throws EsignetException { ClientDetail clientDetailDto = clientManagementService.getClientDetails(oauthDetailReqDto.getClientId()); - assertPARRequiredIsFalse(clientDetailDto); + Map features = null; + if (openidProfile != null && !NONE.equalsIgnoreCase(openidProfile)) { + features = authorizationHelperService.getFeaturesByProfileName(openidProfile); + } + assertPARRequiredIsFalse(clientDetailDto, features); validateRedirectURIAndNonce(oauthDetailReqDto, clientDetailDto); OAuthDetailResponseV1 oAuthDetailResponseV1 = new OAuthDetailResponseV1(); - Pair pair = checkAndBuildOIDCTransaction(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV1); + Pair pair = checkAndBuildOIDCTransaction(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV1, features); oAuthDetailResponseV1 = (OAuthDetailResponseV1) pair.getFirst(); - oAuthDetailResponseV1.setClientName(clientDetailDto.getName().get(Constants.NONE_LANG_KEY)); + oAuthDetailResponseV1.setClientName(clientDetailDto.getName().get(NONE_LANG_KEY)); pair.getSecond().setOauthDetailsHash(getOauthDetailsResponseHash(oAuthDetailResponseV1)); cacheUtilService.setTransaction(oAuthDetailResponseV1.getTransactionId(), pair.getSecond()); auditWrapper.logAudit(Action.TRANSACTION_STARTED, ActionStatus.SUCCESS, AuditHelper.buildAuditDto(oAuthDetailResponseV1.getTransactionId(), @@ -144,10 +150,14 @@ public OAuthDetailResponseV1 getOauthDetails(OAuthDetailRequest oauthDetailReqDt @Override public OAuthDetailResponseV2 getOauthDetailsV2(OAuthDetailRequestV2 oauthDetailReqDto) throws EsignetException { ClientDetail clientDetailDto = clientManagementService.getClientDetails(oauthDetailReqDto.getClientId()); - assertPARRequiredIsFalse(clientDetailDto); + Map features = null; + if (openidProfile != null && !NONE.equalsIgnoreCase(openidProfile)) { + features = authorizationHelperService.getFeaturesByProfileName(openidProfile); + } + assertPARRequiredIsFalse(clientDetailDto, features); validateRedirectURIAndNonce(oauthDetailReqDto, clientDetailDto); OAuthDetailResponseV2 oAuthDetailResponseV2 = new OAuthDetailResponseV2(); - return buildTransactionAndOAuthDetailResponse(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV2); + return buildTransactionAndOAuthDetailResponse(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV2, features); } @Override @@ -172,8 +182,12 @@ public OAuthDetailResponseV2 getPAROAuthDetails(PushedOAuthDetailRequest pushedO OAuthDetailRequestV3 oAuthDetailRequestV3 = mapPushedAuthorizationRequestToOAuthDetailsRequest(pushedAuthorizationRequest); handleIdTokenHint(oAuthDetailRequestV3, httpServletRequest); ClientDetail clientDetailDto = clientManagementService.getClientDetails(oAuthDetailRequestV3.getClientId()); + Map features = null; + if (openidProfile != null && !NONE.equalsIgnoreCase(openidProfile)) { + features = authorizationHelperService.getFeaturesByProfileName(openidProfile); + } OAuthDetailResponseV2 oAuthDetailResponseV2 = new OAuthDetailResponseV2(); - return buildTransactionAndOAuthDetailResponse(oAuthDetailRequestV3, clientDetailDto, oAuthDetailResponseV2); + return buildTransactionAndOAuthDetailResponse(oAuthDetailRequestV3, clientDetailDto, oAuthDetailResponseV2, features); } @Override @@ -411,8 +425,10 @@ private void validateRedirectURIAndNonce(OAuthDetailRequest oAuthDetailRequest, authorizationHelperService.validateNonce(oAuthDetailRequest.getNonce()); } - private void assertPARRequiredIsFalse(ClientDetail clientDetail) throws EsignetException { - boolean isParRequired = clientDetail.getAdditionalConfig(REQUIRE_PAR, false); + private void assertPARRequiredIsFalse(ClientDetail clientDetail, Map features) throws EsignetException { + boolean isParRequired = (openidProfile == null || NONE.equalsIgnoreCase(openidProfile)) + ? clientDetail.getAdditionalConfig(REQUIRE_PAR, false) + : (features!=null && features.containsKey(REQUIRE_PAR)); if (isParRequired) { log.error("Pushed Authorization Request (PAR) flow is mandated for clientId: {}", clientDetail.getId()); throw new EsignetException(ErrorConstants.INVALID_REQUEST); @@ -420,7 +436,7 @@ private void assertPARRequiredIsFalse(ClientDetail clientDetail) throws EsignetE } private Pair checkAndBuildOIDCTransaction(OAuthDetailRequest oauthDetailReqDto, - ClientDetail clientDetailDto, OAuthDetailResponse oAuthDetailResponse) { + ClientDetail clientDetailDto, OAuthDetailResponse oAuthDetailResponse, Map features) { //Resolve the final set of claims based on registered and request parameter. Claims resolvedClaims = claimsHelperService.resolveRequestedClaims(oauthDetailReqDto, clientDetailDto); //Resolve and set ACR claim @@ -469,11 +485,10 @@ private Pair checkAndBuildOIDCTransaction( oidcTransaction.setRequestedCredentialScopes(authorizationHelperService.getCredentialScopes(oauthDetailReqDto.getScope())); oidcTransaction.setInternalAuthSuccess(false); oidcTransaction.setRequestedClaimDetails(oauthDetailReqDto.getClaims()!=null? oauthDetailReqDto.getClaims().getUserinfo() : null); - oidcTransaction.setUserInfoResponseType(clientDetailDto.getAdditionalConfig(USERINFO_RESPONSE_TYPE,"JWS")); oidcTransaction.setPrompt(IdentityProviderUtil.splitAndTrimValue(oauthDetailReqDto.getPrompt(), Constants.SPACE)); oidcTransaction.setConsentExpireMinutes(clientDetailDto.getAdditionalConfig(CONSENT_EXPIRE_IN_MINS, 0)); oidcTransaction.setDpopJkt(oauthDetailReqDto.getDpopJkt()); - oidcTransaction.setDpopBoundAccessToken(clientDetailDto.getAdditionalConfig(Constants.DPOP_BOUND_ACCESS_TOKENS, false)); + setAdditionalConfigInOidcTransaction(oidcTransaction, clientDetailDto, features); return Pair.of(oAuthDetailResponse, oidcTransaction); } @@ -489,9 +504,9 @@ private HashMap getUIConfig() { } private OAuthDetailResponseV2 buildTransactionAndOAuthDetailResponse(OAuthDetailRequestV2 oauthDetailReqDto, - ClientDetail clientDetailDto, OAuthDetailResponseV2 oAuthDetailResponseV2) { + ClientDetail clientDetailDto, OAuthDetailResponseV2 oAuthDetailResponseV2, Map features) { - Pair pair = checkAndBuildOIDCTransaction(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV2); + Pair pair = checkAndBuildOIDCTransaction(oauthDetailReqDto, clientDetailDto, oAuthDetailResponseV2, features); oAuthDetailResponseV2 = (OAuthDetailResponseV2) pair.getFirst(); oAuthDetailResponseV2.setClientName(clientDetailDto.getName()); @@ -631,4 +646,32 @@ private void handleIdTokenHint(OAuthDetailRequestV3 oauthDetailReqDto, HttpServl } } + /** + * Set additional config in OIDC transaction based on openid profile and client additional config + * @param oidcTransaction {@link OIDCTransaction} + * @param clientDetailDto {@link ClientDetail} + * @param features {@link List} + */ + 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<>(); + existingAdditionalConfigs.forEach((key, value) -> resultMap.put(key, value.toString())); + oidcTransaction.setAdditionalConfigMap(resultMap); + + if(features!=null && !features.isEmpty()) { + Map additionalConfigMap = oidcTransaction.getAdditionalConfigMap(); + 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); + } + } + } diff --git a/oidc-service-impl/src/main/java/io/mosip/esignet/services/OAuthServiceImpl.java b/oidc-service-impl/src/main/java/io/mosip/esignet/services/OAuthServiceImpl.java index 88c34c388..1726d330b 100644 --- a/oidc-service-impl/src/main/java/io/mosip/esignet/services/OAuthServiceImpl.java +++ b/oidc-service-impl/src/main/java/io/mosip/esignet/services/OAuthServiceImpl.java @@ -112,7 +112,9 @@ public TokenResponse getTokens(TokenRequestV2 tokenRequest, String dpopHeader, b IdentityProviderUtil.validateRedirectURI(Collections.singletonList(transaction.getRedirectUri()), tokenRequest.getRedirect_uri()); - if (dpopHeader != null || transaction.isDpopBoundAccessToken()) { + if (dpopHeader != null || (Boolean.parseBoolean((transaction.getAdditionalConfigMap() != null + ? transaction.getAdditionalConfigMap().get(DPOP_BOUND_ACCESS_TOKENS) + : null)))) { if (dpopHeader == null) throw new EsignetException(INVALID_REQUEST); transaction.setDpopJkt(validateDpopJktThumbprint(dpopHeader,transaction.getDpopJkt())); if(!tokenService.isValidDpopServerNonce(dpopHeader, transaction)) { @@ -270,7 +272,9 @@ private TokenResponse getTokenResponse(OIDCTransaction transaction, boolean isTr String cNonce = isTransactionVCScoped ? securityHelperService.generateSecureRandomString(20) : null; tokenResponse.setAccess_token(tokenService.getAccessToken(transaction, cNonce)); tokenResponse.setExpires_in(accessTokenExpireSeconds); - if(transaction.isDpopBoundAccessToken()) { + if(Boolean.parseBoolean((transaction.getAdditionalConfigMap() != null + ? transaction.getAdditionalConfigMap().get(DPOP_BOUND_ACCESS_TOKENS) + : null))) { tokenResponse.setToken_type(DPOP); } else { tokenResponse.setToken_type(BEARER); @@ -322,7 +326,9 @@ private KycExchangeResult doKycExchange(OIDCTransaction transaction) { } } kycExchangeDto.setAcceptedClaimDetails(acceptedClaimDetails); - kycExchangeDto.setUserInfoResponseType(transaction.getUserInfoResponseType()); + kycExchangeDto.setUserInfoResponseType(transaction.getAdditionalConfigMap() != null + ? transaction.getAdditionalConfigMap().get("dpop_bound_access_tokens") + : null); if(transaction.isInternalAuthSuccess()) { log.info("Internal kyc exchange is invoked as the transaction is marked as internal auth success"); diff --git a/oidc-service-impl/src/main/java/io/mosip/esignet/services/OpenIdConnectServiceImpl.java b/oidc-service-impl/src/main/java/io/mosip/esignet/services/OpenIdConnectServiceImpl.java index 5a6b2928e..7eb1b577c 100644 --- a/oidc-service-impl/src/main/java/io/mosip/esignet/services/OpenIdConnectServiceImpl.java +++ b/oidc-service-impl/src/main/java/io/mosip/esignet/services/OpenIdConnectServiceImpl.java @@ -23,6 +23,8 @@ import java.util.Map; +import static io.mosip.esignet.core.constants.Constants.DPOP_BOUND_ACCESS_TOKENS; + @Slf4j @Service public class OpenIdConnectServiceImpl implements OpenIdConnectService { @@ -51,7 +53,9 @@ public String getUserInfo(String accessToken, String dpopHeader) throws EsignetE if(transaction == null) throw new NotAuthenticatedException(); - if(transaction.isDpopBoundAccessToken() && !tokenService.isValidDpopServerNonce(dpopHeader, transaction)) { + if((Boolean.parseBoolean((transaction.getAdditionalConfigMap() != null + ? transaction.getAdditionalConfigMap().get(DPOP_BOUND_ACCESS_TOKENS) + : null))) && !tokenService.isValidDpopServerNonce(dpopHeader, transaction)) { tokenService.generateAndStoreNewNonce(accessTokenHash, Constants.USERINFO_CACHE); } diff --git a/oidc-service-impl/src/main/java/io/mosip/esignet/services/TokenServiceImpl.java b/oidc-service-impl/src/main/java/io/mosip/esignet/services/TokenServiceImpl.java index 23699197b..62102024f 100644 --- a/oidc-service-impl/src/main/java/io/mosip/esignet/services/TokenServiceImpl.java +++ b/oidc-service-impl/src/main/java/io/mosip/esignet/services/TokenServiceImpl.java @@ -43,6 +43,7 @@ import java.time.Instant; import java.util.*; +import static io.mosip.esignet.core.constants.Constants.DPOP_BOUND_ACCESS_TOKENS; import static io.mosip.esignet.core.constants.Constants.SPACE; @Slf4j @@ -155,7 +156,9 @@ public String getAccessToken(OIDCTransaction transaction, String cNonce) { payload.put(C_NONCE, cNonce); payload.put(C_NONCE_EXPIRES_IN, cNonceExpireSeconds); } - if(transaction.isDpopBoundAccessToken()) { + if (Boolean.parseBoolean((transaction.getAdditionalConfigMap() != null + ? transaction.getAdditionalConfigMap().get(DPOP_BOUND_ACCESS_TOKENS) + : null))) { payload.put(CNF, Map.of(JKT, transaction.getDpopJkt())); } return getSignedJWT(Constants.OIDC_SERVICE_APP_ID, payload); 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 f1eaac358..ee01f393c 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,6 +20,8 @@ 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; @@ -30,6 +32,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.context.request.async.DeferredResult; @@ -47,8 +50,12 @@ import static io.mosip.esignet.core.constants.ErrorConstants.*; import static io.mosip.esignet.core.spi.TokenService.ACR; import static io.mosip.kernel.keymanagerservice.constant.KeymanagerConstant.CURRENTKEYALIAS; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class AuthorizationHelperServiceTest { @@ -83,6 +90,9 @@ public class AuthorizationHelperServiceTest { @Mock private HttpServletRequest httpServletRequest; + @Mock + private ServerProfileRepository serverProfileRepository; + ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach @@ -95,6 +105,7 @@ public void setUp() { ReflectionTestUtils.setField(claimsHelperService, "claims", claims); ReflectionTestUtils.setField(claimsHelperService, "objectMapper", objectMapper); ReflectionTestUtils.setField(authorizationHelperService, "claimsHelperService", claimsHelperService); + MockitoAnnotations.openMocks(this); } @Test @@ -570,4 +581,39 @@ 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(profile2.getFeature()).thenReturn("feature2"); + when(serverProfileRepository.findByProfileName("profileA")) + .thenReturn(Arrays.asList(profile1, profile2)); + + Map features = authorizationHelperService.getFeaturesByProfileName("profileA"); + assertEquals(2, features.size()); + assertTrue(features.containsKey("feature1")); + assertTrue(features.containsKey("feature2")); + } + + @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")); + } }