diff --git a/certify-core/src/main/java/io/mosip/certify/core/constants/Constants.java b/certify-core/src/main/java/io/mosip/certify/core/constants/Constants.java index fd4ce3c3d..51fd6d2a5 100644 --- a/certify-core/src/main/java/io/mosip/certify/core/constants/Constants.java +++ b/certify-core/src/main/java/io/mosip/certify/core/constants/Constants.java @@ -45,4 +45,7 @@ public class Constants { public static final String PRE_AUTH_CODE_PREFIX = "pre_auth_code:"; public static final String CREDENTIAL_OFFER_PREFIX = "credential_offer:"; public static final String PRE_AUTHORIZED_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:pre-authorized_code"; + public static final String AS_METADATA_PREFIX = "as_metadata:"; + public static final String WELL_KNOWN_OAUTH_AS = "/.well-known/oauth-authorization-server"; + public static final String WELL_KNOWN_OIDC_CONFIG = "/.well-known/openid-configuration"; } diff --git a/certify-core/src/main/java/io/mosip/certify/core/constants/ErrorConstants.java b/certify-core/src/main/java/io/mosip/certify/core/constants/ErrorConstants.java index 4c57aeaa2..842e317d3 100644 --- a/certify-core/src/main/java/io/mosip/certify/core/constants/ErrorConstants.java +++ b/certify-core/src/main/java/io/mosip/certify/core/constants/ErrorConstants.java @@ -57,4 +57,8 @@ public class ErrorConstants { public static final String UNKNOWN_CLAIMS = "unknown_claims"; public static final String INVALID_EXPIRY_RANGE = "invalid_expiry_range"; public static final String INVALID_OFFER_ID_FORMAT = "invalid_offer_id_format"; + public static final String AUTHORIZATION_SERVER_DISCOVERY_FAILED = "authorization_server_discovery_failed"; + public static final String INVALID_AUTHORIZATION_SERVER = "invalid_authorization_server"; + public static final String AUTHORIZATION_SERVER_NOT_CONFIGURED = "authorization_server_not_configured"; + } diff --git a/certify-core/src/main/java/io/mosip/certify/core/dto/AuthorizationServerConfig.java b/certify-core/src/main/java/io/mosip/certify/core/dto/AuthorizationServerConfig.java new file mode 100644 index 000000000..a01359346 --- /dev/null +++ b/certify-core/src/main/java/io/mosip/certify/core/dto/AuthorizationServerConfig.java @@ -0,0 +1,26 @@ +package io.mosip.certify.core.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Configuration for a single authorization server + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthorizationServerConfig implements Serializable { + private static final long serialVersionUID = 1L; + + private String serverId; + private String serverUrl; + private boolean internal; + private String wellKnownUrl; + private long metadataCachedAt; + private AuthorizationServerMetadata metadata; +} \ No newline at end of file diff --git a/certify-core/src/main/java/io/mosip/certify/core/dto/AuthorizationServerMetadata.java b/certify-core/src/main/java/io/mosip/certify/core/dto/AuthorizationServerMetadata.java new file mode 100644 index 000000000..a092bb37b --- /dev/null +++ b/certify-core/src/main/java/io/mosip/certify/core/dto/AuthorizationServerMetadata.java @@ -0,0 +1,50 @@ +package io.mosip.certify.core.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Authorization Server Metadata as per RFC 8414 + * Source: https://www.rfc-editor.org/rfc/rfc8414.html + * Used for discovery via /.well-known/oauth-authorization-server + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AuthorizationServerMetadata { + + @JsonProperty("issuer") + private String issuer; + + @JsonProperty("token_endpoint") + private String tokenEndpoint; + + @JsonProperty("jwks_uri") + private String jwksUri; + + @JsonProperty("authorization_endpoint") + private String authorizationEndpoint; + + @JsonProperty("response_types_supported") + private List responseTypesSupported; + + @JsonProperty("grant_types_supported") + private List grantTypesSupported; + + @JsonProperty("token_endpoint_auth_methods_supported") + private List tokenEndpointAuthMethodsSupported; + + @JsonProperty("code_challenge_methods_supported") + private List codeChallengeMethodsSupported; + + @JsonProperty("scopes_supported") + private List scopesSupported; +} \ No newline at end of file diff --git a/certify-core/src/main/java/io/mosip/certify/core/dto/CredentialOfferResponse.java b/certify-core/src/main/java/io/mosip/certify/core/dto/CredentialOfferResponse.java index 4386371c4..d96b33d7d 100644 --- a/certify-core/src/main/java/io/mosip/certify/core/dto/CredentialOfferResponse.java +++ b/certify-core/src/main/java/io/mosip/certify/core/dto/CredentialOfferResponse.java @@ -22,4 +22,7 @@ public class CredentialOfferResponse { @JsonProperty("grants") private Grant grants; + + @JsonProperty("authorization_server") + private String authorizationServer; } \ No newline at end of file diff --git a/certify-core/src/main/java/io/mosip/certify/core/dto/Grant.java b/certify-core/src/main/java/io/mosip/certify/core/dto/Grant.java index dae8f7f98..4b39ffdff 100644 --- a/certify-core/src/main/java/io/mosip/certify/core/dto/Grant.java +++ b/certify-core/src/main/java/io/mosip/certify/core/dto/Grant.java @@ -13,13 +13,16 @@ public class Grant { @JsonProperty("urn:ietf:params:oauth:grant-type:pre-authorized_code") - private PreAuthorizedCodeGrant preAuthorizedCode; + private PreAuthorizedCodeGrantType preAuthorizedCode; + + @JsonProperty("authorization_code") + private AuthorizedCodeGrantType authorizationCode; @Data @Builder @AllArgsConstructor @NoArgsConstructor - public static class PreAuthorizedCodeGrant { + public static class PreAuthorizedCodeGrantType { @JsonProperty("pre-authorized_code") private String preAuthorizedCode; @@ -27,4 +30,17 @@ public static class PreAuthorizedCodeGrant { @JsonProperty("tx_code") private TxCode txCode; } + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class AuthorizedCodeGrantType { + + @JsonProperty("issuer_state") + private String issuerState; + + @JsonProperty("authorization_server") + private String authorizationServer; + } } \ No newline at end of file diff --git a/certify-service/src/main/java/io/mosip/certify/advice/ExceptionHandlerAdvice.java b/certify-service/src/main/java/io/mosip/certify/advice/ExceptionHandlerAdvice.java index f2ad606c4..5d7a673ef 100644 --- a/certify-service/src/main/java/io/mosip/certify/advice/ExceptionHandlerAdvice.java +++ b/certify-service/src/main/java/io/mosip/certify/advice/ExceptionHandlerAdvice.java @@ -118,15 +118,16 @@ private ResponseEntity handleInternalControllerException(Except } if(ex instanceof MissingServletRequestParameterException) { return new ResponseEntity(getResponseWrapper(INVALID_REQUEST, ex.getMessage()), - HttpStatus.OK); + HttpStatus.BAD_REQUEST); } if(ex instanceof HttpMediaTypeNotAcceptableException) { return new ResponseEntity(getResponseWrapper(INVALID_REQUEST, ex.getMessage()), - HttpStatus.OK); + HttpStatus.NOT_ACCEPTABLE); } if(ex instanceof CertifyException) { String errorCode = ((CertifyException) ex).getErrorCode(); - return new ResponseEntity(getResponseWrapper(errorCode, getMessage(errorCode)), HttpStatus.OK); + return new ResponseEntity(getResponseWrapper(errorCode, getMessage(errorCode)), + HttpStatus.BAD_REQUEST); } if(ex instanceof RenderingTemplateException) { return new ResponseEntity<>(getResponseWrapper(INVALID_REQUEST, ex.getMessage()) ,HttpStatus.NOT_FOUND); @@ -142,7 +143,8 @@ private ResponseEntity handleInternalControllerException(Except return new ResponseEntity(getResponseWrapper(HttpStatus.FORBIDDEN.name(), HttpStatus.FORBIDDEN.getReasonPhrase()), HttpStatus.FORBIDDEN); } - return new ResponseEntity(getResponseWrapper(UNKNOWN_ERROR, ex.getMessage()), HttpStatus.OK); + return new ResponseEntity(getResponseWrapper(UNKNOWN_ERROR, ex.getMessage()), + HttpStatus.INTERNAL_SERVER_ERROR); } public ResponseEntity handleVCIControllerExceptions(Exception ex) { diff --git a/certify-service/src/main/java/io/mosip/certify/controller/WellKnownController.java b/certify-service/src/main/java/io/mosip/certify/controller/WellKnownController.java index 756a64b4d..40312b164 100644 --- a/certify-service/src/main/java/io/mosip/certify/controller/WellKnownController.java +++ b/certify-service/src/main/java/io/mosip/certify/controller/WellKnownController.java @@ -1,13 +1,12 @@ package io.mosip.certify.controller; +import io.mosip.certify.core.dto.AuthorizationServerMetadata; import io.mosip.certify.core.dto.CredentialIssuerMetadataDTO; import io.mosip.certify.core.spi.CredentialConfigurationService; import io.mosip.certify.core.spi.VCIssuanceService; +import io.mosip.certify.services.AuthorizationServerService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.Map; @@ -21,15 +20,27 @@ public class WellKnownController { @Autowired private VCIssuanceService vcIssuanceService; + @Autowired + private AuthorizationServerService authorizationServerService; + @GetMapping(value = "/openid-credential-issuer", produces = "application/json") public CredentialIssuerMetadataDTO getCredentialIssuerMetadata( @RequestParam(name = "version", required = false, defaultValue = "latest") String version) { return credentialConfigurationService.fetchCredentialIssuerMetadata(version); } + @GetMapping(value = "/oauth-authorization-server", produces = "application/json") + public AuthorizationServerMetadata getAuthorizationServerMetadata() { + return authorizationServerService.getInternalAuthServerMetadata(); + } + + @GetMapping(value = "/openid-configuration", produces = "application/json") + public AuthorizationServerMetadata getOpenIDConfiguration() { + return authorizationServerService.getInternalAuthServerMetadata(); + } + @GetMapping(value = "/did.json") public Map getDIDDocument() { return vcIssuanceService.getDIDDocument(); } } - diff --git a/certify-service/src/main/java/io/mosip/certify/services/AuthorizationServerService.java b/certify-service/src/main/java/io/mosip/certify/services/AuthorizationServerService.java new file mode 100644 index 000000000..4374b7e68 --- /dev/null +++ b/certify-service/src/main/java/io/mosip/certify/services/AuthorizationServerService.java @@ -0,0 +1,327 @@ +package io.mosip.certify.services; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.certify.core.constants.Constants; +import io.mosip.certify.core.constants.ErrorConstants; +import io.mosip.certify.core.dto.AuthorizationServerConfig; +import io.mosip.certify.core.dto.AuthorizationServerMetadata; +import io.mosip.certify.core.exception.CertifyException; +import io.mosip.certify.core.exception.InvalidRequestException; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import io.mosip.certify.services.VCICacheService; + +import java.net.URI; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Service for discovering and caching authorization server metadata + */ +@Service +@Slf4j +public class AuthorizationServerService { + + @Autowired + private VCICacheService vciCacheService; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private RestTemplate restTemplate; + + @Value("${mosip.certify.authorization.discovery.retry-count:3}") + private int retryCount; + + @Value("${mosip.certify.authorization.servers:}") + private String authorizationServersConfig; + + @Value("${mosip.certify.authorization.internal.url:}") + private String internalAuthServerUrl; + + @Value("${mosip.certify.authorization.default-server:}") + private String defaultAuthServer; + + @Value("${mosip.certify.credential-config.as-mapping:{}}") + private String credentialConfigMappingJson; + + private List configuredServers; + private Map credentialConfigToASMapping; + + @PostConstruct + public void initialize() { + log.info("Initializing Authorization Server Management Service"); + + configuredServers = new ArrayList<>(); + loadConfiguredServers(); + loadCredentialConfigMappings(); + + log.info("Configured {} authorization servers", configuredServers.size()); + log.info("Loaded {} credential configuration mappings", credentialConfigToASMapping.size()); + } + + private void loadConfiguredServers() { + // Add internal auth server + if (StringUtils.hasText(internalAuthServerUrl)) { + AuthorizationServerConfig internal = AuthorizationServerConfig.builder() + .serverId("internal") + .serverUrl(internalAuthServerUrl) + .internal(true) + .build(); + configuredServers.add(internal); + log.info("Added internal authorization server: {}", internalAuthServerUrl); + } + + // Parse external auth servers + if (StringUtils.hasText(authorizationServersConfig)) { + String[] servers = authorizationServersConfig.split(","); + for (String serverUrl : servers) { + serverUrl = serverUrl.trim(); + if (StringUtils.hasText(serverUrl)) { + AuthorizationServerConfig config = AuthorizationServerConfig.builder() + .serverId(generateServerId(serverUrl)) + .serverUrl(serverUrl) + .internal(false) + .build(); + configuredServers.add(config); + log.info("Added external authorization server: {}", serverUrl); + } + } + } + + if (configuredServers.isEmpty()) { + log.warn("No authorization servers configured"); + } + } + + private void loadCredentialConfigMappings() { + credentialConfigToASMapping = new HashMap<>(); + + try { + if (StringUtils.hasText(credentialConfigMappingJson) && + !credentialConfigMappingJson.trim().equals("{}")) { + + Map mappings = objectMapper.readValue( + credentialConfigMappingJson, + new TypeReference>() { + }); + + credentialConfigToASMapping.putAll(mappings); + log.info("Loaded credential config mappings: {}", mappings); + } + } catch (Exception e) { + log.error("Failed to parse credential config mappings", e); + } + } + + /** + * Get internal authorization server metadata + */ + public AuthorizationServerMetadata getInternalAuthServerMetadata() { + // For internal server, we can construct metadata directly + AuthorizationServerMetadata metadata = new AuthorizationServerMetadata(); + metadata.setIssuer(normalizeUrl(internalAuthServerUrl)); + metadata.setTokenEndpoint(normalizeUrl(internalAuthServerUrl) + "/token"); + metadata.setAuthorizationEndpoint(normalizeUrl(internalAuthServerUrl) + "/authorize"); + metadata.setJwksUri(normalizeUrl(internalAuthServerUrl) + "/jwks.json"); + + return metadata; + } + + /** + * Discover authorization server metadata from well-known endpoint + */ + public AuthorizationServerMetadata discoverMetadata(String serverUrl) { + log.info("Discovering authorization server metadata for: {}", serverUrl); + + // Check cache first + AuthorizationServerMetadata cached = vciCacheService.getASMetadata(serverUrl); + if (cached != null) { + log.info("Using cached AS metadata for: {}", serverUrl); + return cached; + } + + // Try OIDC config first (per RFC 8414 compatibility notes), then OAuth AS + // discovery + AuthorizationServerMetadata metadata = tryDiscoveryEndpoint(serverUrl, Constants.WELL_KNOWN_OIDC_CONFIG); + if (metadata == null) { + log.info("OIDC configuration discovery failed, trying OAuth AS endpoint"); + metadata = tryDiscoveryEndpoint(serverUrl, Constants.WELL_KNOWN_OAUTH_AS); + } + + if (metadata == null) { + log.error("Failed to discover AS metadata for: {}", serverUrl); + throw new CertifyException(ErrorConstants.AUTHORIZATION_SERVER_DISCOVERY_FAILED, + "Could not discover authorization server metadata"); + } + + // Cache the metadata + vciCacheService.setASMetadata(serverUrl, metadata); + log.info("Successfully discovered and cached AS metadata for: {}", serverUrl); + + return metadata; + } + + private AuthorizationServerMetadata tryDiscoveryEndpoint(String serverUrl, String wellKnownPath) { + String discoveryUrl = normalizeUrl(serverUrl) + wellKnownPath; + + for (int attempt = 1; attempt <= retryCount; attempt++) { + try { + log.debug("Discovery attempt {} for URL: {}", attempt, discoveryUrl); + + ResponseEntity response = restTemplate.getForEntity(new URI(discoveryUrl), String.class); + + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + AuthorizationServerMetadata metadata = objectMapper.readValue( + response.getBody(), + AuthorizationServerMetadata.class); + + validateMetadata(metadata, serverUrl); + return metadata; + } + } catch (Exception e) { + log.warn("Discovery attempt {} failed for {}: {}", attempt, discoveryUrl, e.getMessage()); + if (attempt == retryCount) { + log.error("All discovery attempts failed for: {}", discoveryUrl, e); + } + } + } + return null; + } + + private void validateMetadata(AuthorizationServerMetadata metadata, String expectedIssuer) { + if (metadata == null + || !StringUtils.hasText(metadata.getIssuer()) + || !StringUtils.hasText(metadata.getTokenEndpoint())) { + throw new CertifyException(ErrorConstants.AUTHORIZATION_SERVER_DISCOVERY_FAILED); + } + + // Validate issuer matches expected URL + String normalizedExpected = normalizeUrl(expectedIssuer); + String normalizedActual = normalizeUrl(metadata.getIssuer()); + + if (!normalizedActual.equals(normalizedExpected)) { + log.warn("Issuer mismatch: expected {}, got {}", normalizedExpected, normalizedActual); + } + } + + /** + * Get token endpoint for a specific authorization server + */ + public String getTokenEndpoint(String serverUrl) { + AuthorizationServerMetadata metadata = discoverMetadata(serverUrl); + return metadata.getTokenEndpoint(); + } + + /** + * Get JWKS URI for a specific authorization server + */ + public String getJwksUri(String serverUrl) { + AuthorizationServerMetadata metadata = discoverMetadata(serverUrl); + return metadata.getJwksUri(); + } + + /** + * Check if authorization server supports pre-authorized code grant + */ + public boolean supportsPreAuthorizedCodeGrant(String serverUrl) { + try { + AuthorizationServerMetadata metadata = discoverMetadata(serverUrl); + List grantTypes = metadata.getGrantTypesSupported(); + return grantTypes != null && grantTypes.contains(Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE); + } catch (Exception e) { + log.warn("Could not check grant type support for {}: {}", serverUrl, e.getMessage()); + return false; + } + } + + /** + * Get authorization server for a specific credential configuration + */ + public String getAuthorizationServerForCredentialConfig(String credentialConfigId) { + log.debug("Getting authorization server for credential config: {}", credentialConfigId); + + // Check if there's a specific mapping + String mappedServerUrl = credentialConfigToASMapping.get(credentialConfigId); + if (StringUtils.hasText(mappedServerUrl)) { + validateServerConfigured(mappedServerUrl); + log.debug("Found mapped AS for {}: {}", credentialConfigId, mappedServerUrl); + return mappedServerUrl; + } + + // Use default server if configured + if (StringUtils.hasText(defaultAuthServer)) { + validateServerConfigured(defaultAuthServer); + log.debug("Using default AS for {}: {}", credentialConfigId, defaultAuthServer); + return defaultAuthServer; + } + + // Fall back to internal server + if (StringUtils.hasText(internalAuthServerUrl)) { + log.debug("Using internal AS for {}: {}", credentialConfigId, internalAuthServerUrl); + return internalAuthServerUrl; + } + + log.error("No authorization server found for credential config: {}", credentialConfigId); + throw new CertifyException(ErrorConstants.AUTHORIZATION_SERVER_NOT_CONFIGURED, + "No authorization server configured for credential configuration: " + credentialConfigId); + } + + /** + * Get all configured authorization server URLs + */ + public List getAllAuthorizationServerUrls() { + return configuredServers.stream() + .map(AuthorizationServerConfig::getServerUrl) + .collect(Collectors.toList()); + } + + /** + * Check if a server URL is configured + */ + public boolean isServerConfigured(String serverUrl) { + String normalized = normalizeUrl(serverUrl); + return configuredServers.stream() + .anyMatch(config -> normalizeUrl(config.getServerUrl()).equals(normalized)); + } + + private void validateServerConfigured(String serverUrl) { + if (!isServerConfigured(serverUrl)) { + log.error("Authorization server not configured: {}", serverUrl); + throw new InvalidRequestException(ErrorConstants.INVALID_AUTHORIZATION_SERVER); + } + } + + /** + * Normalize URL by removing trailing slashes + */ + private String normalizeUrl(String url) { + if (url == null) { + return ""; + } + return url.replaceAll("/+$", ""); + } + + /** + * Generate unique server ID from URL + */ + private String generateServerId(String serverUrl) { + try { + String normalized = normalizeUrl(serverUrl); + String domain = normalized.replaceAll("https?://", "") + .replaceAll("[^a-zA-Z0-9-]", "-"); + return "as-" + domain; + } catch (Exception e) { + return "as-" + UUID.randomUUID().toString(); + } + } +} \ No newline at end of file diff --git a/certify-service/src/main/java/io/mosip/certify/services/CredentialConfigurationServiceImpl.java b/certify-service/src/main/java/io/mosip/certify/services/CredentialConfigurationServiceImpl.java index be2cc2026..01ea3e869 100644 --- a/certify-service/src/main/java/io/mosip/certify/services/CredentialConfigurationServiceImpl.java +++ b/certify-service/src/main/java/io/mosip/certify/services/CredentialConfigurationServiceImpl.java @@ -39,6 +39,9 @@ public class CredentialConfigurationServiceImpl implements CredentialConfigurati @Autowired private CredentialConfigMapper credentialConfigMapper; + @Autowired + private AuthorizationServerService authServerService; + @Value("${mosip.certify.domain.url:}") private String credentialIssuer; @@ -269,7 +272,8 @@ public CredentialIssuerMetadataDTO fetchCredentialIssuerMetadata(String version) }); credentialIssuerMetadata.setCredentialConfigurationSupportedDTO(credentialConfigurationSupportedMap); credentialIssuerMetadata.setCredentialIssuer(credentialIssuer); - credentialIssuerMetadata.setAuthorizationServers(Collections.singletonList(authUrl)); + List authServers = authServerService.getAllAuthorizationServerUrls(); + credentialIssuerMetadata.setAuthorizationServers(authServers); String credentialEndpoint = credentialIssuer + servletPath + "/issuance" + (!version.equals("latest") ? "/" + version : "") + "/credential"; credentialIssuerMetadata.setCredentialEndpoint(credentialEndpoint); credentialIssuerMetadata.setDisplay(issuerDisplay); @@ -287,7 +291,9 @@ public CredentialIssuerMetadataDTO fetchCredentialIssuerMetadata(String version) }); credentialIssuerMetadata.setCredentialConfigurationSupportedDTO(credentialConfigurationSupportedMap); // Use a different setter for vd12 credentialIssuerMetadata.setCredentialIssuer(credentialIssuer); - credentialIssuerMetadata.setAuthorizationServers(Collections.singletonList(authUrl)); + // credentialIssuerMetadata.setAuthorizationServers(Collections.singletonList(authUrl)); + List authServers = authServerService.getAllAuthorizationServerUrls(); + credentialIssuerMetadata.setAuthorizationServers(authServers); String credentialEndpoint = credentialIssuer + servletPath + "/issuance/" + version + "/credential"; credentialIssuerMetadata.setCredentialEndpoint(credentialEndpoint); credentialIssuerMetadata.setDisplay(issuerDisplay); @@ -306,7 +312,8 @@ public CredentialIssuerMetadataDTO fetchCredentialIssuerMetadata(String version) }); credentialIssuerMetadata.setCredentialConfigurationSupportedDTO(credentialConfigurationSupportedList); // Use a different setter for vd11 credentialIssuerMetadata.setCredentialIssuer(credentialIssuer); - credentialIssuerMetadata.setAuthorizationServers(Collections.singletonList(authUrl)); + List authServers = authServerService.getAllAuthorizationServerUrls(); + credentialIssuerMetadata.setAuthorizationServers(authServers); String credentialEndpoint = credentialIssuer + servletPath + "/issuance/" + version + "/credential"; credentialIssuerMetadata.setCredentialEndpoint(credentialEndpoint); credentialIssuerMetadata.setDisplay(issuerDisplay); diff --git a/certify-service/src/main/java/io/mosip/certify/services/PreAuthorizedCodeService.java b/certify-service/src/main/java/io/mosip/certify/services/PreAuthorizedCodeService.java index 948d91f12..532da077e 100644 --- a/certify-service/src/main/java/io/mosip/certify/services/PreAuthorizedCodeService.java +++ b/certify-service/src/main/java/io/mosip/certify/services/PreAuthorizedCodeService.java @@ -5,6 +5,7 @@ import io.mosip.certify.core.dto.*; import io.mosip.certify.core.exception.CertifyException; import io.mosip.certify.core.exception.InvalidRequestException; +import io.mosip.certify.core.spi.CredentialConfigurationService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -23,6 +24,12 @@ public class PreAuthorizedCodeService { @Autowired private VCICacheService vciCacheService; + @Autowired + private CredentialConfigurationService credentialConfigurationService; + + @Autowired + private AuthorizationServerService authServerService; + @Value("${mosip.certify.identifier}") private String issuerIdentifier; @@ -51,12 +58,8 @@ public class PreAuthorizedCodeService { private static final String ALPHANUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; public String generatePreAuthorizedCode(PreAuthorizedRequest request) { - log.info("Generating pre-authorized code for credential configuration: {}", request.getCredentialConfigurationId()); - validatePreAuthorizedRequest(request); - int expirySeconds = request.getExpiresIn() != null ? request.getExpiresIn() : defaultExpirySeconds; - if (expirySeconds < minExpirySeconds || expirySeconds > maxExpirySeconds) { log.error("expires_in {} out of bounds [{}, {}]", expirySeconds, minExpirySeconds, maxExpirySeconds); throw new InvalidRequestException(ErrorConstants.INVALID_EXPIRY_RANGE); @@ -78,37 +81,64 @@ public String generatePreAuthorizedCode(PreAuthorizedRequest request) { CredentialOfferResponse offerResponse = buildCredentialOffer(request.getCredentialConfigurationId(), preAuthCode, request.getTxCode()); vciCacheService.setCredentialOffer(offerId, offerResponse); - String offerUri = buildCredentialOfferUri(offerId); - log.info("Successfully generated pre-authorized code with offer ID: {}", offerId); - - return offerUri; + return buildCredentialOfferUri(offerId); } private void validatePreAuthorizedRequest(PreAuthorizedRequest request) { - Map metadata = vciCacheService.getIssuerMetadata(); - Map supportedConfigs = (Map) metadata - .get(Constants.CREDENTIAL_CONFIGURATIONS_SUPPORTED); + // Map metadata = vciCacheService.getIssuerMetadata(); + CredentialIssuerMetadataDTO metadata = credentialConfigurationService.fetchCredentialIssuerMetadata("latest"); + // Map supportedConfigs = (Map) + // metadata.get(Constants.CREDENTIAL_CONFIGURATIONS_SUPPORTED); + Map supportedConfigs = metadata + .getCredentialConfigurationSupportedDTO(); if (supportedConfigs == null || !supportedConfigs.containsKey(request.getCredentialConfigurationId())) { log.error("Invalid credential configuration ID: {}", request.getCredentialConfigurationId()); throw new InvalidRequestException(ErrorConstants.INVALID_CREDENTIAL_CONFIGURATION_ID); } - Map config = (Map) supportedConfigs.get(request.getCredentialConfigurationId()); - Map requiredClaims = (Map) config.get(Constants.CLAIMS); - - validateClaims(requiredClaims, request.getClaims()); + CredentialConfigurationSupportedDTO config = supportedConfigs.get(request.getCredentialConfigurationId()); + validateClaims(config, request.getClaims()); } - private void validateClaims(Map requiredClaims, Map providedClaims) { - if (requiredClaims == null || requiredClaims.isEmpty()) { - return; - } - + private void validateClaims(CredentialConfigurationSupportedDTO config, Map providedClaims) { if (providedClaims == null) { providedClaims = Collections.emptyMap(); } + String format = config.getFormat(); + Set allowedClaimKeys; + + if ("ldp_vc".equals(format)) { + // For ldp_vc: claims are defined in credential_definition.credentialSubject + CredentialDefinition credDef = config.getCredentialDefinition(); + if (credDef != null && credDef.getCredentialSubject() != null) { + allowedClaimKeys = credDef.getCredentialSubject().keySet(); + } else { + return; // No claims defined, allow any + } + // For ldp_vc, just validate unknown claims (mandatory not supported in this structure) + List unknownClaims = new ArrayList<>(); + for (String providedClaim : providedClaims.keySet()) { + if (!allowedClaimKeys.contains(providedClaim)) { + unknownClaims.add(providedClaim); + } + } + if (!unknownClaims.isEmpty()) { + log.error("Unknown claims provided: {}", unknownClaims); + throw new InvalidRequestException(ErrorConstants.UNKNOWN_CLAIMS); + } + } else { + // For mso_mdoc, vc+sd-jwt: use top-level claims with mandatory checking + Map requiredClaims = config.getClaims(); + if (requiredClaims == null || requiredClaims.isEmpty()) { + return; + } + validateClaimsWithMandatory(requiredClaims, providedClaims); + } + } + + private void validateClaimsWithMandatory(Map requiredClaims, Map providedClaims) { List missingClaims = new ArrayList<>(); List unknownClaims = new ArrayList<>(); @@ -192,16 +222,19 @@ private String generateUniquePreAuthCode() { } private CredentialOfferResponse buildCredentialOffer(String configId, String preAuthCode, String txnCode) { - Grant.PreAuthorizedCodeGrant grant = Grant.PreAuthorizedCodeGrant.builder() + Grant.PreAuthorizedCodeGrantType grant = Grant.PreAuthorizedCodeGrantType.builder() .preAuthorizedCode(preAuthCode) .txCode(StringUtils.hasText(txnCode) ? buildTxCodeInfo(txnCode) : null).build(); + String authorizationServer = authServerService.getAuthorizationServerForCredentialConfig(configId); Grant grants = Grant.builder().preAuthorizedCode(grant).build(); return CredentialOfferResponse.builder() .credentialIssuer(issuerIdentifier) .credentialConfigurationIds(Collections.singletonList(configId)) - .grants(grants).build(); + .grants(grants) + .authorizationServer(authorizationServer) + .build(); } private TxCode buildTxCodeInfo(String txnCode) { diff --git a/certify-service/src/main/java/io/mosip/certify/services/VCICacheService.java b/certify-service/src/main/java/io/mosip/certify/services/VCICacheService.java index 9641278b6..832eb5206 100644 --- a/certify-service/src/main/java/io/mosip/certify/services/VCICacheService.java +++ b/certify-service/src/main/java/io/mosip/certify/services/VCICacheService.java @@ -2,10 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.mosip.certify.core.constants.Constants; -import io.mosip.certify.core.dto.CredentialOfferResponse; -import io.mosip.certify.core.dto.PreAuthCodeData; -import io.mosip.certify.core.dto.Transaction; -import io.mosip.certify.core.dto.VCIssuanceTransaction; +import io.mosip.certify.core.dto.*; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -19,6 +16,7 @@ import java.time.Duration; import java.util.HashMap; +import java.util.List; import java.util.Map; @Slf4j @@ -28,9 +26,6 @@ public class VCICacheService { @Autowired private CacheManager cacheManager; - @Autowired - private CredentialConfigurationServiceImpl credentialConfigurationService; - @Autowired private ObjectMapper objectMapper; @@ -112,46 +107,6 @@ public void setCredentialOffer(String offerId, CredentialOfferResponse offer) { } } - /** - * Get issuer metadata from cache. If not present, load from database. - */ - public Map getIssuerMetadata() { - Cache cache = cacheManager.getCache("issuerMetadataCache"); - if (cache == null) { - throw new IllegalStateException("issuerMetadataCache not available"); - } - Cache.ValueWrapper wrapper = cache.get(METADATA_KEY); - - if (wrapper == null) { - log.info("Issuer metadata not found in cache, loading from database..."); - try { - var metadata = credentialConfigurationService.fetchCredentialIssuerMetadata("latest"); - - // Convert DTOs to Map structure - Map metadataMap = new HashMap<>(); - Map credentialConfigsMap = new HashMap<>(); - - // Convert each CredentialConfigurationSupportedDTO to Map - metadata.getCredentialConfigurationSupportedDTO().forEach((configId, configDTO) -> { - Map configMap = objectMapper.convertValue(configDTO, Map.class); - credentialConfigsMap.put(configId, configMap); - }); - metadataMap.put(Constants.CREDENTIAL_CONFIGURATIONS_SUPPORTED, credentialConfigsMap); - - // Store in cache - cache.put(METADATA_KEY, metadataMap); - - log.info("Successfully loaded and cached issuer metadata with {} configurations", credentialConfigsMap.size()); - - return metadataMap; - } catch (Exception e) { - log.error("Failed to load issuer metadata", e); - throw new IllegalStateException("Failed to load issuer metadata", e); - } - } - return (Map) wrapper.get(); - } - public boolean isCodeBlacklisted(String code) { String key = "blacklist:" + code; Cache.ValueWrapper wrapper = cacheManager.getCache("preAuthCodeCache").get(key); @@ -195,4 +150,31 @@ public Transaction getTransactionByToken(String accessToken) { } return cache.get(accessToken, Transaction.class); } + + /** + * Cache authorization server metadata + */ + public void setASMetadata(String serverUrl, AuthorizationServerMetadata metadata) { + String key = Constants.AS_METADATA_PREFIX + serverUrl; + Cache cache = cacheManager.getCache("asMetadataCache"); + if (cache == null) { + throw new IllegalStateException("asMetadataCache not available"); + } + cache.put(key, metadata); + log.info("Cached AS metadata for: {}", serverUrl); + } + + /** + * Get cached authorization server metadata + */ + public AuthorizationServerMetadata getASMetadata(String serverUrl) { + String key = Constants.AS_METADATA_PREFIX + serverUrl; + Cache cache = cacheManager.getCache("asMetadataCache"); + if (cache == null) { + log.error("Cache {} not available", "asMetadataCache"); + return null; + } + Cache.ValueWrapper wrapper = cache.get(key); + return wrapper != null ? (AuthorizationServerMetadata) wrapper.get() : null; + } } \ No newline at end of file diff --git a/certify-service/src/test/java/io/mosip/certify/VCICacheServiceTest.java b/certify-service/src/test/java/io/mosip/certify/VCICacheServiceTest.java index 3492a606f..442ddf5e9 100644 --- a/certify-service/src/test/java/io/mosip/certify/VCICacheServiceTest.java +++ b/certify-service/src/test/java/io/mosip/certify/VCICacheServiceTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.mosip.certify.core.constants.Constants; +import io.mosip.certify.core.dto.AuthorizationServerMetadata; import io.mosip.certify.core.dto.CredentialIssuerMetadataVD13DTO; import io.mosip.certify.core.dto.CredentialOfferResponse; import io.mosip.certify.core.dto.PreAuthCodeData; @@ -118,32 +119,6 @@ public void getCredentialOffer_Success() { assertEquals(offer, result); } - @Test - public void getIssuerMetadata_CacheHit() { - Map metadata = new HashMap<>(); - metadata.put("key", "value"); - Cache.ValueWrapper wrapper = mock(Cache.ValueWrapper.class); - when(wrapper.get()).thenReturn(metadata); - when(cache.get("metadata")).thenReturn(wrapper); - - Map result = vciCacheService.getIssuerMetadata(); - assertEquals(metadata, result); - verify(credentialConfigurationService, never()).fetchCredentialIssuerMetadata(anyString()); - } - - @Test - public void getIssuerMetadata_CacheMiss() { - when(cache.get("metadata")).thenReturn(null); - CredentialIssuerMetadataVD13DTO metadataDTO = new CredentialIssuerMetadataVD13DTO(); - metadataDTO.setCredentialConfigurationSupportedDTO(new HashMap<>()); - when(credentialConfigurationService.fetchCredentialIssuerMetadata("latest")).thenReturn(metadataDTO); - - Map result = vciCacheService.getIssuerMetadata(); - assertNotNull(result); - verify(credentialConfigurationService).fetchCredentialIssuerMetadata("latest"); - verify(cache).put(eq("metadata"), anyMap()); - } - @Test public void validateCacheConfiguration_Simple() { ReflectionTestUtils.setField(vciCacheService, "cacheType", "simple"); @@ -201,13 +176,6 @@ public void setCredentialOffer_WhenCacheIsNull_ThrowsIllegalStateException() { vciCacheService.setCredentialOffer("test-offer-id", new CredentialOfferResponse()); } - @Test(expected = IllegalStateException.class) - public void getIssuerMetadata_WhenCacheIsNull_ThrowsIllegalStateException() { - when(cacheManager.getCache("issuerMetadataCache")).thenReturn(null); - - vciCacheService.getIssuerMetadata(); - } - @Test public void getCredentialOffer_WhenNotFound_ReturnsNull() { String offerId = "test-offer-id"; @@ -322,4 +290,72 @@ public void getTransactionByToken_WhenNotFound_ReturnsNull() { assertEquals(null, result); } + + // Tests for setASMetadata and getASMetadata + + private static final String AS_METADATA_CACHE = "asMetadataCache"; + + @Test + public void setASMetadata_Success() { + String serverUrl = "https://auth.example.com"; + AuthorizationServerMetadata metadata = AuthorizationServerMetadata.builder() + .issuer(serverUrl) + .tokenEndpoint(serverUrl + "/token") + .build(); + + when(cacheManager.getCache(AS_METADATA_CACHE)).thenReturn(cache); + + vciCacheService.setASMetadata(serverUrl, metadata); + + verify(cacheManager).getCache(AS_METADATA_CACHE); + verify(cache).put(eq(Constants.AS_METADATA_PREFIX + serverUrl), eq(metadata)); + } + + @Test(expected = IllegalStateException.class) + public void setASMetadata_WhenCacheIsNull_ThrowsIllegalStateException() { + when(cacheManager.getCache(AS_METADATA_CACHE)).thenReturn(null); + + vciCacheService.setASMetadata("https://auth.example.com", + AuthorizationServerMetadata.builder().build()); + } + + @Test + public void getASMetadata_CacheHit_ReturnsMetadata() { + String serverUrl = "https://auth.example.com"; + AuthorizationServerMetadata metadata = AuthorizationServerMetadata.builder() + .issuer(serverUrl) + .tokenEndpoint(serverUrl + "/token") + .build(); + + Cache.ValueWrapper wrapper = mock(Cache.ValueWrapper.class); + when(wrapper.get()).thenReturn(metadata); + when(cacheManager.getCache(AS_METADATA_CACHE)).thenReturn(cache); + when(cache.get(Constants.AS_METADATA_PREFIX + serverUrl)).thenReturn(wrapper); + + AuthorizationServerMetadata result = vciCacheService.getASMetadata(serverUrl); + + assertEquals(metadata, result); + verify(cache).get(Constants.AS_METADATA_PREFIX + serverUrl); + } + + @Test + public void getASMetadata_CacheMiss_ReturnsNull() { + String serverUrl = "https://auth.example.com"; + + when(cacheManager.getCache(AS_METADATA_CACHE)).thenReturn(cache); + when(cache.get(Constants.AS_METADATA_PREFIX + serverUrl)).thenReturn(null); + + AuthorizationServerMetadata result = vciCacheService.getASMetadata(serverUrl); + + assertEquals(null, result); + } + + @Test + public void getASMetadata_WhenCacheIsNull_ReturnsNull() { + when(cacheManager.getCache(AS_METADATA_CACHE)).thenReturn(null); + + AuthorizationServerMetadata result = vciCacheService.getASMetadata("https://auth.example.com"); + + assertEquals(null, result); + } } diff --git a/certify-service/src/test/java/io/mosip/certify/controller/PreAuthorizedCodeControllerTest.java b/certify-service/src/test/java/io/mosip/certify/controller/PreAuthorizedCodeControllerTest.java index c665e5bf6..d4074e883 100644 --- a/certify-service/src/test/java/io/mosip/certify/controller/PreAuthorizedCodeControllerTest.java +++ b/certify-service/src/test/java/io/mosip/certify/controller/PreAuthorizedCodeControllerTest.java @@ -152,10 +152,6 @@ public void getCredentialOffer_NotFound() throws Exception { @Test public void token_Success() throws Exception { - TokenRequest request = new TokenRequest(); - request.setGrantType(Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE); - request.setPreAuthorizedCode("test-pre-auth-code"); - TokenResponse expectedResponse = TokenResponse.builder() .accessToken("at_test_access_token") .tokenType("Bearer") @@ -168,8 +164,9 @@ public void token_Success() throws Exception { .thenReturn(expectedResponse); mockMvc.perform(post("/token") - .content(objectMapper.writeValueAsBytes(request)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE) + .param("pre-authorized_code", "test-pre-auth-code") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.access_token").value("at_test_access_token")) @@ -181,11 +178,6 @@ public void token_Success() throws Exception { @Test public void token_WithTxCode_Success() throws Exception { - TokenRequest request = new TokenRequest(); - request.setGrantType(Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE); - request.setPreAuthorizedCode("test-pre-auth-code"); - request.setTxCode("1234"); - TokenResponse expectedResponse = TokenResponse.builder() .accessToken("at_test_access_token") .tokenType("Bearer") @@ -198,8 +190,10 @@ public void token_WithTxCode_Success() throws Exception { .thenReturn(expectedResponse); mockMvc.perform(post("/token") - .content(objectMapper.writeValueAsBytes(request)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE) + .param("pre-authorized_code", "test-pre-auth-code") + .param("tx_code", "1234") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.access_token").value("at_test_access_token")) @@ -208,17 +202,14 @@ public void token_WithTxCode_Success() throws Exception { @Test public void token_UnsupportedGrantType() throws Exception { - TokenRequest request = new TokenRequest(); - request.setGrantType("invalid_grant_type"); - request.setPreAuthorizedCode("test-pre-auth-code"); - Mockito.when(preAuthorizedCodeService.exchangePreAuthorizedCode(Mockito.any(TokenRequest.class))) .thenThrow(new CertifyException(ErrorConstants.UNSUPPORTED_GRANT_TYPE, "Grant type not supported")); mockMvc.perform(post("/token") - .content(objectMapper.writeValueAsBytes(request)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", "invalid_grant_type") + .param("pre-authorized_code", "test-pre-auth-code") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // ExceptionHandler returns 200 OK with errors .andExpect(jsonPath("$.errors").isArray()) @@ -228,17 +219,14 @@ public void token_UnsupportedGrantType() throws Exception { @Test public void token_InvalidPreAuthCode() throws Exception { - TokenRequest request = new TokenRequest(); - request.setGrantType(Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE); - request.setPreAuthorizedCode("invalid-code"); - Mockito.when(preAuthorizedCodeService.exchangePreAuthorizedCode(Mockito.any(TokenRequest.class))) .thenThrow(new CertifyException(ErrorConstants.INVALID_GRANT, "Pre-authorized code not found")); mockMvc.perform(post("/token") - .content(objectMapper.writeValueAsBytes(request)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE) + .param("pre-authorized_code", "invalid-code") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // ExceptionHandler returns 200 OK with errors .andExpect(jsonPath("$.errors").isArray()) @@ -247,17 +235,14 @@ public void token_InvalidPreAuthCode() throws Exception { @Test public void token_ExpiredPreAuthCode() throws Exception { - TokenRequest request = new TokenRequest(); - request.setGrantType(Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE); - request.setPreAuthorizedCode("expired-code"); - Mockito.when(preAuthorizedCodeService.exchangePreAuthorizedCode(Mockito.any(TokenRequest.class))) .thenThrow(new CertifyException("pre_auth_code_expired", "Pre-authorized code has expired")); mockMvc.perform(post("/token") - .content(objectMapper.writeValueAsBytes(request)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE) + .param("pre-authorized_code", "expired-code") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // ExceptionHandler returns 200 OK with errors .andExpect(jsonPath("$.errors").isArray()) @@ -266,17 +251,14 @@ public void token_ExpiredPreAuthCode() throws Exception { @Test public void token_AlreadyUsedPreAuthCode() throws Exception { - TokenRequest request = new TokenRequest(); - request.setGrantType(Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE); - request.setPreAuthorizedCode("used-code"); - Mockito.when(preAuthorizedCodeService.exchangePreAuthorizedCode(Mockito.any(TokenRequest.class))) .thenThrow(new CertifyException("pre_auth_code_already_used", "Pre-authorized code has already been used")); mockMvc.perform(post("/token") - .content(objectMapper.writeValueAsBytes(request)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE) + .param("pre-authorized_code", "used-code") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // ExceptionHandler returns 200 OK with errors .andExpect(jsonPath("$.errors").isArray()) @@ -285,18 +267,14 @@ public void token_AlreadyUsedPreAuthCode() throws Exception { @Test public void token_TxCodeRequired() throws Exception { - TokenRequest request = new TokenRequest(); - request.setGrantType(Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE); - request.setPreAuthorizedCode("test-code"); - // txCode not provided but required - Mockito.when(preAuthorizedCodeService.exchangePreAuthorizedCode(Mockito.any(TokenRequest.class))) .thenThrow(new CertifyException("tx_code_required", "Transaction code is required for this pre-authorized code")); mockMvc.perform(post("/token") - .content(objectMapper.writeValueAsBytes(request)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE) + .param("pre-authorized_code", "test-code") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // ExceptionHandler returns 200 OK with errors .andExpect(jsonPath("$.errors").isArray()) @@ -305,17 +283,14 @@ public void token_TxCodeRequired() throws Exception { @Test public void token_TxCodeMismatch() throws Exception { - TokenRequest request = new TokenRequest(); - request.setGrantType(Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE); - request.setPreAuthorizedCode("test-code"); - request.setTxCode("wrong-code"); - Mockito.when(preAuthorizedCodeService.exchangePreAuthorizedCode(Mockito.any(TokenRequest.class))) .thenThrow(new CertifyException("tx_code_mismatch", "Transaction code does not match")); mockMvc.perform(post("/token") - .content(objectMapper.writeValueAsBytes(request)) - .contentType(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .param("grant_type", Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE) + .param("pre-authorized_code", "test-code") + .param("tx_code", "wrong-code") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // ExceptionHandler returns 200 OK with errors .andExpect(jsonPath("$.errors").isArray()) diff --git a/certify-service/src/test/java/io/mosip/certify/controller/WellKnownControllerTest.java b/certify-service/src/test/java/io/mosip/certify/controller/WellKnownControllerTest.java index 77410ded5..9d3b5fa29 100644 --- a/certify-service/src/test/java/io/mosip/certify/controller/WellKnownControllerTest.java +++ b/certify-service/src/test/java/io/mosip/certify/controller/WellKnownControllerTest.java @@ -36,6 +36,12 @@ class WellKnownControllerTest { @MockBean private ParsedAccessToken parsedAccessToken; + @MockBean + private io.mosip.certify.api.spi.AuditPlugin auditWrapper; + + @MockBean + private io.mosip.certify.services.AuthorizationServerService authorizationServerService; + @InjectMocks private WellKnownController wellKnownController; @@ -64,9 +70,10 @@ void getCredentialIssuerMetadata_emptyVersion_defaultsToLatest() throws Exceptio @Test void getCredentialIssuerMetadata_unsupportedVersion_returnsError() throws Exception { - when(credentialConfigurationService.fetchCredentialIssuerMetadata("unsupported")).thenThrow( new CertifyException("UNSUPPORTED_VERSION", "Unsupported version")); + when(credentialConfigurationService.fetchCredentialIssuerMetadata("unsupported")) + .thenThrow(new CertifyException("UNSUPPORTED_VERSION", "Unsupported version")); mockMvc.perform(get("/.well-known/openid-credential-issuer?version=unsupported")) - .andExpect(status().is2xxSuccessful()) + .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errors[0].errorCode").value("UNSUPPORTED_VERSION")); } @@ -89,9 +96,68 @@ void getDIDDocument_notFound_returnsEmpty() throws Exception { @Test void getDIDDocument_serviceThrowsException_returnsError() throws Exception { - when(vcIssuanceService.getDIDDocument()).thenThrow(new InvalidRequestException("unsupported_in_current_plugin_mode")); + when(vcIssuanceService.getDIDDocument()) + .thenThrow(new InvalidRequestException("unsupported_in_current_plugin_mode")); mockMvc.perform(get("/.well-known/did.json")) - .andExpect(status().is2xxSuccessful()) + .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errors[0].errorCode").value("unsupported_in_current_plugin_mode")); } + + @Test + void getAuthorizationServerMetadata_success() throws Exception { + io.mosip.certify.core.dto.AuthorizationServerMetadata mockMetadata = + io.mosip.certify.core.dto.AuthorizationServerMetadata.builder() + .issuer("https://auth.example.com") + .tokenEndpoint("https://auth.example.com/token") + .authorizationEndpoint("https://auth.example.com/authorize") + .jwksUri("https://auth.example.com/jwks.json") + .build(); + + when(authorizationServerService.getInternalAuthServerMetadata()).thenReturn(mockMetadata); + + mockMvc.perform(get("/.well-known/oauth-authorization-server")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.issuer").value("https://auth.example.com")) + .andExpect(jsonPath("$.token_endpoint").value("https://auth.example.com/token")) + .andExpect(jsonPath("$.authorization_endpoint").value("https://auth.example.com/authorize")) + .andExpect(jsonPath("$.jwks_uri").value("https://auth.example.com/jwks.json")); + + verify(authorizationServerService, times(1)).getInternalAuthServerMetadata(); + } + + @Test + void getOpenIDConfiguration_success() throws Exception { + io.mosip.certify.core.dto.AuthorizationServerMetadata mockMetadata = + io.mosip.certify.core.dto.AuthorizationServerMetadata.builder() + .issuer("https://auth.example.com") + .tokenEndpoint("https://auth.example.com/token") + .build(); + + when(authorizationServerService.getInternalAuthServerMetadata()).thenReturn(mockMetadata); + + mockMvc.perform(get("/.well-known/openid-configuration")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.issuer").value("https://auth.example.com")) + .andExpect(jsonPath("$.token_endpoint").value("https://auth.example.com/token")); + + verify(authorizationServerService, times(1)).getInternalAuthServerMetadata(); + } + + @Test + void getAuthorizationServerMetadata_returnsNullFields_success() throws Exception { + io.mosip.certify.core.dto.AuthorizationServerMetadata mockMetadata = + io.mosip.certify.core.dto.AuthorizationServerMetadata.builder() + .issuer("https://auth.example.com") + .tokenEndpoint("https://auth.example.com/token") + .build(); + + when(authorizationServerService.getInternalAuthServerMetadata()).thenReturn(mockMetadata); + + mockMvc.perform(get("/.well-known/oauth-authorization-server")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.issuer").value("https://auth.example.com")) + .andExpect(jsonPath("$.token_endpoint").value("https://auth.example.com/token")) + .andExpect(jsonPath("$.authorization_endpoint").doesNotExist()) + .andExpect(jsonPath("$.jwks_uri").doesNotExist()); + } } diff --git a/certify-service/src/test/java/io/mosip/certify/services/AuthorizationServerServiceTest.java b/certify-service/src/test/java/io/mosip/certify/services/AuthorizationServerServiceTest.java new file mode 100644 index 000000000..1d8349b21 --- /dev/null +++ b/certify-service/src/test/java/io/mosip/certify/services/AuthorizationServerServiceTest.java @@ -0,0 +1,547 @@ +package io.mosip.certify.services; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.mosip.certify.core.constants.Constants; +import io.mosip.certify.core.constants.ErrorConstants; +import io.mosip.certify.core.dto.AuthorizationServerConfig; +import io.mosip.certify.core.dto.AuthorizationServerMetadata; +import io.mosip.certify.core.exception.CertifyException; +import io.mosip.certify.core.exception.InvalidRequestException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class AuthorizationServerServiceTest { + + @Mock + private VCICacheService vciCacheService; + + @Mock + private ObjectMapper objectMapper; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private AuthorizationServerService authorizationServerService; + + private static final String INTERNAL_SERVER_URL = "https://internal-auth.example.com"; + private static final String EXTERNAL_SERVER_URL = "https://external-auth.example.com"; + private static final String DEFAULT_SERVER_URL = "https://default-auth.example.com"; + + @Before + public void setup() { + ReflectionTestUtils.setField(authorizationServerService, "retryCount", 3); + ReflectionTestUtils.setField(authorizationServerService, "internalAuthServerUrl", INTERNAL_SERVER_URL); + ReflectionTestUtils.setField(authorizationServerService, "authorizationServersConfig", ""); + ReflectionTestUtils.setField(authorizationServerService, "defaultAuthServer", ""); + ReflectionTestUtils.setField(authorizationServerService, "credentialConfigMappingJson", "{}"); + } + + // ========== Tests for initialize() and loadConfiguredServers() ========== + + @Test + public void initialize_WithInternalServerOnly_Success() { + authorizationServerService.initialize(); + + List urls = authorizationServerService.getAllAuthorizationServerUrls(); + assertEquals(1, urls.size()); + assertEquals(INTERNAL_SERVER_URL, urls.get(0)); + } + + @Test + public void initialize_WithExternalServers_Success() { + ReflectionTestUtils.setField(authorizationServerService, "authorizationServersConfig", + "https://auth1.example.com, https://auth2.example.com"); + + authorizationServerService.initialize(); + + List urls = authorizationServerService.getAllAuthorizationServerUrls(); + assertEquals(3, urls.size()); // internal + 2 external + } + + @Test + public void initialize_WithNoServers_NoException() { + ReflectionTestUtils.setField(authorizationServerService, "internalAuthServerUrl", ""); + ReflectionTestUtils.setField(authorizationServerService, "authorizationServersConfig", ""); + + authorizationServerService.initialize(); + + List urls = authorizationServerService.getAllAuthorizationServerUrls(); + assertEquals(0, urls.size()); + } + + @Test + public void initialize_WithEmptyExternalConfig_OnlyInternalAdded() { + ReflectionTestUtils.setField(authorizationServerService, "authorizationServersConfig", " , "); + + authorizationServerService.initialize(); + + List urls = authorizationServerService.getAllAuthorizationServerUrls(); + assertEquals(1, urls.size()); + assertEquals(INTERNAL_SERVER_URL, urls.get(0)); + } + + // ========== Tests for loadCredentialConfigMappings() ========== + + @Test + public void initialize_WithCredentialConfigMappings_Success() throws Exception { + String mappingJson = "{\"config1\":\"https://auth1.example.com\",\"config2\":\"https://auth2.example.com\"}"; + ReflectionTestUtils.setField(authorizationServerService, "credentialConfigMappingJson", mappingJson); + + when(objectMapper.readValue(eq(mappingJson), any(TypeReference.class))) + .thenReturn(Map.of("config1", "https://auth1.example.com", "config2", "https://auth2.example.com")); + + authorizationServerService.initialize(); + + verify(objectMapper).readValue(eq(mappingJson), any(TypeReference.class)); + } + + @Test + public void initialize_WithInvalidCredentialConfigMappings_NoException() throws Exception { + String mappingJson = "invalid-json"; + ReflectionTestUtils.setField(authorizationServerService, "credentialConfigMappingJson", mappingJson); + + when(objectMapper.readValue(eq(mappingJson), any(TypeReference.class))) + .thenThrow(new RuntimeException("Invalid JSON")); + + // Should not throw, just log error + authorizationServerService.initialize(); + } + + // ========== Tests for getInternalAuthServerMetadata() ========== + + @Test + public void getInternalAuthServerMetadata_Success() { + authorizationServerService.initialize(); + + AuthorizationServerMetadata metadata = authorizationServerService.getInternalAuthServerMetadata(); + + assertNotNull(metadata); + assertEquals(INTERNAL_SERVER_URL, metadata.getIssuer()); + assertEquals(INTERNAL_SERVER_URL + "/token", metadata.getTokenEndpoint()); + assertEquals(INTERNAL_SERVER_URL + "/authorize", metadata.getAuthorizationEndpoint()); + assertEquals(INTERNAL_SERVER_URL + "/jwks.json", metadata.getJwksUri()); + } + + @Test + public void getInternalAuthServerMetadata_NormalizesTrailingSlash() { + ReflectionTestUtils.setField(authorizationServerService, "internalAuthServerUrl", + INTERNAL_SERVER_URL + "/"); + authorizationServerService.initialize(); + + AuthorizationServerMetadata metadata = authorizationServerService.getInternalAuthServerMetadata(); + + assertEquals(INTERNAL_SERVER_URL, metadata.getIssuer()); + } + + // ========== Tests for discoverMetadata() ========== + + @Test + public void discoverMetadata_CacheHit_ReturnsCachedMetadata() { + AuthorizationServerMetadata cachedMetadata = AuthorizationServerMetadata.builder() + .issuer(EXTERNAL_SERVER_URL) + .tokenEndpoint(EXTERNAL_SERVER_URL + "/token") + .build(); + + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(cachedMetadata); + + AuthorizationServerMetadata result = authorizationServerService.discoverMetadata(EXTERNAL_SERVER_URL); + + assertEquals(cachedMetadata, result); + verify(restTemplate, never()).getForEntity(any(URI.class), eq(String.class)); + } + + @Test + public void discoverMetadata_CacheMiss_DiscoverFromOIDC_Success() throws Exception { + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(null); + + String metadataJson = "{\"issuer\":\"" + EXTERNAL_SERVER_URL + "\",\"token_endpoint\":\"" + EXTERNAL_SERVER_URL + "/token\"}"; + ResponseEntity response = new ResponseEntity<>(metadataJson, HttpStatus.OK); + + when(restTemplate.getForEntity(any(URI.class), eq(String.class))).thenReturn(response); + + AuthorizationServerMetadata expectedMetadata = AuthorizationServerMetadata.builder() + .issuer(EXTERNAL_SERVER_URL) + .tokenEndpoint(EXTERNAL_SERVER_URL + "/token") + .build(); + + when(objectMapper.readValue(eq(metadataJson), eq(AuthorizationServerMetadata.class))) + .thenReturn(expectedMetadata); + + AuthorizationServerMetadata result = authorizationServerService.discoverMetadata(EXTERNAL_SERVER_URL); + + assertNotNull(result); + assertEquals(EXTERNAL_SERVER_URL, result.getIssuer()); + verify(vciCacheService).setASMetadata(eq(EXTERNAL_SERVER_URL), eq(expectedMetadata)); + } + + @Test + public void discoverMetadata_OIDCFails_FallbackToOAuth_Success() throws Exception { + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(null); + + // First call to OIDC endpoint fails + ResponseEntity failedResponse = new ResponseEntity<>(null, HttpStatus.NOT_FOUND); + + // Second call to OAuth AS endpoint succeeds + String metadataJson = "{\"issuer\":\"" + EXTERNAL_SERVER_URL + "\",\"token_endpoint\":\"" + EXTERNAL_SERVER_URL + "/token\"}"; + ResponseEntity successResponse = new ResponseEntity<>(metadataJson, HttpStatus.OK); + + when(restTemplate.getForEntity(any(URI.class), eq(String.class))) + .thenReturn(failedResponse) + .thenReturn(failedResponse) + .thenReturn(failedResponse) // 3 retries for OIDC + .thenReturn(successResponse); + + AuthorizationServerMetadata expectedMetadata = AuthorizationServerMetadata.builder() + .issuer(EXTERNAL_SERVER_URL) + .tokenEndpoint(EXTERNAL_SERVER_URL + "/token") + .build(); + + when(objectMapper.readValue(eq(metadataJson), eq(AuthorizationServerMetadata.class))) + .thenReturn(expectedMetadata); + + AuthorizationServerMetadata result = authorizationServerService.discoverMetadata(EXTERNAL_SERVER_URL); + + assertNotNull(result); + } + + @Test + public void discoverMetadata_AllAttemptsFail_ThrowsCertifyException() throws Exception { + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(null); + + // All calls fail + ResponseEntity failedResponse = new ResponseEntity<>(null, HttpStatus.NOT_FOUND); + when(restTemplate.getForEntity(any(URI.class), eq(String.class))).thenReturn(failedResponse); + + CertifyException exception = assertThrows(CertifyException.class, + () -> authorizationServerService.discoverMetadata(EXTERNAL_SERVER_URL)); + + assertEquals(ErrorConstants.AUTHORIZATION_SERVER_DISCOVERY_FAILED, exception.getErrorCode()); + } + + @Test + public void discoverMetadata_InvalidMetadata_ThrowsCertifyException() throws Exception { + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(null); + + String metadataJson = "{\"issuer\":\"\"}"; // Missing token_endpoint + ResponseEntity response = new ResponseEntity<>(metadataJson, HttpStatus.OK); + + when(restTemplate.getForEntity(any(URI.class), eq(String.class))).thenReturn(response); + + AuthorizationServerMetadata invalidMetadata = AuthorizationServerMetadata.builder() + .issuer("") + .build(); + + when(objectMapper.readValue(eq(metadataJson), eq(AuthorizationServerMetadata.class))) + .thenReturn(invalidMetadata); + + CertifyException exception = assertThrows(CertifyException.class, + () -> authorizationServerService.discoverMetadata(EXTERNAL_SERVER_URL)); + + assertEquals(ErrorConstants.AUTHORIZATION_SERVER_DISCOVERY_FAILED, exception.getErrorCode()); + } + + // ========== Tests for getTokenEndpoint() ========== + + @Test + public void getTokenEndpoint_Success() { + AuthorizationServerMetadata metadata = AuthorizationServerMetadata.builder() + .issuer(EXTERNAL_SERVER_URL) + .tokenEndpoint(EXTERNAL_SERVER_URL + "/token") + .build(); + + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(metadata); + + String tokenEndpoint = authorizationServerService.getTokenEndpoint(EXTERNAL_SERVER_URL); + + assertEquals(EXTERNAL_SERVER_URL + "/token", tokenEndpoint); + } + + // ========== Tests for getJwksUri() ========== + + @Test + public void getJwksUri_Success() { + AuthorizationServerMetadata metadata = AuthorizationServerMetadata.builder() + .issuer(EXTERNAL_SERVER_URL) + .tokenEndpoint(EXTERNAL_SERVER_URL + "/token") + .jwksUri(EXTERNAL_SERVER_URL + "/jwks.json") + .build(); + + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(metadata); + + String jwksUri = authorizationServerService.getJwksUri(EXTERNAL_SERVER_URL); + + assertEquals(EXTERNAL_SERVER_URL + "/jwks.json", jwksUri); + } + + // ========== Tests for supportsPreAuthorizedCodeGrant() ========== + + @Test + public void supportsPreAuthorizedCodeGrant_Supported_ReturnsTrue() { + AuthorizationServerMetadata metadata = AuthorizationServerMetadata.builder() + .issuer(EXTERNAL_SERVER_URL) + .tokenEndpoint(EXTERNAL_SERVER_URL + "/token") + .grantTypesSupported(Arrays.asList("authorization_code", Constants.PRE_AUTHORIZED_CODE_GRANT_TYPE)) + .build(); + + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(metadata); + + boolean result = authorizationServerService.supportsPreAuthorizedCodeGrant(EXTERNAL_SERVER_URL); + + assertTrue(result); + } + + @Test + public void supportsPreAuthorizedCodeGrant_NotSupported_ReturnsFalse() { + AuthorizationServerMetadata metadata = AuthorizationServerMetadata.builder() + .issuer(EXTERNAL_SERVER_URL) + .tokenEndpoint(EXTERNAL_SERVER_URL + "/token") + .grantTypesSupported(Arrays.asList("authorization_code")) + .build(); + + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(metadata); + + boolean result = authorizationServerService.supportsPreAuthorizedCodeGrant(EXTERNAL_SERVER_URL); + + assertFalse(result); + } + + @Test + public void supportsPreAuthorizedCodeGrant_NullGrantTypes_ReturnsFalse() { + AuthorizationServerMetadata metadata = AuthorizationServerMetadata.builder() + .issuer(EXTERNAL_SERVER_URL) + .tokenEndpoint(EXTERNAL_SERVER_URL + "/token") + .build(); + + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(metadata); + + boolean result = authorizationServerService.supportsPreAuthorizedCodeGrant(EXTERNAL_SERVER_URL); + + assertFalse(result); + } + + @Test + public void supportsPreAuthorizedCodeGrant_DiscoveryFails_ReturnsFalse() { + when(vciCacheService.getASMetadata(EXTERNAL_SERVER_URL)).thenReturn(null); + when(restTemplate.getForEntity(any(URI.class), eq(String.class))) + .thenThrow(new RuntimeException("Connection failed")); + + boolean result = authorizationServerService.supportsPreAuthorizedCodeGrant(EXTERNAL_SERVER_URL); + + assertFalse(result); + } + + // ========== Tests for getAuthorizationServerForCredentialConfig() ========== + + @Test + public void getAuthorizationServerForCredentialConfig_MappedAS_ReturnsMapping() throws Exception { + String configId = "test-config"; + String mappedUrl = "https://mapped-auth.example.com"; + + ReflectionTestUtils.setField(authorizationServerService, "authorizationServersConfig", mappedUrl); + ReflectionTestUtils.setField(authorizationServerService, "credentialConfigMappingJson", + "{\"" + configId + "\":\"" + mappedUrl + "\"}"); + + when(objectMapper.readValue(anyString(), any(TypeReference.class))) + .thenReturn(Map.of(configId, mappedUrl)); + + authorizationServerService.initialize(); + + String result = authorizationServerService.getAuthorizationServerForCredentialConfig(configId); + + assertEquals(mappedUrl, result); + } + + @Test + public void getAuthorizationServerForCredentialConfig_NoMapping_UsesDefault() throws Exception { + String configId = "unmapped-config"; + + ReflectionTestUtils.setField(authorizationServerService, "defaultAuthServer", DEFAULT_SERVER_URL); + ReflectionTestUtils.setField(authorizationServerService, "authorizationServersConfig", DEFAULT_SERVER_URL); + + authorizationServerService.initialize(); + + String result = authorizationServerService.getAuthorizationServerForCredentialConfig(configId); + + assertEquals(DEFAULT_SERVER_URL, result); + } + + @Test + public void getAuthorizationServerForCredentialConfig_NoDefaultOrMapping_UsesInternal() { + String configId = "some-config"; + + ReflectionTestUtils.setField(authorizationServerService, "defaultAuthServer", ""); + + authorizationServerService.initialize(); + + String result = authorizationServerService.getAuthorizationServerForCredentialConfig(configId); + + assertEquals(INTERNAL_SERVER_URL, result); + } + + @Test + public void getAuthorizationServerForCredentialConfig_NoASConfigured_ThrowsCertifyException() { + String configId = "some-config"; + + ReflectionTestUtils.setField(authorizationServerService, "internalAuthServerUrl", ""); + ReflectionTestUtils.setField(authorizationServerService, "defaultAuthServer", ""); + + authorizationServerService.initialize(); + + CertifyException exception = assertThrows(CertifyException.class, + () -> authorizationServerService.getAuthorizationServerForCredentialConfig(configId)); + + assertEquals(ErrorConstants.AUTHORIZATION_SERVER_NOT_CONFIGURED, exception.getErrorCode()); + } + + @Test + public void getAuthorizationServerForCredentialConfig_MappedButNotConfigured_ThrowsInvalidRequestException() throws Exception { + String configId = "test-config"; + String unconfiguredUrl = "https://unconfigured.example.com"; + + ReflectionTestUtils.setField(authorizationServerService, "credentialConfigMappingJson", + "{\"" + configId + "\":\"" + unconfiguredUrl + "\"}"); + + when(objectMapper.readValue(anyString(), any(TypeReference.class))) + .thenReturn(Map.of(configId, unconfiguredUrl)); + + authorizationServerService.initialize(); + + InvalidRequestException exception = assertThrows(InvalidRequestException.class, + () -> authorizationServerService.getAuthorizationServerForCredentialConfig(configId)); + + assertEquals(ErrorConstants.INVALID_AUTHORIZATION_SERVER, exception.getErrorCode()); + } + + // ========== Tests for getAllAuthorizationServerUrls() ========== + + @Test + public void getAllAuthorizationServerUrls_ReturnsAllConfigured() { + ReflectionTestUtils.setField(authorizationServerService, "authorizationServersConfig", + "https://ext1.example.com, https://ext2.example.com"); + + authorizationServerService.initialize(); + + List urls = authorizationServerService.getAllAuthorizationServerUrls(); + + assertEquals(3, urls.size()); + assertTrue(urls.contains(INTERNAL_SERVER_URL)); + assertTrue(urls.contains("https://ext1.example.com")); + assertTrue(urls.contains("https://ext2.example.com")); + } + + // ========== Tests for isServerConfigured() ========== + + @Test + public void isServerConfigured_ConfiguredServer_ReturnsTrue() { + authorizationServerService.initialize(); + + boolean result = authorizationServerService.isServerConfigured(INTERNAL_SERVER_URL); + + assertTrue(result); + } + + @Test + public void isServerConfigured_ConfiguredServerWithTrailingSlash_ReturnsTrue() { + authorizationServerService.initialize(); + + boolean result = authorizationServerService.isServerConfigured(INTERNAL_SERVER_URL + "/"); + + assertTrue(result); + } + + @Test + public void isServerConfigured_UnconfiguredServer_ReturnsFalse() { + authorizationServerService.initialize(); + + boolean result = authorizationServerService.isServerConfigured("https://unknown.example.com"); + + assertFalse(result); + } + + // ========== Tests for normalizeUrl() (accessed via reflection) ========== + + @Test + public void normalizeUrl_RemovesTrailingSlash() { + authorizationServerService.initialize(); + + String result = ReflectionTestUtils.invokeMethod(authorizationServerService, "normalizeUrl", + "https://example.com/"); + + assertEquals("https://example.com", result); + } + + @Test + public void normalizeUrl_RemovesMultipleTrailingSlashes() { + authorizationServerService.initialize(); + + String result = ReflectionTestUtils.invokeMethod(authorizationServerService, "normalizeUrl", + "https://example.com///"); + + assertEquals("https://example.com", result); + } + + @Test + public void normalizeUrl_NullUrl_ReturnsEmptyString() { + authorizationServerService.initialize(); + + String result = ReflectionTestUtils.invokeMethod(authorizationServerService, "normalizeUrl", + (String) null); + + assertEquals("", result); + } + + @Test + public void normalizeUrl_NoTrailingSlash_ReturnsSame() { + authorizationServerService.initialize(); + + String result = ReflectionTestUtils.invokeMethod(authorizationServerService, "normalizeUrl", + "https://example.com"); + + assertEquals("https://example.com", result); + } + + // ========== Tests for generateServerId() (accessed via reflection) ========== + + @Test + public void generateServerId_ValidUrl_GeneratesId() { + authorizationServerService.initialize(); + + String result = ReflectionTestUtils.invokeMethod(authorizationServerService, "generateServerId", + "https://auth.example.com"); + + assertNotNull(result); + assertTrue(result.startsWith("as-")); + assertTrue(result.contains("auth-example-com")); + } + + @Test + public void generateServerId_UrlWithPort_GeneratesId() { + authorizationServerService.initialize(); + + String result = ReflectionTestUtils.invokeMethod(authorizationServerService, "generateServerId", + "https://auth.example.com:8080"); + + assertNotNull(result); + assertTrue(result.startsWith("as-")); + } +} diff --git a/certify-service/src/test/java/io/mosip/certify/services/CredentialConfigurationServiceImplTest.java b/certify-service/src/test/java/io/mosip/certify/services/CredentialConfigurationServiceImplTest.java index 0ed32fa0f..18ad69458 100644 --- a/certify-service/src/test/java/io/mosip/certify/services/CredentialConfigurationServiceImplTest.java +++ b/certify-service/src/test/java/io/mosip/certify/services/CredentialConfigurationServiceImplTest.java @@ -38,6 +38,9 @@ public class CredentialConfigurationServiceImplTest { @Mock private CredentialConfigMapper credentialConfigMapper; + @Mock + private AuthorizationServerService authServerService; + @InjectMocks private CredentialConfigurationServiceImpl credentialConfigurationService; @@ -98,6 +101,8 @@ public void setup() { ReflectionTestUtils.setField(credentialConfigurationService, "credentialSigningAlgValuesSupportedMap", credentialSigningMap); ReflectionTestUtils.setField(credentialConfigurationService, "proofTypesSupported", new LinkedHashMap<>()); ReflectionTestUtils.setField(credentialConfigurationService, "keyAliasMapper", keyAliasMapper); + + when(authServerService.getAllAuthorizationServerUrls()).thenReturn(List.of("http://auth.com")); } @Test @@ -717,4 +722,4 @@ public void validateKeyAliasMpperConfiguration_NoMatchingAppIdAndRefId_ThrowsExc ); assertEquals("No matching appId and refId found in the key chooser list.", exception.getMessage()); } -} +} \ No newline at end of file diff --git a/certify-service/src/test/java/io/mosip/certify/services/PreAuthorizedCodeServiceTest.java b/certify-service/src/test/java/io/mosip/certify/services/PreAuthorizedCodeServiceTest.java index 778217b42..1b2be3491 100644 --- a/certify-service/src/test/java/io/mosip/certify/services/PreAuthorizedCodeServiceTest.java +++ b/certify-service/src/test/java/io/mosip/certify/services/PreAuthorizedCodeServiceTest.java @@ -5,6 +5,7 @@ import io.mosip.certify.core.dto.*; import io.mosip.certify.core.exception.CertifyException; import io.mosip.certify.core.exception.InvalidRequestException; +import io.mosip.certify.core.spi.CredentialConfigurationService; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -15,6 +16,7 @@ import org.springframework.test.util.ReflectionTestUtils; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import static org.junit.Assert.assertThrows; @@ -28,14 +30,21 @@ public class PreAuthorizedCodeServiceTest { @Mock private VCICacheService vciCacheService; - @InjectMocks - private PreAuthorizedCodeService preAuthorizedCodeService; + @Mock + private CredentialConfigurationService credentialConfigurationService; - private PreAuthorizedRequest request; - private Map issuerMetadata; - private Map supportedConfigs; - private Map config; - private final String CONFIG_ID = "test-config"; + @Mock + private AuthorizationServerService authServerService; + + @InjectMocks + private PreAuthorizedCodeService preAuthorizedCodeService; + + private PreAuthorizedRequest request; + private Map issuerMetadata; + private Map supportedConfigs; + private Map config; + private CredentialIssuerMetadataDTO metadataDTO; + private final String CONFIG_ID = "test-config"; @Before public void setup() { @@ -62,7 +71,20 @@ public void setup() { supportedConfigs.put(CONFIG_ID, config); issuerMetadata.put(Constants.CREDENTIAL_CONFIGURATIONS_SUPPORTED, supportedConfigs); - when(vciCacheService.getIssuerMetadata()).thenReturn(issuerMetadata); + // Setup mock for credentialConfigurationService + Map supportedDTOMap = new LinkedHashMap<>(); + CredentialConfigurationSupportedDTO configDTO = new CredentialConfigurationSupportedDTO(); + configDTO.setClaims(requiredClaims); + supportedDTOMap.put(CONFIG_ID, configDTO); + + metadataDTO = mock(CredentialIssuerMetadataDTO.class); + when(metadataDTO.getCredentialConfigurationSupportedDTO()).thenReturn(supportedDTOMap); + + // KEY FIX: Mock the credentialConfigurationService to return metadataDTO + when(credentialConfigurationService.fetchCredentialIssuerMetadata(anyString())).thenReturn(metadataDTO); + + // Setup mock for authServerService + when(authServerService.getAuthorizationServerForCredentialConfig(anyString())).thenReturn(null); } @Test @@ -89,8 +111,12 @@ public void generatePreAuthorizedCode_WithTxCode_Success() { public void generatePreAuthorizedCode_Failure_If_InvalidConfigId() { request.setCredentialConfigurationId("invalid-id"); + // Update metadata mock to not include invalid-id + Map emptyMap = new LinkedHashMap<>(); + when(metadataDTO.getCredentialConfigurationSupportedDTO()).thenReturn(emptyMap); + InvalidRequestException exception = assertThrows(InvalidRequestException.class, - () -> preAuthorizedCodeService.generatePreAuthorizedCode(request)); + () -> preAuthorizedCodeService.generatePreAuthorizedCode(request)); Assert.assertEquals(ErrorConstants.INVALID_CREDENTIAL_CONFIGURATION_ID, exception.getMessage()); } @@ -470,5 +496,3 @@ public void exchangePreAuthorizedCode_SingleUseDisabled_DoesNotBlacklist() { verify(vciCacheService, never()).blacklistPreAuthCode(anyString()); } } - -