Skip to content
Merged

ES-2712 #1613

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/esignet-openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1986,6 +1986,7 @@ paths:
- invalid_prompt
- unsupported_pkce_challenge_method
- invalid_pkce_challenge
- use_pkce
errorMessage:
type: string
examples:
Expand Down Expand Up @@ -2233,6 +2234,7 @@ paths:
- invalid_pkce_challenge
- invalid_request
- invalid_id_token_hint
- use_pkce
errorMessage:
type: string
examples:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,20 @@ public class OIDCTransaction implements Serializable {
Map<String, List<JsonNode>> claimMetadata;
Map<String, JsonNode> 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<String, String> additionalConfigMap;
String dpopJkt;
String dpopServerNonce;
Long dpopServerNonceTTL;

//Feature flags
boolean requirePushedAuthorizationRequests;
boolean dpopBoundAccessToken;
boolean requirePKCE;
String userInfoResponseType;
}
Original file line number Diff line number Diff line change
@@ -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<String, String> featureMap;
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,21 +223,18 @@ public static String getJWKString(Map<String, Object> 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);
}

try {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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()
Expand All @@ -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<String, String> profileDataMap = new HashMap<>();
profile.setFeatureMap(profileDataMap);

if("none".equalsIgnoreCase(serverProfile)) {
return profile;
}

List<io.mosip.esignet.entity.ServerProfile> 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 ========================");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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 -------------------------------------------

Expand Down
82 changes: 82 additions & 0 deletions esignet-service/src/test/java/io/mosip/esignet/AppConfigTest.java
Original file line number Diff line number Diff line change
@@ -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"));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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<ServerProfile, ServerProfileId> {
@Cacheable(value = "serverprofile", key = "#profileName")
List<ServerProfile> findByProfileName(String profileName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")) {
Expand Down Expand Up @@ -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<String, String> getFeaturesByProfileName(String profileName) {
List<ServerProfile> 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));
}
}
Loading
Loading