diff --git a/pom.xml b/pom.xml index 4d98c8aa..89450fa6 100644 --- a/pom.xml +++ b/pom.xml @@ -89,8 +89,8 @@ 1.70 3.8.1 3.3.3 - 7.2.0 - 2.0.0 + 8.0.3 + 5.0.3 5.5.13 3.6.1 3.7 @@ -98,8 +98,7 @@ 1.15 2.9.2 1.5.10 - - + 0.9.1 **/constant/**,**/config/**,**/httpfilter/**,**/cache/**,**/dto/**,**/entity/**,**/model/**,**/exception/**,**/repository/**,**/security/**,**/*Config.java,**/*BootApplication.java,**/*VertxApplication.java,**/cbeffutil/**,**/core.http/**,**/util/** @@ -365,6 +364,11 @@ nimbus-jose-jwt 9.25.6 + + io.jsonwebtoken + jjwt + ${jjwt-version} + org.bitbucket.b_c jose4j diff --git a/src/main/java/io/mosip/mimoto/controller/IssuersController.java b/src/main/java/io/mosip/mimoto/controller/IssuersController.java index 7ed1363a..30244884 100644 --- a/src/main/java/io/mosip/mimoto/controller/IssuersController.java +++ b/src/main/java/io/mosip/mimoto/controller/IssuersController.java @@ -1,27 +1,41 @@ package io.mosip.mimoto.controller; import io.mosip.mimoto.core.http.ResponseWrapper; +import io.mosip.mimoto.dto.DisplayDTO; import io.mosip.mimoto.dto.ErrorDTO; import io.mosip.mimoto.dto.IssuerDTO; import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialSupportedDisplayResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.dto.mimoto.IssuerSupportedCredentialsResponse; import io.mosip.mimoto.exception.ApiNotAccessibleException; import io.mosip.mimoto.service.IssuersService; import io.mosip.mimoto.util.DateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -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.RequestMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.List; +import java.util.Optional; import static io.mosip.mimoto.exception.PlatformErrorMessages.API_NOT_ACCESSIBLE_EXCEPTION; +import static io.mosip.mimoto.exception.PlatformErrorMessages.INVALID_CREDENTIAL_TYPE_EXCEPTION; import static io.mosip.mimoto.exception.PlatformErrorMessages.INVALID_ISSUER_ID_EXCEPTION; +import static io.mosip.mimoto.exception.PlatformErrorMessages.MIMOTO_PDF_SIGN_EXCEPTION; @RestController @RequestMapping(value = "/issuers") @@ -29,18 +43,20 @@ public class IssuersController { @Autowired IssuersService issuersService; + private static final String defaultLanguageConstant = "en"; + private static final String ID = "mosip.mimoto.issuers"; private final Logger logger = LoggerFactory.getLogger(IssuersController.class); @GetMapping() - public ResponseEntity getAllIssuers() { + public ResponseEntity getAllIssuers(@RequestParam(required = false) String search) { ResponseWrapper responseWrapper = new ResponseWrapper<>(); responseWrapper.setId(ID); responseWrapper.setVersion("v1"); responseWrapper.setResponsetime(DateUtils.getRequestTimeString()); try { - responseWrapper.setResponse(issuersService.getAllIssuers()); + responseWrapper.setResponse(issuersService.getAllIssuers(search)); } catch (ApiNotAccessibleException | IOException e) { logger.error("Exception occurred while fetching issuers ", e); responseWrapper.setErrors(List.of(new ErrorDTO(API_NOT_ACCESSIBLE_EXCEPTION.getCode(), API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); @@ -78,4 +94,73 @@ public ResponseEntity getIssuerConfig(@PathVariable("issuer-id") String return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); } + @GetMapping("/{issuer-id}/credentialTypes") + public ResponseEntity getCredentialTypes(@PathVariable("issuer-id") String issuerId, + @RequestParam(required = false) String search) { + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + responseWrapper.setId(ID); + responseWrapper.setVersion("v1"); + responseWrapper.setResponsetime(DateUtils.getRequestTimeString()); + IssuerSupportedCredentialsResponse credentialTypes; + try { + credentialTypes = issuersService.getCredentialsSupported(issuerId, search); + }catch (ApiNotAccessibleException | IOException exception){ + logger.error("Exception occurred while fetching credential types", exception); + responseWrapper.setErrors(List.of(new ErrorDTO(API_NOT_ACCESSIBLE_EXCEPTION.getCode(), API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + responseWrapper.setResponse(credentialTypes); + + if (credentialTypes.getSupportedCredentials() == null) { + logger.error("invalid issuer id passed - {}", issuerId); + responseWrapper.setErrors(List.of(new ErrorDTO(INVALID_ISSUER_ID_EXCEPTION.getCode(), INVALID_ISSUER_ID_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(responseWrapper); + } + + return ResponseEntity.status(HttpStatus.OK).body(responseWrapper); + } + + @GetMapping("/{issuer-id}/credentials/{credentialType}/download") + public ResponseEntity generatePdfForVC(@RequestHeader("Bearer") String token, + @PathVariable("issuer-id") String issuerId, + @PathVariable("credentialType") String credentialType) { + + ResponseWrapper responseWrapper = new ResponseWrapper<>(); + responseWrapper.setId(ID); + responseWrapper.setVersion("v1"); + responseWrapper.setResponsetime(DateUtils.getRequestTimeString()); + + try{ + IssuerDTO issuerConfig = issuersService.getIssuerConfig(issuerId); + CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse = issuersService.getCredentialWellKnownFromJson(); + Optional credentialsSupportedResponse = credentialIssuerWellKnownResponse.getCredentialsSupported().stream() + .filter(credentialsSupported -> credentialsSupported.getId().equals(credentialType)) + .findFirst(); + if (credentialsSupportedResponse.isEmpty()){ + logger.error("Invalid credential Type passed - {}", credentialType); + responseWrapper.setErrors(List.of(new ErrorDTO(INVALID_CREDENTIAL_TYPE_EXCEPTION.getCode(), INVALID_CREDENTIAL_TYPE_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } + ByteArrayInputStream inputStream = issuersService.generatePdfForVerifiableCredentials(token, issuerConfig, credentialsSupportedResponse.get(), credentialIssuerWellKnownResponse.getCredentialEndPoint()); + //PDF file name with issuer display name and credential type display name + String pdfFileName = issuerConfig.getDisplay().stream().filter(displayDTO -> defaultLanguageConstant.equals(displayDTO.getLanguage())).map(DisplayDTO::getName).findFirst().orElse(null) + + "_" + credentialsSupportedResponse.get().getDisplay().stream() + .filter(credentialSupportedDisplayResponse -> defaultLanguageConstant.equals(credentialSupportedDisplayResponse.getLocale())).map(CredentialSupportedDisplayResponse::getName).findFirst().orElse(null); + return ResponseEntity + .ok() + .contentType(MediaType.APPLICATION_PDF) + .header(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=%s.pdf", pdfFileName)) + .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "Content-Disposition") + .body(new InputStreamResource(inputStream)); + }catch (ApiNotAccessibleException | IOException exception){ + logger.error("Exception occurred while fetching credential types ", exception); + responseWrapper.setErrors(List.of(new ErrorDTO(API_NOT_ACCESSIBLE_EXCEPTION.getCode(), API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseWrapper); + } catch (Exception exception) { + logger.error("Exception occurred while generating pdf ", exception); + responseWrapper.setErrors(List.of(new ErrorDTO(MIMOTO_PDF_SIGN_EXCEPTION.getCode(), MIMOTO_PDF_SIGN_EXCEPTION.getMessage()))); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(responseWrapper); + } + } + } diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDefinitionResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDefinitionResponseDto.java new file mode 100644 index 00000000..ce001b8a --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDefinitionResponseDto.java @@ -0,0 +1,12 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class CredentialDefinitionResponseDto { + private List type; + private Map credentialSubject; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDisplayResponseDto.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDisplayResponseDto.java new file mode 100644 index 00000000..caa4faa4 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialDisplayResponseDto.java @@ -0,0 +1,10 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +import java.util.List; + +@Data +public class CredentialDisplayResponseDto { + private List display; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerDisplayResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerDisplayResponse.java new file mode 100644 index 00000000..5a6cf683 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerDisplayResponse.java @@ -0,0 +1,9 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class CredentialIssuerDisplayResponse { + private String name; + private String locale; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerWellKnownResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerWellKnownResponse.java new file mode 100644 index 00000000..5cb868cc --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialIssuerWellKnownResponse.java @@ -0,0 +1,22 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.util.List; + +@Data +public class CredentialIssuerWellKnownResponse { + @SerializedName("credential_issuer") + @JsonProperty("credential_issuer") + private String credentialIssuer; + + @SerializedName("credential_endpoint") + @JsonProperty("credential_endpoint") + private String credentialEndPoint; + + @SerializedName("credentials_supported") + @JsonProperty("credentials_supported") + private List credentialsSupported; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialSupportedDisplayResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialSupportedDisplayResponse.java new file mode 100644 index 00000000..09ee09d8 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialSupportedDisplayResponse.java @@ -0,0 +1,38 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import io.mosip.mimoto.dto.LogoDTO; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; + +@Data +public class CredentialSupportedDisplayResponse { + + @Expose + @NotBlank + String name; + + @Expose + @Valid + LogoDTO logo; + + @Expose + @NotBlank + String locale; + + @JsonProperty("background_color") + @SerializedName("background_color") + @Expose + @NotBlank + String backgroundColor; + + @JsonProperty("text_color") + @SerializedName("text_color") + @Expose + @NotBlank + String textColor; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialsSupportedResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialsSupportedResponse.java new file mode 100644 index 00000000..42ee4f87 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/CredentialsSupportedResponse.java @@ -0,0 +1,24 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +import java.util.List; + +@Data +public class CredentialsSupportedResponse { + private String format; + private String id; + private String scope; + + @SerializedName("proof_types_supported") + @JsonProperty("proof_types_supported") + private List proofTypesSupported; + + @SerializedName("credential_definition") + @JsonProperty("credential_definition") + private CredentialDefinitionResponseDto credentialDefinition; + + private List display; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/IssuerSupportedCredentialsResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/IssuerSupportedCredentialsResponse.java new file mode 100644 index 00000000..1a2ecc86 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/IssuerSupportedCredentialsResponse.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class IssuerSupportedCredentialsResponse { + @JsonProperty("authorization_endpoint") + private String authorizationEndPoint; + private List supportedCredentials; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialDefinition.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialDefinition.java new file mode 100644 index 00000000..a07a56ab --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialDefinition.java @@ -0,0 +1,24 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Map; + +@Data +@Builder +public class VCCredentialDefinition { + + @JsonProperty("@context") + private List<@NotBlank String> context; + + @NotEmpty + private List<@NotBlank String> type; + + private Map credentialSubject; + +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialIssueBody.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialIssueBody.java new file mode 100644 index 00000000..23ffad92 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialIssueBody.java @@ -0,0 +1,13 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +@Data +public class VCCredentialIssueBody { + private VCCredentialProperties credential; + private String credentialSchemaId; + private String createdAt; + private String createdBy; + private String updatedAt; + private String updatedBy; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialProperties.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialProperties.java new file mode 100644 index 00000000..682767ac --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialProperties.java @@ -0,0 +1,28 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.List; +import java.util.Map; + +@Data +public class VCCredentialProperties { + private String issuer; + + private String id; + + private String issuanceDate; + + private VCCredentialResponseProof proof; + + private Map credentialSubject; + + @JsonProperty("@context") + private Object context; + + @NotEmpty + private List<@NotBlank String> type; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequest.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequest.java new file mode 100644 index 00000000..671ca172 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequest.java @@ -0,0 +1,27 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + + +@Data +@Builder +public class VCCredentialRequest { + + @NotBlank + private String format; + + @Valid + @NotNull + private VCCredentialRequestProof proof; + + @JsonProperty("credential_definition") + @Valid + @NotNull + private VCCredentialDefinition credentialDefinition; +} \ No newline at end of file diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequestProof.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequestProof.java new file mode 100644 index 00000000..2c1f1635 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialRequestProof.java @@ -0,0 +1,20 @@ +package io.mosip.mimoto.dto.mimoto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@Builder +public class VCCredentialRequestProof { + + @JsonProperty("proof_type") + @NotBlank + private String proofType; + + private String jwt; + + private String cwt; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponse.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponse.java new file mode 100644 index 00000000..5d83fd65 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponse.java @@ -0,0 +1,18 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +public class VCCredentialResponse { + + @NotBlank + private String format; + + @Valid + @NotNull + private VCCredentialProperties credential; +} diff --git a/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponseProof.java b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponseProof.java new file mode 100644 index 00000000..8a48fc00 --- /dev/null +++ b/src/main/java/io/mosip/mimoto/dto/mimoto/VCCredentialResponseProof.java @@ -0,0 +1,19 @@ +package io.mosip.mimoto.dto.mimoto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class VCCredentialResponseProof { + @NotBlank + private String type; + @NotBlank + private String created; + @NotBlank + private String proofPurpose; + @NotBlank + private String verificationMethod; + @NotBlank + private String jws; +} diff --git a/src/main/java/io/mosip/mimoto/exception/PlatformErrorMessages.java b/src/main/java/io/mosip/mimoto/exception/PlatformErrorMessages.java index b936e221..833b4b57 100644 --- a/src/main/java/io/mosip/mimoto/exception/PlatformErrorMessages.java +++ b/src/main/java/io/mosip/mimoto/exception/PlatformErrorMessages.java @@ -81,7 +81,8 @@ public enum PlatformErrorMessages { MIMOTO_IDP_CONSENT_EXCEPTION(PlatformConstants.PREFIX + "032", "Idp consent exception occured"), MIMOTO_IDP_OTP_EXCEPTION(PlatformConstants.PREFIX + "033", "IDP Otp error occured"), MIMOTO_IDP_GENERIC_EXCEPTION(PlatformConstants.PREFIX + "034", "Could not get response from server"), - INVALID_ISSUER_ID_EXCEPTION(PlatformConstants.PREFIX + "035", "Invalid issuer ID"); + INVALID_ISSUER_ID_EXCEPTION(PlatformConstants.PREFIX + "035", "Invalid issuer ID"), + INVALID_CREDENTIAL_TYPE_EXCEPTION(PlatformConstants.PREFIX + "036", "Invalid Credential Type Id"); /** The error message. */ private final String errorMessage; diff --git a/src/main/java/io/mosip/mimoto/service/IssuersService.java b/src/main/java/io/mosip/mimoto/service/IssuersService.java index 7bd0fd0d..70b7b08a 100644 --- a/src/main/java/io/mosip/mimoto/service/IssuersService.java +++ b/src/main/java/io/mosip/mimoto/service/IssuersService.java @@ -2,14 +2,27 @@ import io.mosip.mimoto.dto.IssuerDTO; import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.dto.mimoto.IssuerSupportedCredentialsResponse; import io.mosip.mimoto.exception.ApiNotAccessibleException; +import java.io.ByteArrayInputStream; import java.io.IOException; public interface IssuersService { - IssuersDTO getAllIssuers() throws ApiNotAccessibleException, IOException; + IssuersDTO getAllIssuers(String search) throws ApiNotAccessibleException, IOException; IssuersDTO getAllIssuersWithAllFields() throws ApiNotAccessibleException, IOException; IssuerDTO getIssuerConfig(String issuerId) throws ApiNotAccessibleException, IOException; + + IssuerSupportedCredentialsResponse getCredentialsSupported(String issuerId, String search) throws ApiNotAccessibleException, IOException; + + CredentialIssuerWellKnownResponse getCredentialWellKnownFromJson() throws IOException, ApiNotAccessibleException; + + ByteArrayInputStream generatePdfForVerifiableCredentials(String accessToken, IssuerDTO issuerDTO, CredentialsSupportedResponse credentialsSupportedResponse, String credentialEndPoint) throws Exception; + + + } diff --git a/src/main/java/io/mosip/mimoto/service/impl/IssuersServiceImpl.java b/src/main/java/io/mosip/mimoto/service/impl/IssuersServiceImpl.java index deaedfda..e7dc2bc8 100644 --- a/src/main/java/io/mosip/mimoto/service/impl/IssuersServiceImpl.java +++ b/src/main/java/io/mosip/mimoto/service/impl/IssuersServiceImpl.java @@ -2,24 +2,75 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.itextpdf.html2pdf.ConverterProperties; +import com.itextpdf.html2pdf.HtmlConverter; +import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider; +import com.itextpdf.kernel.pdf.PdfWriter; +import io.mosip.kernel.core.logger.spi.Logger; import io.mosip.mimoto.dto.IssuerDTO; import io.mosip.mimoto.dto.IssuersDTO; +import io.mosip.mimoto.dto.mimoto.CredentialDisplayResponseDto; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.dto.mimoto.IssuerSupportedCredentialsResponse; +import io.mosip.mimoto.dto.mimoto.VCCredentialDefinition; +import io.mosip.mimoto.dto.mimoto.VCCredentialRequest; +import io.mosip.mimoto.dto.mimoto.VCCredentialRequestProof; +import io.mosip.mimoto.dto.mimoto.VCCredentialResponse; import io.mosip.mimoto.exception.ApiNotAccessibleException; import io.mosip.mimoto.service.IssuersService; +import io.mosip.mimoto.util.JoseUtil; +import io.mosip.mimoto.util.LoggerUtil; +import io.mosip.mimoto.util.RestApiClient; import io.mosip.mimoto.util.Utilities; +import org.apache.commons.lang.StringUtils; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.StringWriter; +import java.security.PublicKey; +import java.text.ParseException; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Properties; +import java.util.stream.Collectors; @Service public class IssuersServiceImpl implements IssuersService { + private final Logger logger = LoggerUtil.getLogger(IssuersServiceImpl.class); + + private static final String context = "https://www.w3.org/2018/credentials/v1"; + @Autowired private Utilities utilities; + @Autowired + private RestApiClient restApiClient; + + @Autowired + private JoseUtil joseUtil; + + @Value("${mosip.oidc.p12.filename}") + private String fileName; + + @Value("${mosip.oidc.p12.password}") + private String cyptoPassword; + + @Value("${mosip.oidc.p12.path}") + String keyStorePath; + @Override - public IssuersDTO getAllIssuers() throws ApiNotAccessibleException, IOException { + public IssuersDTO getAllIssuers(String search) throws ApiNotAccessibleException, IOException { IssuersDTO issuers; String issuersConfigJsonValue = utilities.getIssuersConfigJsonValue(); if (issuersConfigJsonValue == null) { @@ -28,6 +79,15 @@ public IssuersDTO getAllIssuers() throws ApiNotAccessibleException, IOException Gson gsonWithIssuerDataOnlyFilter = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); issuers = gsonWithIssuerDataOnlyFilter.fromJson(issuersConfigJsonValue, IssuersDTO.class); + // Filter issuers list with search string + if (!StringUtils.isEmpty(search)) { + List filteredIssuers = issuers.getIssuers().stream() + .filter(issuer -> issuer.getDisplay().stream() + .anyMatch(displayDTO -> displayDTO.getName().toLowerCase().contains(search.toLowerCase()))) + .collect(Collectors.toList()); + issuers.setIssuers(filteredIssuers); + return issuers; + } return issuers; } @@ -61,4 +121,139 @@ public IssuerDTO getIssuerConfig(String issuerId) throws ApiNotAccessibleExcepti issuerDTO = issuerConfigResp.get(); return issuerDTO; } + + @Override + public IssuerSupportedCredentialsResponse getCredentialsSupported(String issuerId, String search) throws ApiNotAccessibleException, IOException { + IssuerSupportedCredentialsResponse credentialTypesWithAuthorizationEndpoint = new IssuerSupportedCredentialsResponse(); + + IssuersDTO issuersDto = getAllIssuersWithAllFields(); + + Optional issuerConfigResp = issuersDto.getIssuers().stream() + .filter(issuer -> issuer.getCredential_issuer().equals(issuerId)) + .findFirst(); + if (issuerConfigResp.isPresent()) { + IssuerDTO issuerDto = issuerConfigResp.get(); + + // Get credential supported types from well known endpoint + CredentialIssuerWellKnownResponse response = restApiClient.getApi(issuerDto.getCredential_issuer(), CredentialIssuerWellKnownResponse.class); + if (response == null) { + response = getCredentialWellKnownFromJson(); + } + List issuerCredentialsSupported = response.getCredentialsSupported(); + credentialTypesWithAuthorizationEndpoint.setAuthorizationEndPoint(issuerDto.getAuthorization_endpoint()); + credentialTypesWithAuthorizationEndpoint.setSupportedCredentials(issuerCredentialsSupported); + + // Filter Credential supported types with search string + if (!StringUtils.isEmpty(search)){ + credentialTypesWithAuthorizationEndpoint.setSupportedCredentials(issuerCredentialsSupported + .stream() + .filter(credentialsSupportedResponse -> credentialsSupportedResponse.getDisplay().stream() + .anyMatch(credDisplay -> credDisplay.getName().toLowerCase().contains(search.toLowerCase()))) + .collect(Collectors.toList())); + } + return credentialTypesWithAuthorizationEndpoint; + } + return credentialTypesWithAuthorizationEndpoint; + } + + @Override + public CredentialIssuerWellKnownResponse getCredentialWellKnownFromJson() throws IOException, ApiNotAccessibleException { + String credentialsSupportedResponseJson = utilities.getCredentialsSupportedConfigJsonValue(); + if (credentialsSupportedResponseJson == null){ + throw new ApiNotAccessibleException(); + } + return new Gson().fromJson(credentialsSupportedResponseJson, CredentialIssuerWellKnownResponse.class); + } + + @Override + public ByteArrayInputStream generatePdfForVerifiableCredentials(String accessToken, IssuerDTO issuerDTO, CredentialsSupportedResponse credentialsSupportedResponse, String credentialEndPoint) throws Exception { + LinkedHashMap vcPropertiesFromWellKnown = new LinkedHashMap<>(); + Map credentialSubject = credentialsSupportedResponse.getCredentialDefinition().getCredentialSubject(); + //populating display properties from credential Types json for pdf + credentialSubject.keySet().forEach(VCProperty -> vcPropertiesFromWellKnown.put(VCProperty, credentialSubject.get(VCProperty).getDisplay().get(0).getName())); + String backgroundColor = credentialsSupportedResponse.getDisplay().get(0).getBackgroundColor(); + String textColor = credentialsSupportedResponse.getDisplay().get(0).getTextColor(); + VCCredentialRequest vcCredentialRequest = generateVCCredentialRequest(issuerDTO, credentialsSupportedResponse, accessToken); + logger.debug("VC Credential Request is -> " + vcCredentialRequest); + //Esignet API call for credential issue + VCCredentialResponse vcCredentialResponse = restApiClient.postApi(credentialEndPoint, MediaType.APPLICATION_JSON, + vcCredentialRequest, VCCredentialResponse.class, accessToken); + logger.debug("VC Credential Response is -> " + vcCredentialResponse); + if (vcCredentialResponse == null) throw new RuntimeException("VC Credential Issue API not accessible"); + Map credentialProperties = vcCredentialResponse.getCredential().getCredentialSubject(); + LinkedHashMap displayProperties = new LinkedHashMap<>(); + vcPropertiesFromWellKnown.keySet().forEach(vcProperty -> displayProperties.put(vcPropertiesFromWellKnown.get(vcProperty), credentialProperties.get(vcProperty))); + return getPdfResourceFromVcProperties(displayProperties, textColor, backgroundColor, + credentialsSupportedResponse.getDisplay().get(0).getName(), + issuerDTO.getDisplay().stream().map(d -> d.getLogo().getUrl()).findFirst().orElse("")); + } + + private VCCredentialRequest generateVCCredentialRequest(IssuerDTO issuerDTO, CredentialsSupportedResponse credentialsSupportedResponse, String accessToken) throws ParseException { + //Getting public key from the p12 file + PublicKey publicKeyString = joseUtil.getPublicKeyString(keyStorePath, fileName, issuerDTO.getClient_alias(), cyptoPassword); + //Generating proof from the public key with custom header + String jwt = joseUtil.generateJwt(publicKeyString, keyStorePath, fileName, issuerDTO.getClient_alias(), + cyptoPassword, issuerDTO.getCredential_audience(), issuerDTO.getClient_id(), accessToken); + return VCCredentialRequest.builder() + .format(credentialsSupportedResponse.getFormat()) + .proof(VCCredentialRequestProof.builder() + .proofType(credentialsSupportedResponse.getProofTypesSupported().get(0)) + .jwt(jwt) + .build()) + .credentialDefinition(VCCredentialDefinition.builder() + .type(credentialsSupportedResponse.getCredentialDefinition().getType()) + .context(List.of(context)) + .build()) + .build(); + } + + private ByteArrayInputStream getPdfResourceFromVcProperties(LinkedHashMap displayProperties, String textColor, + String backgroundColor, + String credentialSupportedType, String issuerLogoUrl) throws IOException { + Map data = new HashMap<>(); + LinkedHashMap headerProperties = new LinkedHashMap<>(); + LinkedHashMap rowProperties = new LinkedHashMap<>(); + + //Assigning two properties at the top of pdf and the rest dynamically below them + displayProperties.entrySet().stream() + .forEachOrdered(entry -> { + if (headerProperties.size() < 2) { + headerProperties.put(entry.getKey(), entry.getValue()); + } else { + rowProperties.put(entry.getKey(), entry.getValue()); + } + }); + + int rowPropertiesCount = rowProperties.size(); + data.put("logoUrl", issuerLogoUrl); + data.put("headerProperties", headerProperties); + data.put("rowProperties", rowProperties); + data.put("keyFontColor", textColor); + data.put("bgColor", backgroundColor); + data.put("rowPropertiesMargin", rowPropertiesCount % 2 == 0 ? (rowPropertiesCount/2 -1)*40 : (rowPropertiesCount/2)*40); //for adjusting the height in pdf for dynamic properties + data.put("titleName", credentialSupportedType); + + String credentialTemplate = utilities.getCredentialSupportedTemplateString(); + + Properties props = new Properties(); + props.setProperty("resource.loader", "class"); + props.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); + Velocity.init(props); + VelocityContext velocityContext = new VelocityContext(data); + + // Merge the context with the template + StringWriter writer = new StringWriter(); + Velocity.evaluate(velocityContext, writer, "Credential Template", credentialTemplate); + + // Get the merged HTML string + String mergedHtml = writer.toString(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + PdfWriter pdfwriter = new PdfWriter(outputStream); + DefaultFontProvider defaultFont = new DefaultFontProvider(true, false, false); + ConverterProperties converterProperties = new ConverterProperties(); + converterProperties.setFontProvider(defaultFont); + HtmlConverter.convertToPdf(mergedHtml, pdfwriter, converterProperties); + return new ByteArrayInputStream(outputStream.toByteArray()); + } } diff --git a/src/main/java/io/mosip/mimoto/util/JoseUtil.java b/src/main/java/io/mosip/mimoto/util/JoseUtil.java index 837c5fe0..9ea1e2f4 100644 --- a/src/main/java/io/mosip/mimoto/util/JoseUtil.java +++ b/src/main/java/io/mosip/mimoto/util/JoseUtil.java @@ -2,12 +2,16 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; -import com.auth0.jwt.interfaces.RSAKeyProvider; import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.KeyUse; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.util.X509CertUtils; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; import io.mosip.kernel.core.logger.spi.Logger; import io.mosip.kernel.core.util.CryptoUtil; import io.mosip.mimoto.core.http.ResponseWrapper; @@ -16,7 +20,6 @@ import io.mosip.mimoto.dto.mimoto.WalletBindingResponseDto; import org.jose4j.jws.JsonWebSignature; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; @@ -28,6 +31,7 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.RSAPublicKeySpec; import java.security.spec.X509EncodedKeySpec; +import java.text.ParseException; import java.time.Instant; import java.util.Date; import java.util.HashMap; @@ -143,7 +147,7 @@ public String getJWT(String clientId, String keyStorePath, String fileName, Stri KeyStore.PrivateKeyEntry privateKeyEntry = cryptoCoreUtil.loadP12(keyStorePathWithFileName, alias, cyptoPassword); privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey(); } catch (IOException e) { - logger.error("Exception happened while loading the p12 file for invoking token call."); + logger.error("Exception happened while loading the p12 file for invoking token call."); } return JWT.create() .withHeader(header) @@ -154,4 +158,63 @@ public String getJWT(String clientId, String keyStorePath, String fileName, Stri .withIssuedAt(issuedAt) .sign(Algorithm.RSA256(null, privateKey)); } + + public PublicKey getPublicKeyString(String keyStorePath, String fileName, String alias, String cyptoPassword){ + String keyStorePathWithFileName = keyStorePath + fileName; + try { + KeyStore.PrivateKeyEntry privateKeyEntry = cryptoCoreUtil.loadP12(keyStorePathWithFileName, alias, cyptoPassword); + PublicKey publicKey = privateKeyEntry.getCertificate().getPublicKey(); + return publicKey; + } catch (IOException e) { + logger.error("Exception happened while loading the p12 file for invoking token call."); + } + return null; + } + //Added this Util for generating JWT as the existing Jwt builder will not allow to have a custom header "typ" + public String generateJwt(PublicKey publicJwk, String keyStorePath, String fileName, String alias, String cyptoPassword, String audience, String clientId, String accessToken) throws ParseException { + RSAKey jwk = new RSAKey.Builder((java.security.interfaces.RSAPublicKey) publicJwk) + .keyUse(KeyUse.SIGNATURE) + .algorithm(JWSAlgorithm.RS256) + .build(); + + Map header = new HashMap<>(); + + header.put("alg", ALG_RS256); + header.put("typ", "openid4vci-proof+jwt"); + header.put("jwk", jwk.getRequiredParams()); + + + String keyStorePathWithFileName = keyStorePath + fileName; + Date issuedAt = Date.from(Instant.now()); + Date expiresAt = Date.from(Instant.now().plusMillis(120000000)); + RSAPrivateKey privateKey = null; + try { + KeyStore.PrivateKeyEntry privateKeyEntry = cryptoCoreUtil.loadP12(keyStorePathWithFileName, alias, cyptoPassword); + privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey(); + } catch (IOException e) { + logger.error("Exception happened while loading the p12 file for invoking token call."); + } + SignedJWT jwt = (SignedJWT) JWTParser.parse(accessToken); + Map jsonObject = jwt.getPayload().toJSONObject(); + String cNounce = (String) jsonObject.get("c_nonce"); + + Map payload = new HashMap<>(); + payload.put("sub", clientId); + payload.put("aud", audience); + payload.put("nonce", cNounce); + payload.put("iss", clientId); + payload.put("exp", expiresAt.toInstant().getEpochSecond()); + payload.put("iat", issuedAt.toInstant().getEpochSecond()); + + return Jwts.builder() + .setHeader(header) + .setIssuer(clientId) + .setSubject(clientId) + .setAudience(audience) + .setExpiration(expiresAt) + .setIssuedAt(issuedAt) + .setClaims(payload) + .signWith(SignatureAlgorithm.RS256, privateKey) + .compact(); + } } diff --git a/src/main/java/io/mosip/mimoto/util/RestApiClient.java b/src/main/java/io/mosip/mimoto/util/RestApiClient.java index 87cbc070..1df44902 100644 --- a/src/main/java/io/mosip/mimoto/util/RestApiClient.java +++ b/src/main/java/io/mosip/mimoto/util/RestApiClient.java @@ -178,6 +178,42 @@ public T postApi(String uri, MediaType mediaType, Object requestType, Class< return result; } + public T postApi(String uri, MediaType mediaType, Object requestType, Class responseClass, String bearerToken){ + T result = null; + try { + logger.info("RestApiClient::postApi()::entry uri: {}", uri); + result = (T) plainRestTemplate.postForObject(uri, setRequestHeader(requestType, mediaType, bearerToken), responseClass); + } catch (Exception e) { + logger.error("RestApiClient::postApi()::error uri: {} {} {}", uri, e.getMessage(), e); + } + return result; + } + + private HttpEntity setRequestHeader(Object requestType, MediaType mediaType, String bearerToken){ + MultiValueMap headers = new LinkedMultiValueMap(); + if (mediaType != null) { + headers.add(CONTENT_TYPE, mediaType.toString()); + } + headers.add("Authorization", "Bearer "+bearerToken); + if (requestType != null) { + try { + HttpEntity httpEntity = (HttpEntity) requestType; + HttpHeaders httpHeader = httpEntity.getHeaders(); + Iterator iterator = httpHeader.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (!(headers.containsKey(CONTENT_TYPE) && key.equals(CONTENT_TYPE))) + headers.add(key, Objects.requireNonNull(httpHeader.get(key)).get(0)); + } + + return new HttpEntity(httpEntity.getBody(), headers); + } catch (ClassCastException | NullPointerException e) { + return new HttpEntity(requestType, headers); + } + } else + return new HttpEntity(headers); + } + private HttpEntity setRequestHeader(Object requestType, MediaType mediaType) throws IOException { return setRequestHeader(requestType, mediaType, false); } diff --git a/src/main/java/io/mosip/mimoto/util/Utilities.java b/src/main/java/io/mosip/mimoto/util/Utilities.java index 3c693c93..2e0dfc18 100644 --- a/src/main/java/io/mosip/mimoto/util/Utilities.java +++ b/src/main/java/io/mosip/mimoto/util/Utilities.java @@ -103,6 +103,12 @@ public class Utilities { @Value("${mosip.openid.issuers}") private String getIssuersConfigJson; + @Value("${mosip.openid.issuer.credentialSupported}") + private String getIssuerCredentialSupportedJson; + + @Value("${mosip.openid.htmlTemplate}") + private String getCredentialSupportedHtml; + private String mappingJsonString = null; private String identityMappingJsonString = null; @@ -111,9 +117,17 @@ public class Utilities { private String issuersConfigJsonString = null; + private String credentialsSupportedJsonString = null; + + private String credentialTemplateHtmlString = null; // uncomment for running mimoto Locally to populate the issuers json -// public Utilities(@Value("classpath:mimoto-issuers-config.json") Resource resource) throws IOException { +// public Utilities(@Value("classpath:/wellKnownIssuer/Insurance.json") Resource credentialsSupportedResource, +// @Value("classpath:mimoto-issuers-config.json") Resource resource, +// @Value("classpath:/templates/CredentialTemplate.html") Resource credentialTemplateResource) throws IOException{ +// // issuersConfigJsonString = (Files.readString(resource.getFile().toPath())); +// credentialsSupportedJsonString = (Files.readString(credentialsSupportedResource.getFile().toPath())); +// credentialTemplateHtmlString = (Files.readString(credentialTemplateResource.getFile().toPath())); // } public JSONObject getTemplate() throws JsonParseException, JsonMappingException, IOException { @@ -284,6 +298,16 @@ public String getIssuersConfigJsonValue() throws IOException { issuersConfigJsonString : getJson(configServerFileStorageURL, getIssuersConfigJson); } + public String getCredentialsSupportedConfigJsonValue() throws IOException{ + return (credentialsSupportedJsonString != null && !credentialsSupportedJsonString.isEmpty()) ? + credentialsSupportedJsonString : getJson(configServerFileStorageURL, getIssuerCredentialSupportedJson); + } + + public String getCredentialSupportedTemplateString() throws IOException{ + return (credentialTemplateHtmlString != null && !credentialTemplateHtmlString.isEmpty()) ? + credentialTemplateHtmlString : getJson(configServerFileStorageURL, getCredentialSupportedHtml); + } + /** * retrieve UIN from IDRepo by registration id. diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index 39b6514f..ef23e6d9 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -200,6 +200,8 @@ wallet.binding.partner.api.key=Aci9jg28B8mO_LDfDXo3ZTp5_HKgEMun2tYyHCa1e8k # OpenID mosip.openid.issuers=mimoto-issuers-config.json +mosip.openid.issuer.credentialSupported=/wellKnownIssuer/Insurance.json +mosip.openid.htmlTemplate=CredentialTemplate.html #configurations related to openid4vc mosip.oidc.client.assertion.type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer mosip.oidc.p12.filename=oidckeystore.p12 diff --git a/src/main/resources/templates/CredentialTemplate.html b/src/main/resources/templates/CredentialTemplate.html new file mode 100644 index 00000000..b7a1e20a --- /dev/null +++ b/src/main/resources/templates/CredentialTemplate.html @@ -0,0 +1,46 @@ + + + + + + Identity Card + + + +
+
+ $titleName +
+
+
+ Logo +
+
+ #foreach($entry in $headerProperties.entrySet()) +
+
$entry.key
+
$entry.value
+
+ #end +
+
+ +
+
+
+ #foreach($entry in $rowProperties.entrySet()) + #if($entry.value) +
+
$entry.key
+
$entry.value
+
+ #end + #end +
+
+ Logo +
+
+ + + \ No newline at end of file diff --git a/src/main/resources/wellKnownIssuer/Insurance.json b/src/main/resources/wellKnownIssuer/Insurance.json new file mode 100644 index 00000000..a701f1f0 --- /dev/null +++ b/src/main/resources/wellKnownIssuer/Insurance.json @@ -0,0 +1,170 @@ +{ + "credential_issuer": "MosipInsurance", + "credential_endpoint": "http://localhost:8088/v1/esignet/vci/credential", + "credentials_supported": [ + { + "format": "ldp_vc", + "id": "InsuranceCredential", + "scope": "sunbird_rc_insurance_vc_ldp", + "cryptographic_binding_methods_supported": [ + "did:jwk" + ], + "cryptographic_suites_supported": [ + "RsaSignature2018" + ], + "proof_types_supported": [ + "jwt" + ], + "credential_definition": { + "type": [ + "VerifiableCredential", + "InsuranceCredential" + ], + "credentialSubject": { + "fullName": { + "display": [ + { + "name": "Full Name", + "locale": "en" + } + ] + }, + "policyName": { + "display": [ + { + "name": "Policy Name", + "locale": "en" + } + ] + }, + "policyNumber": { + "display": [ + { + "name": "Policy Number", + "locale": "en" + } + ] + }, + "gender": { + "display": [ + { + "name": "Gender", + "locale": "en" + } + ] + }, + "expiresOn": { + "display": [ + { + "name": "Expiry Date", + "locale": "en" + } + ] + }, + "dob": { + "display": [ + { + "name": "Date Of Birth", + "locale": "en" + } + ] + } + } + }, + "display": [ + { + "name": "MOSIP Insurance", + "locale": "en", + "logo": { + "url": "https://api.qa-inji1.mosip.net/inji/mosip-logo.png", + "alt_text": "insurance logo" + }, + "background_color": "#fafcff", + "text_color": "#00284d" + } + ] + }, + { + "format": "ldp_vc", + "id": "LifeInsuranceCredential_ldp", + "scope": "life_insurance_vc_ldp", + "cryptographic_binding_methods_supported": [ + "did:jwk" + ], + "cryptographic_suites_supported": [ + "RsaSignature2018" + ], + "proof_types_supported": [ + "jwt" + ], + "credential_definition": { + "type": [ + "VerifiableCredential", + "LifeInsuranceCredential" + ], + "credentialSubject": { + "fullName": { + "display": [ + { + "name": "Full Name", + "locale": "en" + } + ] + }, + "policyName": { + "display": [ + { + "name": "Policy Name", + "locale": "en" + } + ] + }, + "policyNumber": { + "display": [ + { + "name": "Policy Number", + "locale": "en" + } + ] + }, + "gender": { + "display": [ + { + "name": "Gender", + "locale": "en" + } + ] + }, + "policyExpiresOn": { + "display": [ + { + "name": "Expiry Date", + "locale": "en" + } + ] + }, + "dob": { + "display": [ + { + "name": "Date Of Birth", + "locale": "en" + } + ] + } + } + }, + "display": [ + { + "name": "Sunbird Life Insurance", + "locale": "en", + "logo": { + "url": "https://sunbird.org/images/sunbird-logo-new.png", + "alt_text": "Sunbird life insurance logo" + }, + "background_color": "#fefcfa", + "text_color": "#7C4616" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/java/io/mosip/mimoto/controller/InjiControllerTest.java b/src/test/java/io/mosip/mimoto/controller/InjiControllerTest.java index 972d67b2..f29f754b 100644 --- a/src/test/java/io/mosip/mimoto/controller/InjiControllerTest.java +++ b/src/test/java/io/mosip/mimoto/controller/InjiControllerTest.java @@ -40,16 +40,17 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.file.Files; import java.util.*; +import java.util.stream.Collectors; import static io.mosip.mimoto.exception.PlatformErrorMessages.API_NOT_ACCESSIBLE_EXCEPTION; import static io.mosip.mimoto.exception.PlatformErrorMessages.INVALID_ISSUER_ID_EXCEPTION; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @RunWith(SpringRunner.class) @SpringBootTest(classes = TestBootApplication.class) @@ -149,14 +150,127 @@ static IssuerDTO getIssuerDTO(String issuerName) { return issuer; } + static CredentialsSupportedResponse getCredentialSupportedResponse(String credentialSupportedName){ + LogoDTO logo = new LogoDTO(); + logo.setUrl("/logo"); + logo.setAlt_text("logo-url"); + CredentialSupportedDisplayResponse credentialSupportedDisplay = new CredentialSupportedDisplayResponse(); + credentialSupportedDisplay.setLogo(logo); + credentialSupportedDisplay.setName(credentialSupportedName); + credentialSupportedDisplay.setLocale("en"); + credentialSupportedDisplay.setTextColor("#FFFFFF"); + credentialSupportedDisplay.setBackgroundColor("#B34622"); + CredentialIssuerDisplayResponse credentialIssuerDisplayResponse = new CredentialIssuerDisplayResponse(); + credentialIssuerDisplayResponse.setName("Given Name"); + credentialIssuerDisplayResponse.setLocale("en"); + CredentialDisplayResponseDto credentialDisplayResponseDto = new CredentialDisplayResponseDto(); + credentialDisplayResponseDto.setDisplay(Collections.singletonList(credentialIssuerDisplayResponse)); + CredentialDefinitionResponseDto credentialDefinitionResponseDto = new CredentialDefinitionResponseDto(); + credentialDefinitionResponseDto.setType(List.of("VerifiableCredential", credentialSupportedName)); + credentialDefinitionResponseDto.setCredentialSubject(Map.of("name", credentialDisplayResponseDto)); + CredentialsSupportedResponse credentialsSupportedResponse = new CredentialsSupportedResponse(); + credentialsSupportedResponse.setFormat("ldp_vc"); + credentialsSupportedResponse.setId(credentialSupportedName); + credentialsSupportedResponse.setScope(credentialSupportedName+"_vc_ldp"); + credentialsSupportedResponse.setDisplay(Collections.singletonList(credentialSupportedDisplay)); + credentialsSupportedResponse.setProofTypesSupported(Collections.singletonList("jwt")); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinitionResponseDto); + return credentialsSupportedResponse; + } + + static CredentialIssuerWellKnownResponse getCredentialIssuerWellKnownResponseDto(String issuerName, List credentialsSupportedResponses){ + CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse = new CredentialIssuerWellKnownResponse(); + credentialIssuerWellKnownResponse.setCredentialIssuer(issuerName); + credentialIssuerWellKnownResponse.setCredentialEndPoint("credential_endpoint"); + credentialIssuerWellKnownResponse.setCredentialsSupported(credentialsSupportedResponses); + return credentialIssuerWellKnownResponse; + } + + @Test + public void getCredentialTypesTest() throws Exception{ + IssuerSupportedCredentialsResponse credentialTypesResponse = new IssuerSupportedCredentialsResponse(); + credentialTypesResponse.setSupportedCredentials(List.of(getCredentialSupportedResponse("credentialType1"), + getCredentialSupportedResponse("credentialType2"))); + credentialTypesResponse.setAuthorizationEndPoint("authorization-endpoint"); + Mockito.when(issuersService.getCredentialsSupported("Issuer1", null)) + .thenReturn(credentialTypesResponse) + .thenThrow(new ApiNotAccessibleException()); + Mockito.when(issuersService.getCredentialsSupported("invalidId", null)) + .thenReturn(new IssuerSupportedCredentialsResponse()); + + mockMvc.perform(get("/issuers/{issuer-id}/credentialTypes", "Issuer1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response", Matchers.allOf( + Matchers.hasKey("authorization_endpoint"), + Matchers.hasKey("supportedCredentials") + ))) + .andExpect(jsonPath("$.response.supportedCredentials", Matchers.everyItem( + Matchers.allOf( + Matchers.hasKey("format"), + Matchers.hasKey("id"), + Matchers.hasKey("scope"), + Matchers.hasKey("display"), + Matchers.hasKey("proof_types_supported"), + Matchers.hasKey("credential_definition") + ) + ))); + mockMvc.perform(get("/issuers/{issuer-id}/credentialTypes", "Issuer1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + + mockMvc.perform(get("/issuers/{issuer-id}/credentialTypes", "invalidId").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(INVALID_ISSUER_ID_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(INVALID_ISSUER_ID_EXCEPTION.getMessage()))); + } + + @Test + public void generatePdfForVCTest() throws Exception { + Mockito.when(issuersService.getIssuerConfig("Issuer1")) + .thenReturn(getIssuerDTO("Issuer1")) + .thenThrow(new ApiNotAccessibleException()); + + Mockito.when(issuersService.getCredentialWellKnownFromJson()) + .thenReturn(getCredentialIssuerWellKnownResponseDto("Issuer1", + List.of(getCredentialSupportedResponse("CredentialType1")))); + + + Mockito.when(issuersService.generatePdfForVerifiableCredentials("accessToken", + getIssuerDTO("Issuer1"), getCredentialSupportedResponse("CredentialType1"), + "credential_endpoint")) + .thenReturn(new ByteArrayInputStream("Mock Pdf".getBytes())) + .thenThrow(new Exception()); + + mockMvc.perform(get("/issuers/Issuer1/credentials/CredentialType1/download") + .header("Bearer", "accessToken") + .accept(MediaType.APPLICATION_PDF)) + .andExpect(status().isOk()); + + mockMvc.perform(get("/issuers/Issuer1/credentials/CredentialType1/download") + .header("Bearer", "accessToken").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + } + @Test public void getAllIssuersTest() throws Exception { IssuersDTO issuers = new IssuersDTO(); issuers.setIssuers((List.of(getIssuerDTO("Issuer1"), getIssuerDTO("Issuer2")))); - Mockito.when(issuersService.getAllIssuers()) + Mockito.when(issuersService.getAllIssuers(null)) .thenReturn(issuers) .thenThrow(new ApiNotAccessibleException()); + IssuersDTO filteredIssuers = new IssuersDTO(); + filteredIssuers.setIssuers(issuers.getIssuers().stream().filter(issuer -> issuer.getDisplay().stream() + .anyMatch(displayDTO -> displayDTO.getName().toLowerCase().contains("Issuer1".toLowerCase()))) + .collect(Collectors.toList())); + + Mockito.when(issuersService.getAllIssuers("Issuer1")) + .thenReturn(filteredIssuers) + .thenThrow(new ApiNotAccessibleException()); + mockMvc.perform(get("/issuers").accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(status().isOk()) .andExpect(jsonPath("$.response.issuers", Matchers.everyItem( @@ -175,10 +289,34 @@ public void getAllIssuersTest() throws Exception { ) ))); + mockMvc.perform(get("/issuers?search=Issuer1").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.response.issuers", Matchers.everyItem( + Matchers.allOf( + Matchers.hasKey("credential_issuer"), + Matchers.hasKey("display"), + Matchers.hasKey("client_id"), + Matchers.hasKey(".well-known"), + Matchers.not(Matchers.hasKey("redirect_url")), + Matchers.not(Matchers.hasKey("authorization_endpoint")), + Matchers.not(Matchers.hasKey("token_endpoint")), + Matchers.not(Matchers.hasKey("credential_endpoint")), + Matchers.not(Matchers.hasKey("credential_audience")), + Matchers.not(Matchers.hasKey("additional_headers")), + Matchers.not(Matchers.hasKey("scopes_supported")) + ) + ))); + mockMvc.perform(get("/issuers").accept(MediaType.APPLICATION_JSON_VALUE)) .andExpect(status().isInternalServerError()) .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); + + + mockMvc.perform(get("/issuers?search=Issuer1").accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.errors[0].errorCode", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getCode()))) + .andExpect(jsonPath("$.errors[0].errorMessage", Matchers.is(API_NOT_ACCESSIBLE_EXCEPTION.getMessage()))); } @Test diff --git a/src/test/java/io/mosip/mimoto/service/IssuersServiceTest.java b/src/test/java/io/mosip/mimoto/service/IssuersServiceTest.java index aad42b07..6aa85dfb 100644 --- a/src/test/java/io/mosip/mimoto/service/IssuersServiceTest.java +++ b/src/test/java/io/mosip/mimoto/service/IssuersServiceTest.java @@ -5,9 +5,19 @@ import io.mosip.mimoto.dto.IssuersDTO; import io.mosip.mimoto.dto.DisplayDTO; import io.mosip.mimoto.dto.LogoDTO; +import io.mosip.mimoto.dto.mimoto.CredentialDefinitionResponseDto; +import io.mosip.mimoto.dto.mimoto.CredentialDisplayResponseDto; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerDisplayResponse; +import io.mosip.mimoto.dto.mimoto.CredentialIssuerWellKnownResponse; +import io.mosip.mimoto.dto.mimoto.CredentialSupportedDisplayResponse; +import io.mosip.mimoto.dto.mimoto.CredentialsSupportedResponse; +import io.mosip.mimoto.dto.mimoto.IssuerSupportedCredentialsResponse; import io.mosip.mimoto.exception.ApiNotAccessibleException; import io.mosip.mimoto.service.impl.IssuersServiceImpl; +import io.mosip.mimoto.util.RestApiClient; import io.mosip.mimoto.util.Utilities; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -18,10 +28,13 @@ import org.springframework.boot.test.context.SpringBootTest; import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; import java.util.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; @RunWith(MockitoJUnitRunner.class) @SpringBootTest @@ -33,6 +46,9 @@ public class IssuersServiceTest { @Mock Utilities utilities; + @Mock + public RestApiClient restApiClient; + List issuerConfigRelatedFields = List.of("additional_headers", "authorization_endpoint","authorization_audience", "token_endpoint", "proxy_token_endpoint", "credential_endpoint", "credential_audience", "redirect_uri"); @@ -72,11 +88,49 @@ static IssuerDTO getIssuerDTO(String issuerName, List nullFields) { return issuer; } + static CredentialsSupportedResponse getCredentialSupportedResponse(String credentialSupportedName){ + LogoDTO logo = new LogoDTO(); + logo.setUrl("/logo"); + logo.setAlt_text("logo-url"); + CredentialSupportedDisplayResponse credentialSupportedDisplay = new CredentialSupportedDisplayResponse(); + credentialSupportedDisplay.setLogo(logo); + credentialSupportedDisplay.setName(credentialSupportedName); + credentialSupportedDisplay.setLocale("en"); + credentialSupportedDisplay.setTextColor("#FFFFFF"); + credentialSupportedDisplay.setBackgroundColor("#B34622"); + CredentialIssuerDisplayResponse credentialIssuerDisplayResponse = new CredentialIssuerDisplayResponse(); + credentialIssuerDisplayResponse.setName("Given Name"); + credentialIssuerDisplayResponse.setLocale("en"); + CredentialDisplayResponseDto credentialDisplayResponseDto = new CredentialDisplayResponseDto(); + credentialDisplayResponseDto.setDisplay(Collections.singletonList(credentialIssuerDisplayResponse)); + CredentialDefinitionResponseDto credentialDefinitionResponseDto = new CredentialDefinitionResponseDto(); + credentialDefinitionResponseDto.setType(List.of("VerifiableCredential", credentialSupportedName)); + credentialDefinitionResponseDto.setCredentialSubject(Map.of("name", credentialDisplayResponseDto)); + CredentialsSupportedResponse credentialsSupportedResponse = new CredentialsSupportedResponse(); + credentialsSupportedResponse.setFormat("ldp_vc"); + credentialsSupportedResponse.setId(credentialSupportedName+"id"); + credentialsSupportedResponse.setScope(credentialSupportedName+"_vc_ldp"); + credentialsSupportedResponse.setDisplay(Collections.singletonList(credentialSupportedDisplay)); + credentialsSupportedResponse.setProofTypesSupported(Collections.singletonList("jwt")); + credentialsSupportedResponse.setCredentialDefinition(credentialDefinitionResponseDto); + return credentialsSupportedResponse; + } + + static CredentialIssuerWellKnownResponse getCredentialIssuerWellKnownResponseDto(String issuerName, List credentialsSupportedResponses){ + CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse = new CredentialIssuerWellKnownResponse(); + credentialIssuerWellKnownResponse.setCredentialIssuer(issuerName); + credentialIssuerWellKnownResponse.setCredentialEndPoint("/credential_endpoint"); + credentialIssuerWellKnownResponse.setCredentialsSupported(credentialsSupportedResponses); + return credentialIssuerWellKnownResponse; + } @Before public void setUp() throws Exception { IssuersDTO issuers = new IssuersDTO(); issuers.setIssuers(List.of(getIssuerDTO("Issuer1", Collections.emptyList()), getIssuerDTO("Issuer2", Collections.emptyList()))); + CredentialIssuerWellKnownResponse credentialIssuerWellKnownResponse = getCredentialIssuerWellKnownResponseDto("Issuer1", + List.of(getCredentialSupportedResponse("CredentialSupported1"), getCredentialSupportedResponse("CredentialSupported2"))); + Mockito.when(utilities.getCredentialsSupportedConfigJsonValue()).thenReturn(new Gson().toJson(credentialIssuerWellKnownResponse)); Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(new Gson().toJson(issuers)); } @@ -86,16 +140,21 @@ public void shouldReturnIssuersWithIssuerConfigAsNull() throws ApiNotAccessibleE List issuers = new ArrayList<>(List.of(getIssuerDTO("Issuer1", issuerConfigRelatedFields), getIssuerDTO("Issuer2", issuerConfigRelatedFields))); expectedIssuers.setIssuers(issuers); - IssuersDTO allIssuers = issuersService.getAllIssuers(); + IssuersDTO expectedFilteredIssuers = new IssuersDTO(); + List filteredIssuersList = new ArrayList<>(List.of(getIssuerDTO("Issuer1", issuerConfigRelatedFields))); + expectedFilteredIssuers.setIssuers(filteredIssuersList); + IssuersDTO allIssuers = issuersService.getAllIssuers(null); + IssuersDTO filteredIssuers = issuersService.getAllIssuers("Issuer1"); assertEquals(expectedIssuers, allIssuers); + assertEquals(expectedFilteredIssuers, filteredIssuers); } @Test(expected = ApiNotAccessibleException.class) public void shouldThrowApiNotAccessibleExceptionWhenIssuersJsonStringIsNullForGettingAllIssuers() throws IOException, ApiNotAccessibleException { Mockito.when(utilities.getIssuersConfigJsonValue()).thenReturn(null); - issuersService.getAllIssuers(); + issuersService.getAllIssuers(null); } @Test @@ -131,4 +190,51 @@ public void shouldThrowApiNotAccessibleExceptionWhenIssuersJsonStringIsNullForGe issuersService.getIssuerConfig("Issuers1id"); } + + @Test(expected = ApiNotAccessibleException.class) + public void shouldThrowApiNotAccessibleExceptionWhenCredentialsSupportedJsonStringIsNullForGettingCredentialsSupportedList() throws Exception { + Mockito.when(utilities.getCredentialsSupportedConfigJsonValue()).thenReturn(null); + issuersService.getCredentialsSupported("Issuer1id", null); + Mockito.when(restApiClient.getApi(Mockito.any(URI.class), Mockito.any(Class.class))).thenReturn(null); + } + + @Test + public void shouldReturnIssuerCredentialSupportedResponseForTheIssuerIdIfExist() throws Exception { + IssuerSupportedCredentialsResponse expectedIssuerCredentialsSupported = new IssuerSupportedCredentialsResponse(); + List credentialsSupportedResponses =List.of(getCredentialSupportedResponse("CredentialSupported1"), + getCredentialSupportedResponse("CredentialSupported2")); + + String authorization_endpoint = getIssuerDTO("Issuer1", issuerConfigRelatedFields).getAuthorization_endpoint(); + expectedIssuerCredentialsSupported.setSupportedCredentials(credentialsSupportedResponses); + expectedIssuerCredentialsSupported.setAuthorizationEndPoint(authorization_endpoint); + + IssuerSupportedCredentialsResponse issuerSupportedCredentialsResponse = issuersService.getCredentialsSupported("Issuer1id", null); + assertEquals(issuerSupportedCredentialsResponse, expectedIssuerCredentialsSupported); + } + + @Test + public void shouldReturnNullIfTheIssuerIdNotExistsForCredentialSupportedTypes() throws ApiNotAccessibleException, IOException { + IssuerSupportedCredentialsResponse issuerSupportedCredentialsResponse = issuersService.getCredentialsSupported("Issuer3id", null); + assertNull(issuerSupportedCredentialsResponse.getSupportedCredentials()); + assertNull(issuerSupportedCredentialsResponse.getAuthorizationEndPoint()); + } + + @Test + public void shouldParseHtmlStringToDocument() { + String htmlContent = "

$message

"; + Map data = new HashMap<>(); + data.put("message", "PDF"); + // Create VelocityContext + VelocityContext velocityContext = new VelocityContext(); + // Create StringWriter to capture output + StringWriter writer = new StringWriter(); + // Merge template with data + velocityContext.put("message", data.get("message")); + Velocity.evaluate(velocityContext, writer, "Credential Template", htmlContent); + // Get merged HTML + String mergedHtml = writer.toString(); + // Assertion + assertTrue(mergedHtml.contains("PDF")); + } + }