diff --git a/CHANGELOG.md b/CHANGELOG.md index 9403c83..6d8389c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v1.0.15](https://github.com/in2workspace/in2-verifier-api/releases/tag/v1.0.15) +### Fixed +- Fix token serialization issue +- Add cors config for registered clients + ## [v1.0.14](https://github.com/in2workspace/in2-verifier-api/releases/tag/v1.0.14) ### Fixed - Rename the verifiableCredential claim of the access token to vc diff --git a/build.gradle b/build.gradle index 874a961..f52685b 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ plugins { } group = 'es.in2' -version = '1.0.14' +version = '1.0.15' java { toolchain { diff --git a/src/main/java/es/in2/verifier/VerifierApplication.java b/src/main/java/es/in2/verifier/VerifierApplication.java index 6bfc477..a11092d 100644 --- a/src/main/java/es/in2/verifier/VerifierApplication.java +++ b/src/main/java/es/in2/verifier/VerifierApplication.java @@ -10,10 +10,12 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableConfigurationProperties @ConfigurationPropertiesScan +@EnableScheduling public class VerifierApplication { private static final ObjectMapper OBJECT_MAPPER = diff --git a/src/main/java/es/in2/verifier/config/CacheStoreConfig.java b/src/main/java/es/in2/verifier/config/CacheStoreConfig.java index d5e4b75..16fca94 100644 --- a/src/main/java/es/in2/verifier/config/CacheStoreConfig.java +++ b/src/main/java/es/in2/verifier/config/CacheStoreConfig.java @@ -9,6 +9,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -18,6 +19,7 @@ public class CacheStoreConfig { private final SecurityProperties securityProperties; + @Bean public CacheStore cacheStoreForAuthorizationRequestJWT() { return new CacheStore<>( @@ -39,5 +41,10 @@ public CacheStore cacheStoreForAuthorizationCodeData() { public Set jtiCache() { return new HashSet<>(); } + + @Bean + public Set allowedClientsOrigins() { + return Collections.synchronizedSet(new HashSet<>()); + } } diff --git a/src/main/java/es/in2/verifier/config/ClientLoaderConfig.java b/src/main/java/es/in2/verifier/config/ClientLoaderConfig.java index 9561584..a3162bf 100644 --- a/src/main/java/es/in2/verifier/config/ClientLoaderConfig.java +++ b/src/main/java/es/in2/verifier/config/ClientLoaderConfig.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; @@ -17,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.UUID; @Configuration @@ -24,9 +26,15 @@ public class ClientLoaderConfig { private final TrustFrameworkService trustFrameworkService; + private final Set allowedClientsOrigins; @Bean - public RegisteredClientRepository registeredClientRepository() { + public RegisteredClientRepository getRegisteredClientRepository() { + return registeredClientRepository(); + } + + @Scheduled(cron = "0 */30 * * * *") + private RegisteredClientRepository registeredClientRepository() { List clients = loadClients(); // Cargar los clientes return new InMemoryRegisteredClientRepository(clients); // Pasar los clientes al repositorio } @@ -65,6 +73,11 @@ private List loadClients() { } registeredClientBuilder.clientSettings(clientSettingsBuilder.build()); registeredClients.add(registeredClientBuilder.build()); + + // Add the client origin to the allowed clients origins + if (clientData.url() != null && !clientData.url().isBlank()) { + allowedClientsOrigins.add(clientData.url()); + } } return registeredClients; } catch (Exception e) { diff --git a/src/main/java/es/in2/verifier/model/credentials/Contact.java b/src/main/java/es/in2/verifier/model/credentials/Contact.java deleted file mode 100644 index 858af60..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/Contact.java +++ /dev/null @@ -1,10 +0,0 @@ -package es.in2.verifier.model.credentials; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; - -@Builder -public record Contact( - @JsonProperty("email") String email, - @JsonProperty("phone") String mobilePhone -){} \ No newline at end of file diff --git a/src/main/java/es/in2/verifier/model/credentials/Issuer.java b/src/main/java/es/in2/verifier/model/credentials/Issuer.java index 81bef90..9523b05 100644 --- a/src/main/java/es/in2/verifier/model/credentials/Issuer.java +++ b/src/main/java/es/in2/verifier/model/credentials/Issuer.java @@ -1,9 +1,13 @@ package es.in2.verifier.model.credentials; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; @Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public record Issuer( @JsonProperty("id")String id -) { } \ No newline at end of file +) { } diff --git a/src/main/java/es/in2/verifier/model/credentials/LifeSpan.java b/src/main/java/es/in2/verifier/model/credentials/LifeSpan.java deleted file mode 100644 index 8b54c16..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/LifeSpan.java +++ /dev/null @@ -1,10 +0,0 @@ -package es.in2.verifier.model.credentials; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; - -@Builder -public record LifeSpan( - @JsonProperty("end_date_time") String endDateTime, - @JsonProperty("start_date_time") String startDateTime -) {} \ No newline at end of file diff --git a/src/main/java/es/in2/verifier/model/credentials/employee/CredentialSubjectLCEmployee.java b/src/main/java/es/in2/verifier/model/credentials/employee/CredentialSubjectLCEmployee.java deleted file mode 100644 index 4debb0d..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/employee/CredentialSubjectLCEmployee.java +++ /dev/null @@ -1,8 +0,0 @@ -package es.in2.verifier.model.credentials.employee; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; - -@Builder -public record CredentialSubjectLCEmployee(@JsonProperty("mandate") MandateLCEmployee mandate) { -} \ No newline at end of file diff --git a/src/main/java/es/in2/verifier/model/credentials/employee/LEARCredentialEmployee.java b/src/main/java/es/in2/verifier/model/credentials/employee/LEARCredentialEmployee.java deleted file mode 100644 index d4b4926..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/employee/LEARCredentialEmployee.java +++ /dev/null @@ -1,21 +0,0 @@ -package es.in2.verifier.model.credentials.employee; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; - -import java.util.List; - -@Builder -@JsonIgnoreProperties(ignoreUnknown = true) -public record LEARCredentialEmployee( - @JsonProperty("@context") List context, - @JsonProperty("id") String id, - @JsonProperty("type") List type, - @JsonProperty("credentialSubject") CredentialSubjectLCEmployee credentialSubject, - @JsonProperty("expirationDate") String expirationDate, - @JsonProperty("issuanceDate") String issuanceDate, - @JsonProperty("issuer") String issuer, - @JsonProperty("validFrom") String validFrom, - @JsonProperty("validUntil") String validUntil -) {} \ No newline at end of file diff --git a/src/main/java/es/in2/verifier/model/credentials/employee/MandateLCEmployee.java b/src/main/java/es/in2/verifier/model/credentials/employee/MandateLCEmployee.java deleted file mode 100644 index daf1932..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/employee/MandateLCEmployee.java +++ /dev/null @@ -1,19 +0,0 @@ -package es.in2.verifier.model.credentials.employee; - -import com.fasterxml.jackson.annotation.JsonProperty; -import es.in2.verifier.model.credentials.LifeSpan; -import es.in2.verifier.model.credentials.Mandator; -import es.in2.verifier.model.credentials.Signer; -import lombok.Builder; - -import java.util.List; - -@Builder -public record MandateLCEmployee( - @JsonProperty("id") String id, - @JsonProperty("life_span") LifeSpan lifeSpan, - @JsonProperty("mandatee") MandateeLCEmployee mandatee, - @JsonProperty("mandator") Mandator mandator, - @JsonProperty("power") List power, - @JsonProperty("signer") Signer signer -) {} \ No newline at end of file diff --git a/src/main/java/es/in2/verifier/model/credentials/employee/MandateeLCEmployee.java b/src/main/java/es/in2/verifier/model/credentials/employee/MandateeLCEmployee.java deleted file mode 100644 index fe6a7bc..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/employee/MandateeLCEmployee.java +++ /dev/null @@ -1,13 +0,0 @@ -package es.in2.verifier.model.credentials.employee; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; - -@Builder -public record MandateeLCEmployee( - @JsonProperty("id") String id, - @JsonProperty("email") String email, - @JsonProperty("first_name") String firstName, - @JsonProperty("last_name") String lastName, - @JsonProperty("mobile_phone") String mobilePhone -) {} \ No newline at end of file diff --git a/src/main/java/es/in2/verifier/model/credentials/lear/CredentialSubject.java b/src/main/java/es/in2/verifier/model/credentials/lear/CredentialSubject.java new file mode 100644 index 0000000..fa71612 --- /dev/null +++ b/src/main/java/es/in2/verifier/model/credentials/lear/CredentialSubject.java @@ -0,0 +1,13 @@ +package es.in2.verifier.model.credentials.lear; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record CredentialSubject( + @JsonProperty("mandate") Mandate mandate +) {} diff --git a/src/main/java/es/in2/verifier/model/credentials/lear/LEARCredential.java b/src/main/java/es/in2/verifier/model/credentials/lear/LEARCredential.java new file mode 100644 index 0000000..5473339 --- /dev/null +++ b/src/main/java/es/in2/verifier/model/credentials/lear/LEARCredential.java @@ -0,0 +1,12 @@ +package es.in2.verifier.model.credentials.lear; + +import java.util.List; + +public interface LEARCredential { + List context(); + String id(); + List type(); + String issuerId(); // Adjusted to be common + String mandateeId(); + String mandatorOrganizationIdentifier(); +} diff --git a/src/main/java/es/in2/verifier/model/credentials/lear/LifeSpan.java b/src/main/java/es/in2/verifier/model/credentials/lear/LifeSpan.java new file mode 100644 index 0000000..2c25a13 --- /dev/null +++ b/src/main/java/es/in2/verifier/model/credentials/lear/LifeSpan.java @@ -0,0 +1,15 @@ +package es.in2.verifier.model.credentials.lear; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record LifeSpan( + @JsonProperty("end_date_time") String endDateTime, + @JsonProperty("start_date_time") String startDateTime +) {} + diff --git a/src/main/java/es/in2/verifier/model/credentials/lear/Mandate.java b/src/main/java/es/in2/verifier/model/credentials/lear/Mandate.java new file mode 100644 index 0000000..64889ec --- /dev/null +++ b/src/main/java/es/in2/verifier/model/credentials/lear/Mandate.java @@ -0,0 +1,20 @@ +package es.in2.verifier.model.credentials.lear; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +import java.util.List; + +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record Mandate( + @JsonProperty("id") String id, + @JsonProperty("life_span") LifeSpan lifeSpan, + @JsonProperty("mandatee") Mandatee mandatee, + @JsonProperty("mandator") Mandator mandator, + @JsonProperty("power") List power, + @JsonProperty("signer") Signer signer +) {} diff --git a/src/main/java/es/in2/verifier/model/credentials/lear/Mandatee.java b/src/main/java/es/in2/verifier/model/credentials/lear/Mandatee.java new file mode 100644 index 0000000..e5c32b0 --- /dev/null +++ b/src/main/java/es/in2/verifier/model/credentials/lear/Mandatee.java @@ -0,0 +1,25 @@ +package es.in2.verifier.model.credentials.lear; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import es.in2.verifier.model.credentials.lear.machine.Contact; +import lombok.Builder; + +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record Mandatee( + @JsonProperty("id") String id, + @JsonProperty("email") String email, + @JsonProperty("first_name") String firstName, + @JsonProperty("last_name") String lastName, + @JsonProperty("mobile_phone") String mobilePhone, + @JsonProperty("serviceName") String serviceName, + @JsonProperty("serviceType") String serviceType, + @JsonProperty("version") String version, + @JsonProperty("domain") String domain, + @JsonProperty("ipAddress") String ipAddress, + @JsonProperty("description") String description, + @JsonProperty("contact") Contact contact +) {} diff --git a/src/main/java/es/in2/verifier/model/credentials/Mandator.java b/src/main/java/es/in2/verifier/model/credentials/lear/Mandator.java similarity index 64% rename from src/main/java/es/in2/verifier/model/credentials/Mandator.java rename to src/main/java/es/in2/verifier/model/credentials/lear/Mandator.java index 458b6c2..6132168 100644 --- a/src/main/java/es/in2/verifier/model/credentials/Mandator.java +++ b/src/main/java/es/in2/verifier/model/credentials/lear/Mandator.java @@ -1,9 +1,13 @@ -package es.in2.verifier.model.credentials; +package es.in2.verifier.model.credentials.lear; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; @Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public record Mandator( @JsonProperty("commonName") String commonName, @JsonProperty("country") String country, @@ -11,4 +15,5 @@ public record Mandator( @JsonProperty("organization") String organization, @JsonProperty("organizationIdentifier") String organizationIdentifier, @JsonProperty("serialNumber") String serialNumber -) {} \ No newline at end of file +) {} + diff --git a/src/main/java/es/in2/verifier/model/credentials/employee/PowerLCEmployee.java b/src/main/java/es/in2/verifier/model/credentials/lear/Power.java similarity index 55% rename from src/main/java/es/in2/verifier/model/credentials/employee/PowerLCEmployee.java rename to src/main/java/es/in2/verifier/model/credentials/lear/Power.java index f04c5ff..d9b7763 100644 --- a/src/main/java/es/in2/verifier/model/credentials/employee/PowerLCEmployee.java +++ b/src/main/java/es/in2/verifier/model/credentials/lear/Power.java @@ -1,14 +1,18 @@ -package es.in2.verifier.model.credentials.employee; - +package es.in2.verifier.model.credentials.lear; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; @Builder -public record PowerLCEmployee( +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record Power( @JsonProperty("id") String id, @JsonProperty("tmf_action") Object tmfAction, @JsonProperty("tmf_domain") String tmfDomain, @JsonProperty("tmf_function") String tmfFunction, @JsonProperty("tmf_type") String tmfType -) {} \ No newline at end of file +) {} + diff --git a/src/main/java/es/in2/verifier/model/credentials/Signer.java b/src/main/java/es/in2/verifier/model/credentials/lear/Signer.java similarity index 64% rename from src/main/java/es/in2/verifier/model/credentials/Signer.java rename to src/main/java/es/in2/verifier/model/credentials/lear/Signer.java index 6cfa8a6..0d4f89d 100644 --- a/src/main/java/es/in2/verifier/model/credentials/Signer.java +++ b/src/main/java/es/in2/verifier/model/credentials/lear/Signer.java @@ -1,9 +1,13 @@ -package es.in2.verifier.model.credentials; +package es.in2.verifier.model.credentials.lear; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; @Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) public record Signer( @JsonProperty("commonName") String commonName, @JsonProperty("country") String country, @@ -11,4 +15,4 @@ public record Signer( @JsonProperty("organization") String organization, @JsonProperty("organizationIdentifier") String organizationIdentifier, @JsonProperty("serialNumber") String serialNumber -) {} \ No newline at end of file +) {} diff --git a/src/main/java/es/in2/verifier/model/credentials/lear/employee/LEARCredentialEmployee.java b/src/main/java/es/in2/verifier/model/credentials/lear/employee/LEARCredentialEmployee.java new file mode 100644 index 0000000..326e212 --- /dev/null +++ b/src/main/java/es/in2/verifier/model/credentials/lear/employee/LEARCredentialEmployee.java @@ -0,0 +1,63 @@ +package es.in2.verifier.model.credentials.lear.employee; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import es.in2.verifier.model.credentials.lear.LEARCredential; +import es.in2.verifier.model.credentials.lear.CredentialSubject; +import lombok.Builder; + +import java.util.List; + +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record LEARCredentialEmployee( + @JsonProperty("@context") + List context, + @JsonProperty("id") + String id, + @JsonProperty("type") + List type, + @JsonProperty("issuer") + String issuer, + @JsonProperty("credentialSubject") + CredentialSubject credentialSubject, + @JsonProperty("validFrom") + String validFrom, + @JsonProperty("validUntil") + String validUntil, + @JsonProperty("expirationDate") + String expirationDate, + @JsonProperty("issuanceDate") + String issuanceDate +) implements LEARCredential { + + @Override + public String issuerId() { + return issuer; + } + + @Override + public String mandateeId() { + return credentialSubject.mandate().mandatee().id(); + } + + @Override + public String mandatorOrganizationIdentifier() { + return credentialSubject.mandate().mandator().organizationIdentifier(); + } + + public String mandateeFirstName(){ + return credentialSubject.mandate().mandatee().firstName(); + } + + public String mandateeLastName(){ + return credentialSubject.mandate().mandatee().lastName(); + } + + public String mandateeEmail(){ + return credentialSubject.mandate().mandatee().email(); + } + +} diff --git a/src/main/java/es/in2/verifier/model/credentials/lear/machine/Contact.java b/src/main/java/es/in2/verifier/model/credentials/lear/machine/Contact.java new file mode 100644 index 0000000..4466fd5 --- /dev/null +++ b/src/main/java/es/in2/verifier/model/credentials/lear/machine/Contact.java @@ -0,0 +1,15 @@ +package es.in2.verifier.model.credentials.lear.machine; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record Contact( + @JsonProperty("email") String email, + @JsonProperty("phone") String mobilePhone +) {} + diff --git a/src/main/java/es/in2/verifier/model/credentials/lear/machine/LEARCredentialMachine.java b/src/main/java/es/in2/verifier/model/credentials/lear/machine/LEARCredentialMachine.java new file mode 100644 index 0000000..5886f69 --- /dev/null +++ b/src/main/java/es/in2/verifier/model/credentials/lear/machine/LEARCredentialMachine.java @@ -0,0 +1,52 @@ +package es.in2.verifier.model.credentials.lear.machine; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import es.in2.verifier.model.credentials.Issuer; +import es.in2.verifier.model.credentials.lear.LEARCredential; +import es.in2.verifier.model.credentials.lear.CredentialSubject; +import lombok.Builder; + +import java.util.List; + +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record LEARCredentialMachine( + @JsonProperty("@context") + List context, + @JsonProperty("id") + String id, + @JsonProperty("type") + List type, + @JsonProperty("issuer") + Issuer issuer, + @JsonProperty("credentialSubject") + CredentialSubject credentialSubject, + @JsonProperty("validFrom") + String validFrom, + @JsonProperty("validUntil") + String validUntil, + @JsonProperty("expirationDate") + String expirationDate, + @JsonProperty("issuanceDate") + String issuanceDate +) implements LEARCredential { + + @Override + public String mandateeId() { + return credentialSubject.mandate().mandatee().id(); + } + + @Override + public String issuerId() { + return issuer.id(); + } + + @Override + public String mandatorOrganizationIdentifier() { + return credentialSubject.mandate().mandator().organizationIdentifier(); + } + +} diff --git a/src/main/java/es/in2/verifier/model/credentials/machine/CredentialSubjectLCMachine.java b/src/main/java/es/in2/verifier/model/credentials/machine/CredentialSubjectLCMachine.java deleted file mode 100644 index 75a245b..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/machine/CredentialSubjectLCMachine.java +++ /dev/null @@ -1,9 +0,0 @@ -package es.in2.verifier.model.credentials.machine; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; - -@Builder -public record CredentialSubjectLCMachine( - @JsonProperty("mandate") MandateLCMachine mandate -) {} diff --git a/src/main/java/es/in2/verifier/model/credentials/machine/LEARCredentialMachine.java b/src/main/java/es/in2/verifier/model/credentials/machine/LEARCredentialMachine.java deleted file mode 100644 index 35664c5..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/machine/LEARCredentialMachine.java +++ /dev/null @@ -1,23 +0,0 @@ -package es.in2.verifier.model.credentials.machine; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import es.in2.verifier.model.credentials.Issuer; -import lombok.Builder; - -import java.util.List; - -@Builder -@JsonIgnoreProperties(ignoreUnknown = true) - -public record LEARCredentialMachine( - @JsonProperty("@context") List context, - @JsonProperty("id") String id, - @JsonProperty("type") List type, - @JsonProperty("issuer") Issuer issuer, - @JsonProperty("issuanceDate") String issuanceDate, - @JsonProperty("validFrom") String validFrom, - @JsonProperty("validUntil") String validUntil, - @JsonProperty("expirationDate") String expirationDate, - @JsonProperty("credentialSubject") CredentialSubjectLCMachine credentialSubject -) {} \ No newline at end of file diff --git a/src/main/java/es/in2/verifier/model/credentials/machine/MandateLCMachine.java b/src/main/java/es/in2/verifier/model/credentials/machine/MandateLCMachine.java deleted file mode 100644 index 4347a9a..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/machine/MandateLCMachine.java +++ /dev/null @@ -1,18 +0,0 @@ -package es.in2.verifier.model.credentials.machine; - -import com.fasterxml.jackson.annotation.JsonProperty; -import es.in2.verifier.model.credentials.LifeSpan; -import es.in2.verifier.model.credentials.Mandator; -import es.in2.verifier.model.credentials.Signer; -import lombok.Builder; - -import java.util.List; -@Builder -public record MandateLCMachine( - @JsonProperty("id") String id, - @JsonProperty("life_span") LifeSpan lifeSpan, - @JsonProperty("mandatee") MandateeLCMachine mandatee, - @JsonProperty("mandator") Mandator mandator, - @JsonProperty("power") List power, - @JsonProperty("signer") Signer signer -) {} diff --git a/src/main/java/es/in2/verifier/model/credentials/machine/MandateeLCMachine.java b/src/main/java/es/in2/verifier/model/credentials/machine/MandateeLCMachine.java deleted file mode 100644 index b5f08a0..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/machine/MandateeLCMachine.java +++ /dev/null @@ -1,17 +0,0 @@ -package es.in2.verifier.model.credentials.machine; - -import com.fasterxml.jackson.annotation.JsonProperty; -import es.in2.verifier.model.credentials.Contact; -import lombok.Builder; - -@Builder -public record MandateeLCMachine( - @JsonProperty("id") String id, - @JsonProperty("serviceName") String serviceName, - @JsonProperty("serviceType") String serviceType, - @JsonProperty("version") String version, - @JsonProperty("domain") String domain, - @JsonProperty("ipAddress") String ipAddress, - @JsonProperty("description") String description, - @JsonProperty("contact") Contact contact -) {} diff --git a/src/main/java/es/in2/verifier/model/credentials/machine/PowerLCMachine.java b/src/main/java/es/in2/verifier/model/credentials/machine/PowerLCMachine.java deleted file mode 100644 index d404ce1..0000000 --- a/src/main/java/es/in2/verifier/model/credentials/machine/PowerLCMachine.java +++ /dev/null @@ -1,12 +0,0 @@ -package es.in2.verifier.model.credentials.machine; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; - -@Builder -public record PowerLCMachine( - @JsonProperty("id") String id, - @JsonProperty("domain") String domain, - @JsonProperty("function") String function, - @JsonProperty("action") String action -) {} diff --git a/src/main/java/es/in2/verifier/security/AuthorizationServerConfig.java b/src/main/java/es/in2/verifier/security/AuthorizationServerConfig.java index bb623b4..83d6d8a 100644 --- a/src/main/java/es/in2/verifier/security/AuthorizationServerConfig.java +++ b/src/main/java/es/in2/verifier/security/AuthorizationServerConfig.java @@ -54,6 +54,7 @@ public class AuthorizationServerConfig { private final RegisteredClientRepository registeredClientRepository; private final CacheStore cacheStoreForAuthorizationCodeData; private final ObjectMapper objectMapper; + private final RegisteredClientsCorsConfig registeredClientsCorsConfig; @Bean @Order(Ordered.HIGHEST_PRECEDENCE) @@ -61,6 +62,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http + .cors(cors -> cors.configurationSource(registeredClientsCorsConfig.registeredClientsCorsConfigurationSource())) .getConfigurer(OAuth2AuthorizationServerConfigurer.class) .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint diff --git a/src/main/java/es/in2/verifier/security/CorsConfig.java b/src/main/java/es/in2/verifier/security/CorsConfig.java deleted file mode 100644 index d89cc48..0000000 --- a/src/main/java/es/in2/verifier/security/CorsConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package es.in2.verifier.security; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import java.util.List; - -@Configuration -public class CorsConfig { - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - // we need to allow all origins because the service is public and must be accessible from any domain. - configuration.setAllowedOriginPatterns(List.of("*")); - configuration.setAllowedMethods(List.of("GET", "POST")); - configuration.setAllowedHeaders(List.of("Content-Type")); - - // We do not allow the sending of credentials to improve security - configuration.setAllowCredentials(false); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/health", configuration); - source.registerCorsConfiguration("/oid4vp/auth-request/**", configuration); - source.registerCorsConfiguration("/oid4vp/auth-response", configuration); - - return source; - } - -} - diff --git a/src/main/java/es/in2/verifier/security/PublicCorsConfig.java b/src/main/java/es/in2/verifier/security/PublicCorsConfig.java new file mode 100644 index 0000000..e68f05c --- /dev/null +++ b/src/main/java/es/in2/verifier/security/PublicCorsConfig.java @@ -0,0 +1,31 @@ +package es.in2.verifier.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class PublicCorsConfig { + + @Bean + public CorsConfigurationSource publicCorsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + // Configure public endpoints + CorsConfiguration publicConfig = new CorsConfiguration(); + publicConfig.setAllowedOriginPatterns(List.of("*")); + publicConfig.setAllowedMethods(List.of("GET", "POST")); + publicConfig.setAllowedHeaders(List.of("Content-Type")); + publicConfig.setAllowCredentials(false); + source.registerCorsConfiguration("/health", publicConfig); + source.registerCorsConfiguration("/oid4vp/auth-request/**", publicConfig); + source.registerCorsConfiguration("/oid4vp/auth-response", publicConfig); + return source; + } + +} diff --git a/src/main/java/es/in2/verifier/security/RegisteredClientsCorsConfig.java b/src/main/java/es/in2/verifier/security/RegisteredClientsCorsConfig.java new file mode 100644 index 0000000..3dadd16 --- /dev/null +++ b/src/main/java/es/in2/verifier/security/RegisteredClientsCorsConfig.java @@ -0,0 +1,46 @@ +package es.in2.verifier.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@Configuration +@RequiredArgsConstructor +public class RegisteredClientsCorsConfig { + + private final Set allowedClientsOrigins; + + @Bean + public CorsConfigurationSource registeredClientsCorsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + + // Configure endpoints for clients that used the OIDC protocol + CorsConfiguration authConfig = new CorsConfiguration(); + authConfig.setAllowedOrigins(new ArrayList<>(allowedClientsOrigins)); + authConfig.setAllowedMethods(List.of("GET", "POST")); + authConfig.setAllowedHeaders(List.of("Content-Type", "Authorization")); + authConfig.setAllowCredentials(false); + + // Register the configuration for each endpoint + source.registerCorsConfiguration("/oidc/authorize", authConfig); + source.registerCorsConfiguration("/oidc/device_authorization", authConfig); + source.registerCorsConfiguration("/oidc/device_verification", authConfig); + source.registerCorsConfiguration("/oidc/token", authConfig); + source.registerCorsConfiguration("/oidc/introspect", authConfig); + source.registerCorsConfiguration("/oidc/revoke", authConfig); + source.registerCorsConfiguration("/oidc/jwks", authConfig); + source.registerCorsConfiguration("/oidc/logout", authConfig); + source.registerCorsConfiguration("/oidc/userinfo", authConfig); + source.registerCorsConfiguration("/oidc/register", authConfig); + source.registerCorsConfiguration("/.well-known/openid-configuration", authConfig); + + return source; + } +} \ No newline at end of file diff --git a/src/main/java/es/in2/verifier/security/SecurityConfig.java b/src/main/java/es/in2/verifier/security/SecurityConfig.java index 99d8a51..76b13fb 100644 --- a/src/main/java/es/in2/verifier/security/SecurityConfig.java +++ b/src/main/java/es/in2/verifier/security/SecurityConfig.java @@ -18,12 +18,12 @@ @RequiredArgsConstructor public class SecurityConfig { - private final CorsConfig corsConfig; + private final PublicCorsConfig publicCorsConfig; @Bean public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http - .cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource())) + .cors(cors -> cors.configurationSource(publicCorsConfig.publicCorsConfigurationSource())) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/health").permitAll() .requestMatchers("/oid4vp/auth-request/*").permitAll() diff --git a/src/main/java/es/in2/verifier/security/filters/CustomAuthenticationProvider.java b/src/main/java/es/in2/verifier/security/filters/CustomAuthenticationProvider.java index 1226643..e51edf2 100644 --- a/src/main/java/es/in2/verifier/security/filters/CustomAuthenticationProvider.java +++ b/src/main/java/es/in2/verifier/security/filters/CustomAuthenticationProvider.java @@ -1,14 +1,17 @@ package es.in2.verifier.security.filters; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jwt.JWTClaimsSet; import es.in2.verifier.config.properties.SecurityProperties; import es.in2.verifier.exception.InvalidCredentialTypeException; import es.in2.verifier.exception.JsonConversionException; -import es.in2.verifier.model.credentials.employee.LEARCredentialEmployee; -import es.in2.verifier.model.credentials.machine.LEARCredentialMachine; +import es.in2.verifier.model.credentials.lear.LEARCredential; +import es.in2.verifier.model.credentials.lear.employee.LEARCredentialEmployee; +import es.in2.verifier.model.credentials.lear.machine.LEARCredentialMachine; +import es.in2.verifier.model.enums.LEARCredentialType; import es.in2.verifier.service.JWTService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -64,11 +67,11 @@ private Authentication handleGrant(OAuth2AuthorizationGrantAuthenticationToken a ); log.debug("CustomAuthenticationProvider -- handleGrant -- Issue time: {}, Expiration time: {}", issueTime, expirationTime); - Object credential = getVerifiableCredential(authentication); - String subject = getCredentialSubjectFromVerifiableCredential(credential); + LEARCredential credential = getVerifiableCredential(authentication); + String subject = credential.mandateeId(); log.debug("CustomAuthenticationProvider -- handleGrant -- Credential subject obtained: {}", subject); - String audience = getAudience(authentication,credential); + String audience = getAudience(authentication, credential); log.debug("CustomAuthenticationProvider -- handleGrant -- Audience for credential: {}", audience); String jwtToken = generateAccessTokenWithVc(credential, issueTime, expirationTime, subject, audience); @@ -87,7 +90,7 @@ private Authentication handleGrant(OAuth2AuthorizationGrantAuthenticationToken a additionalParameters.put("id_token", idToken); log.info("Authorization grant successfully processed"); - return new OAuth2AccessTokenAuthenticationToken(registeredClient, authentication, oAuth2AccessToken, null,additionalParameters); + return new OAuth2AccessTokenAuthenticationToken(registeredClient, authentication, oAuth2AccessToken, null, additionalParameters); } private String getClientId(OAuth2AuthorizationGrantAuthenticationToken authentication) { @@ -110,42 +113,25 @@ private RegisteredClient getRegisteredClient(String clientId) { } return registeredClient; } - private Object getVerifiableCredential(OAuth2AuthorizationGrantAuthenticationToken authentication) { - // Obtener el JsonNode de los parámetros adicionales + + private LEARCredential getVerifiableCredential(OAuth2AuthorizationGrantAuthenticationToken authentication) { Map additionalParameters = authentication.getAdditionalParameters(); if (!additionalParameters.containsKey("vc")) { - log.error("CustomAuthenticationProvider -- getVerifiableCredential -- Parameter 'vc' not found in request"); throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } JsonNode verifiableCredential = objectMapper.convertValue(additionalParameters.get("vc"), JsonNode.class); - log.debug("CustomAuthenticationProvider -- getVerifiableCredential -- Verifiable credential obtained: {}", verifiableCredential); - // Diferenciar el tipo de credencial basado en la clase concreta de autenticación if (authentication instanceof OAuth2AuthorizationCodeAuthenticationToken) { - // Retorna la credencial específica para el tipo `LEARCredentialEmployee` return objectMapper.convertValue(verifiableCredential, LEARCredentialEmployee.class); } else if (authentication instanceof OAuth2ClientCredentialsAuthenticationToken) { - // Retorna la credencial específica para el tipo `LEARCredentialMachine` return objectMapper.convertValue(verifiableCredential, LEARCredentialMachine.class); - } - log.error("CustomAuthenticationProvider -- getVerifiableCredential -- Unsupported authentication type for credential"); - throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); - } - private String getCredentialSubjectFromVerifiableCredential(Object verifiableCredential) { - log.debug("CustomAuthenticationProvider -- getCredentialSubjectFromVerifiableCredential -- Obtaining subject from verifiable credential"); - if (verifiableCredential instanceof LEARCredentialEmployee learCredentialEmployee) { - // Extrae y retorna el credentialSubject específico para `LEARCredentialEmployee` - return learCredentialEmployee.credentialSubject().mandate().mandatee().id(); - } else if (verifiableCredential instanceof LEARCredentialMachine learCredentialMachine) { - // Extrae y retorna el credentialSubject específico para `LEARCredentialMachine` - return learCredentialMachine.credentialSubject().mandate().mandatee().id(); } - log.error("CustomAuthenticationProvider -- getCredentialSubjectFromVerifiableCredential -- Unsupported credential LEARCredentialType: {}", verifiableCredential.getClass().getName()); + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } - private String getAudience(OAuth2AuthorizationGrantAuthenticationToken authentication, Object credential) { + private String getAudience(OAuth2AuthorizationGrantAuthenticationToken authentication, LEARCredential credential) { // Extraer el audience en función del tipo de credencial if (credential instanceof LEARCredentialMachine) { return securityProperties.authorizationServer(); @@ -162,22 +148,40 @@ private String getAudience(OAuth2AuthorizationGrantAuthenticationToken authentic throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); } - private String generateAccessTokenWithVc(Object verifiableCredential, Instant issueTime, Instant expirationTime, String subject, String audience) { + private String generateAccessTokenWithVc(LEARCredential learCredential, Instant issueTime, Instant expirationTime, String subject, String audience) { log.info("Generating access token with verifiableCredential"); - JWTClaimsSet payload = new JWTClaimsSet.Builder() + + // Construir el builder del JWTClaimsSet + JWTClaimsSet.Builder claimsBuilder = new JWTClaimsSet.Builder() .issuer(securityProperties.authorizationServer()) - .audience(audience) // Utiliza el valor de "audience" calculado + .audience(audience) .subject(subject) .jwtID(UUID.randomUUID().toString()) .issueTime(Date.from(issueTime)) .expirationTime(Date.from(expirationTime)) - .claim(OAuth2ParameterNames.SCOPE, getScope(verifiableCredential)) - .claim("vc", verifiableCredential) - .build(); + .claim(OAuth2ParameterNames.SCOPE, getScope(learCredential)); + + List credentialTypes = learCredential.type(); + + if (credentialTypes.contains(LEARCredentialType.LEAR_CREDENTIAL_EMPLOYEE.getValue())) { + LEARCredentialEmployee credential = (LEARCredentialEmployee) learCredential; + Map credentialData = objectMapper.convertValue(credential, new TypeReference<>() {}); + claimsBuilder.claim("vc", credentialData); + } else if (credentialTypes.contains(LEARCredentialType.LEAR_CREDENTIAL_MACHINE.getValue())) { + LEARCredentialMachine credential = (LEARCredentialMachine) learCredential; + Map credentialData = objectMapper.convertValue(credential, new TypeReference<>() {}); + claimsBuilder.claim("vc", credentialData); + } else { + throw new InvalidCredentialTypeException("Unsupported credential type: " + credentialTypes); + } + + JWTClaimsSet payload = claimsBuilder.build(); + return jwtService.generateJWT(payload.toString()); } - private String generateIdToken(Object verifiableCredential, String subject, String audience, Map additionalParameters) { + + private String generateIdToken(LEARCredential learCredential, String subject, String audience, Map additionalParameters) { Instant issueTime = Instant.now(); Instant expirationTime = issueTime.plus( Long.parseLong(securityProperties.token().idToken().expiration()), @@ -187,7 +191,7 @@ private String generateIdToken(Object verifiableCredential, String subject, Stri // Convert the VerifiableCredential to a JSON string String verifiableCredentialJson; try { - verifiableCredentialJson = objectMapper.writeValueAsString(verifiableCredential); + verifiableCredentialJson = objectMapper.writeValueAsString(learCredential); } catch (JsonProcessingException e) { throw new JsonConversionException("Error converting Verifiable Credential to JSON: " + e.getMessage()); } @@ -209,7 +213,7 @@ private String generateIdToken(Object verifiableCredential, String subject, Stri Map additionalClaims; if (additionalParameters.containsKey(OAuth2ParameterNames.SCOPE)) { - additionalClaims = extractClaimsFromVerifiableCredential(verifiableCredential, additionalParameters); + additionalClaims = extractClaimsFromVerifiableCredential(learCredential, additionalParameters); additionalClaims.forEach(idTokenClaimsBuilder::claim); } @@ -219,22 +223,22 @@ private String generateIdToken(Object verifiableCredential, String subject, Stri return jwtService.generateJWT(idTokenClaims.toString()); } - private Map extractClaimsFromVerifiableCredential(Object verifiableCredential, Map additionalParameters) { + private Map extractClaimsFromVerifiableCredential(LEARCredential learCredential, Map additionalParameters) { Set requestedScopes = new HashSet<>(Arrays.asList(additionalParameters.get(OAuth2ParameterNames.SCOPE).toString().split(" "))); Map claims = new HashMap<>(); - if (verifiableCredential instanceof LEARCredentialEmployee learCredentialEmployee) { + if (learCredential instanceof LEARCredentialEmployee learCredentialEmployee) { // Check if "profile" scope is requested and add profile-related claims if (requestedScopes.contains("profile")) { - String name = learCredentialEmployee.credentialSubject().mandate().mandatee().firstName() + " " + learCredentialEmployee.credentialSubject().mandate().mandatee().lastName(); + String name = learCredentialEmployee.mandateeFirstName() + " " + learCredentialEmployee.mandateeLastName(); claims.put("name", name); - claims.put("given_name", learCredentialEmployee.credentialSubject().mandate().mandatee().firstName()); - claims.put("family_name", learCredentialEmployee.credentialSubject().mandate().mandatee().lastName()); + claims.put("given_name", learCredentialEmployee.mandateeFirstName()); + claims.put("family_name", learCredentialEmployee.mandateeLastName()); } // Check if "email" scope is requested and add email-related claims if (requestedScopes.contains("email")) { - claims.put("email", learCredentialEmployee.credentialSubject().mandate().mandatee().email()); + claims.put("email", learCredentialEmployee.mandateeEmail()); claims.put("email_verified", true); } } @@ -242,14 +246,13 @@ private Map extractClaimsFromVerifiableCredential(Object verifia } - private String getScope(Object verifiableCredential){ - if (verifiableCredential instanceof LEARCredentialEmployee) { + private String getScope(LEARCredential learCredential) { + if (learCredential instanceof LEARCredentialEmployee) { return "openid learcredential"; - } else if (verifiableCredential instanceof LEARCredentialMachine) { + } else if (learCredential instanceof LEARCredentialMachine) { return "machine learcredential"; } else { - log.error("CustomAuthenticationProvider -- getScope -- Unsupported credential type: {}", verifiableCredential.getClass().getName()); - throw new InvalidCredentialTypeException("Credential Type not supported: " + verifiableCredential.getClass().getName()); + throw new InvalidCredentialTypeException("Credential Type not supported: " + learCredential.getClass().getName()); } } diff --git a/src/main/java/es/in2/verifier/security/filters/CustomTokenRequestConverter.java b/src/main/java/es/in2/verifier/security/filters/CustomTokenRequestConverter.java index eb90eb5..427687c 100644 --- a/src/main/java/es/in2/verifier/security/filters/CustomTokenRequestConverter.java +++ b/src/main/java/es/in2/verifier/security/filters/CustomTokenRequestConverter.java @@ -9,7 +9,7 @@ import es.in2.verifier.exception.InvalidVPtokenException; import es.in2.verifier.exception.UnsupportedGrantTypeException; import es.in2.verifier.model.AuthorizationCodeData; -import es.in2.verifier.model.credentials.machine.LEARCredentialMachine; +import es.in2.verifier.model.credentials.lear.machine.LEARCredentialMachine; import es.in2.verifier.model.enums.LEARCredentialType; import es.in2.verifier.service.ClientAssertionValidationService; import es.in2.verifier.service.JWTService; @@ -42,22 +42,18 @@ public class CustomTokenRequestConverter implements AuthenticationConverter { private final OAuth2AuthorizationService oAuth2AuthorizationService; private final ObjectMapper objectMapper; - - @Override public Authentication convert(HttpServletRequest request) { log.info("CustomTokenRequestConverter --> convert -- INIT"); MultiValueMap parameters = getParameters(request); // grant_type (REQUIRED) String grantType = parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE); - assert grantType != null; return switch (grantType) { case "authorization_code" -> handleH2MGrant(parameters); case "client_credentials" -> handleM2MGrant(parameters); default -> throw new UnsupportedGrantTypeException("Unsupported grant_type: " + grantType); }; - } private Authentication handleH2MGrant(MultiValueMap parameters) { @@ -65,46 +61,36 @@ private Authentication handleH2MGrant(MultiValueMap parameters) String code = parameters.getFirst(OAuth2ParameterNames.CODE); String state = parameters.getFirst(OAuth2ParameterNames.STATE); String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); - AuthorizationCodeData authorizationCodeData = cacheStoreForAuthorizationCodeData.get(code); // Remove the state from cache after retrieving the Object cacheStoreForAuthorizationCodeData.delete(code); - // Remove the authorization from the initial request oAuth2AuthorizationService.remove(authorizationCodeData.oAuth2Authorization()); - // Check state only if it is not null and not blank if (state != null && !state.isBlank() && (!authorizationCodeData.state().equals(state))) { log.error("CustomTokenRequestConverter -- handleH2MGrant -- State mismatch. Expected: {}, Actual: {}", authorizationCodeData.state(), state); throw new IllegalArgumentException("Invalid state parameter"); - } Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); - // 3. Generar un JWT que contenga la VC como un claim log.info("Authorization code grant successfully handled"); - Map additionalParameters = new HashMap<>(); additionalParameters.put(OAuth2ParameterNames.CLIENT_ID,clientId); additionalParameters.put("vc",authorizationCodeData.verifiableCredential()); additionalParameters.put(OAuth2ParameterNames.SCOPE, String.join(" ", authorizationCodeData.requestedScopes())); additionalParameters.put(OAuth2ParameterNames.AUDIENCE,clientId); - return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, null,additionalParameters); } private Authentication handleM2MGrant(MultiValueMap parameters) { log.info("CustomTokenRequestConverter --> handleM2MGrant -- INIT"); Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); - String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); String clientAssertion = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION); - SignedJWT signedJWT = jwtService.parseJWT(clientAssertion); Payload payload = jwtService.getPayloadFromSignedJWT(signedJWT); String vpToken = jwtService.getClaimFromPayload(payload,"vp_token"); - // Check if VC is LEARCredentialMachine Type JsonNode vc = vpService.getCredentialFromTheVerifiablePresentationAsJsonNode(vpToken); LEARCredentialMachine learCredentialMachine = objectMapper.convertValue(vc, LEARCredentialMachine.class); @@ -113,14 +99,12 @@ private Authentication handleM2MGrant(MultiValueMap parameters) log.error("CustomTokenRequestConverter -- handleM2MGrant -- LEARCredentialType Expected: {}", LEARCredentialType.LEAR_CREDENTIAL_MACHINE.getValue()); throw new InvalidCredentialTypeException("Invalid LEARCredentialType. Expected LEARCredentialMachine"); } - // Check Client Assertion JWT Claims boolean isValid = clientAssertionValidationService.validateClientAssertionJWTClaims(clientId,payload); if (!isValid) { log.error("CustomTokenRequestConverter -- handleM2MGrant -- JWT claims from assertion are invalid"); throw new IllegalArgumentException("Invalid JWT claims from assertion"); } - // Validate VP isValid = vpService.validateVerifiablePresentation(vpToken); if (!isValid) { @@ -128,7 +112,6 @@ private Authentication handleM2MGrant(MultiValueMap parameters) throw new InvalidVPtokenException("VP Token used in M2M flow is invalid"); } log.info("VP Token validated successfully"); - Map additionalParameters = new HashMap<>(); additionalParameters.put(OAuth2ParameterNames.CLIENT_ID,clientId); additionalParameters.put("vc",vc); diff --git a/src/main/java/es/in2/verifier/service/impl/VpServiceImpl.java b/src/main/java/es/in2/verifier/service/impl/VpServiceImpl.java index 0549d4f..4b497bf 100644 --- a/src/main/java/es/in2/verifier/service/impl/VpServiceImpl.java +++ b/src/main/java/es/in2/verifier/service/impl/VpServiceImpl.java @@ -3,11 +3,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.Payload; -import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap; import com.nimbusds.jwt.SignedJWT; import es.in2.verifier.exception.*; -import es.in2.verifier.model.credentials.employee.LEARCredentialEmployee; -import es.in2.verifier.model.credentials.machine.LEARCredentialMachine; +import es.in2.verifier.model.credentials.lear.LEARCredential; +import es.in2.verifier.model.credentials.lear.employee.LEARCredentialEmployee; +import es.in2.verifier.model.credentials.lear.machine.LEARCredentialMachine; import es.in2.verifier.model.enums.LEARCredentialType; import es.in2.verifier.model.issuer.IssuerCredentialsCapabilities; import es.in2.verifier.service.*; @@ -18,6 +18,7 @@ import java.security.PublicKey; import java.text.ParseException; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -55,17 +56,20 @@ public boolean validateVerifiablePresentation(String verifiablePresentation) { Payload payload = jwtService.getPayloadFromSignedJWT(jwtCredential); log.debug("VpServiceImpl -- validateVerifiablePresentation -- Successfully extracted the Verifiable Credential payload"); + // Step 1.1: Map the payload to a VerifiableCredential object + LEARCredential learCredential = mapPayloadToVerifiableCredential(payload); + // Step 2: Validate the credential id is not in the revoked list log.debug("VpServiceImpl -- validateVerifiablePresentation -- Validating that the credential is not revoked"); - validateCredentialNotRevoked(payload); + validateCredentialNotRevoked(learCredential.id()); log.info("Credential is not revoked"); // Step 3: Validate the issuer - String credentialIssuerDid = jwtService.getClaimFromPayload(payload, "iss"); + String credentialIssuerDid = learCredential.issuerId(); log.debug("VpServiceImpl -- validateVerifiablePresentation -- Retrieved issuer DID from payload: {}", credentialIssuerDid); // Step 4: Extract and validate credential types - List credentialTypes = getCredentialTypes(payload); + List credentialTypes = learCredential.type(); log.debug("VpServiceImpl -- validateVerifiablePresentation -- Credential types extracted: {}", credentialTypes); // Step 5: Retrieve the list of issuer capabilities @@ -84,8 +88,8 @@ public boolean validateVerifiablePresentation(String verifiablePresentation) { // Map vcHeader = jwtCredential.getHeader().toJSONObject(); // certificateValidationService.extractAndVerifyCertificate(jwtCredential.serialize(),vcHeader, credentialIssuerDid.substring("did:elsi:".length())); // Extract public key from x5c certificate and validate OrganizationIdentifier - // Step 8: Extract the mandateId from the Verifiable Credential - String mandatorOrganizationIdentifier = extractMandatorOrganizationIdentifier(credentialTypes, payload); + // Step 8: Extract the mandator organization identifier from the Verifiable Credential + String mandatorOrganizationIdentifier = learCredential.mandatorOrganizationIdentifier(); log.debug("VpServiceImpl -- validateVerifiablePresentation -- Extracted Mandator Organization Identifier from Verifiable Credential: {}", mandatorOrganizationIdentifier); //TODO this must be validated against the participants list, not the issuer list @@ -94,7 +98,7 @@ public boolean validateVerifiablePresentation(String verifiablePresentation) { log.info("Mandator OrganizationIdentifier {} is valid and allowed", mandatorOrganizationIdentifier); // Step 9: Validate the VP's signature with the DIDService (the DID of the holder of the VP) - String mandateeId = extractMandateeId(credentialTypes, payload); + String mandateeId = learCredential.mandateeId(); PublicKey holderPublicKey = didService.getPublicKeyFromDid(mandateeId); // Get the holder's public key in bytes jwtService.verifyJWTWithECKey(verifiablePresentation, holderPublicKey); // Validate the VP was signed by the holder DID log.info("VP's signature is valid, holder DID {} confirmed", mandateeId); @@ -122,62 +126,61 @@ public JsonNode getCredentialFromTheVerifiablePresentationAsJsonNode(String veri return convertObjectToJSONNode(getCredentialFromTheVerifiablePresentation(verifiablePresentation)); } - private List getCredentialTypes(Payload payload) { - log.debug("VpServiceImpl -- getCredentialTypes -- Extracting credential types from payload"); - // Extract and validate the credential types from the payload - Object vcFromPayload = jwtService.getVCFromPayload(payload); - - if (vcFromPayload instanceof LinkedTreeMap vcObject) { - // Use a wildcard generic type to avoid unchecked cast warning - Object typeObject = vcObject.get("type"); - - if (typeObject instanceof List typeList) { - // Check each element to ensure it's a String - if (typeList.stream().allMatch(String.class::isInstance)) { - // Safely cast the List to List - log.info("Credential types successfully extracted: {}", typeList); - return typeList.stream().map(String.class::cast).toList(); - } else { - log.error("VpServiceImpl -- getCredentialTypes -- Type list elements are not all of type String."); - throw new InvalidCredentialTypeException("Type list elements are not all of type String."); - } - } else { - log.error("VpServiceImpl -- getCredentialTypes -- 'type' key does not map to a List."); - throw new InvalidCredentialTypeException("'type' key does not map to a List."); - } - } else { - log.error("VpServiceImpl -- getCredentialTypes -- VC from payload is not a LinkedTreeMap."); - throw new InvalidCredentialTypeException("VC from payload is not a LinkedTreeMap."); + private LEARCredential mapPayloadToVerifiableCredential(Payload payload) { + Object vcObject = jwtService.getVCFromPayload(payload); + try { + Map vcMap = validateAndCastToMap(vcObject); + List types = extractAndValidateTypes(vcMap); + return mapToSpecificCredential(vcMap, types); + } catch (IllegalArgumentException e) { + throw new CredentialMappingException("Error mapping VC payload to specific Verifiable Credential class: " + e.getMessage()); } } - private String extractMandateeId(List credentialTypes, Payload payload) { - Object vcObject = jwtService.getVCFromPayload(payload); + private Map validateAndCastToMap(Object vcObject) { + if (!(vcObject instanceof Map map)) { + throw new CredentialMappingException("Invalid payload format for Verifiable Credential."); + } - if (credentialTypes.contains(LEARCredentialType.LEAR_CREDENTIAL_EMPLOYEE.getValue())) { - LEARCredentialEmployee learCredentialEmployee = mapCredentialToLEARCredentialEmployee(vcObject); - return learCredentialEmployee.credentialSubject().mandate().mandatee().id(); - } else if (credentialTypes.contains(LEARCredentialType.LEAR_CREDENTIAL_MACHINE.getValue())) { - LEARCredentialMachine learCredentialMachine = mapCredentialToLEARCredentialMachine(vcObject); - return learCredentialMachine.credentialSubject().mandate().mandatee().id(); - } else { - log.error("VpServiceImpl -- extractMandateeId -- Invalid Credential Type. LEARCredentialEmployee or LEARCredentialMachine required."); - throw new InvalidCredentialTypeException("Invalid Credential Type. LEARCredentialEmployee or LEARCredentialMachine required."); + // Ensure the map's keys are all types are Strings and values are Objects + Map validatedMap = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (!(entry.getKey() instanceof String)) { + throw new CredentialMappingException("Invalid key type found in Verifiable Credential map: " + entry.getKey()); + } + validatedMap.put((String) entry.getKey(), entry.getValue()); } + + return validatedMap; } - private String extractMandatorOrganizationIdentifier(List credentialTypes, Payload payload) { - Object vcObject = jwtService.getVCFromPayload(payload); + private List extractAndValidateTypes(Map vcMap) { + Object typeObject = vcMap.get("type"); - if (credentialTypes.contains(LEARCredentialType.LEAR_CREDENTIAL_EMPLOYEE.getValue())) { - LEARCredentialEmployee learCredentialEmployee = mapCredentialToLEARCredentialEmployee(vcObject); - return learCredentialEmployee.credentialSubject().mandate().mandator().organizationIdentifier(); - } else if (credentialTypes.contains(LEARCredentialType.LEAR_CREDENTIAL_MACHINE.getValue())) { - LEARCredentialMachine learCredentialMachine = mapCredentialToLEARCredentialMachine(vcObject); - return learCredentialMachine.credentialSubject().mandate().mandator().organizationIdentifier(); + // Validate that the "type" object is a list + if (!(typeObject instanceof List typeList)) { + throw new CredentialMappingException("'type' key is not a list."); + } + + // Ensure that all elements in the list are Strings + if (!typeList.stream().allMatch(String.class::isInstance)) { + throw new CredentialMappingException("'type' list contains non-string elements."); + } + + // Safely cast the List to List + return typeList.stream() + .map(String.class::cast) + .toList(); + } + + private LEARCredential mapToSpecificCredential(Map vcMap, List types) { + if (types.contains(LEARCredentialType.LEAR_CREDENTIAL_EMPLOYEE.getValue())) { + return objectMapper.convertValue(vcMap, LEARCredentialEmployee.class); + } else if (types.contains(LEARCredentialType.LEAR_CREDENTIAL_MACHINE.getValue())) { + return objectMapper.convertValue(vcMap, LEARCredentialMachine.class); } else { - throw new InvalidCredentialTypeException("Invalid Credential Type. LEARCredentialEmployee or LEARCredentialMachine required."); + throw new InvalidCredentialTypeException("Unsupported credential type: " + types); } } @@ -196,24 +199,12 @@ private void validateCredentialTypeWithIssuerCapabilities(List vcObject) { - // Use a wildcard generic type to avoid unchecked cast warning - Object credentialId = vcObject.get("id").toString(); - List revokedIds = trustFrameworkService.getRevokedCredentialIds(); - if (revokedIds.contains(credentialId)) { - log.error("VpServiceImpl -- validateCredentialNotRevoked -- Credential ID {} is revoked", credentialId); - throw new CredentialRevokedException("Credential ID " + credentialId + " is revoked."); - } - } - else { - log.error("VpServiceImpl -- validateCredentialNotRevoked -- VC from payload is not a LinkedTreeMap."); - throw new InvalidCredentialTypeException("VC from payload is not a LinkedTreeMap."); + private void validateCredentialNotRevoked(String credentialId) { + List revokedIds = trustFrameworkService.getRevokedCredentialIds(); + if (revokedIds.contains(credentialId)) { + throw new CredentialRevokedException("Credential ID " + credentialId + " is revoked."); } + } @@ -236,24 +227,6 @@ private JsonNode convertObjectToJSONNode(Object vcObject) throws JsonConversionE return jsonNode; } - - private LEARCredentialMachine mapCredentialToLEARCredentialMachine(Object vcObject) { - try { - return objectMapper.convertValue(vcObject, LEARCredentialMachine.class); - } catch (IllegalArgumentException e) { - throw new CredentialMappingException("Error converting VC to LEARCredentialMachine"); - } - } - - private LEARCredentialEmployee mapCredentialToLEARCredentialEmployee(Object vcObject) { - try { - // Convert the Object to a Map or directly to the LEARCredentialEmployee class - return objectMapper.convertValue(vcObject, LEARCredentialEmployee.class); - } catch (IllegalArgumentException e) { - throw new CredentialMappingException("Error converting VC to LEARCredentialEmployee"); - } - } - private SignedJWT extractFirstVerifiableCredential(String verifiablePresentation) { try { // Parse the Verifiable Presentation (VP) JWT diff --git a/src/test/java/es/in2/verifier/security/filters/CustomAuthenticationProviderTest.java b/src/test/java/es/in2/verifier/security/filters/CustomAuthenticationProviderTest.java index d465d59..e2cd3b7 100644 --- a/src/test/java/es/in2/verifier/security/filters/CustomAuthenticationProviderTest.java +++ b/src/test/java/es/in2/verifier/security/filters/CustomAuthenticationProviderTest.java @@ -3,14 +3,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import es.in2.verifier.config.properties.SecurityProperties; -import es.in2.verifier.model.credentials.employee.CredentialSubjectLCEmployee; -import es.in2.verifier.model.credentials.employee.LEARCredentialEmployee; -import es.in2.verifier.model.credentials.employee.MandateLCEmployee; -import es.in2.verifier.model.credentials.employee.MandateeLCEmployee; -import es.in2.verifier.model.credentials.machine.CredentialSubjectLCMachine; -import es.in2.verifier.model.credentials.machine.LEARCredentialMachine; -import es.in2.verifier.model.credentials.machine.MandateLCMachine; -import es.in2.verifier.model.credentials.machine.MandateeLCMachine; +import es.in2.verifier.model.credentials.Issuer; +import es.in2.verifier.model.credentials.lear.CredentialSubject; +import es.in2.verifier.model.credentials.lear.Mandate; +import es.in2.verifier.model.credentials.lear.Mandatee; +import es.in2.verifier.model.credentials.lear.employee.LEARCredentialEmployee; +import es.in2.verifier.model.credentials.lear.machine.LEARCredentialMachine; import es.in2.verifier.service.JWTService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -20,7 +18,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -58,91 +55,123 @@ class CustomAuthenticationProviderTest { private CustomAuthenticationProvider customAuthenticationProvider; @Test - void authenticate_validCodeGrant_success_With_LEARCredentialEmployee() { + void authenticate_validAuthorizationCodeGrant_withEmployeeCredential_success() throws Exception { + // Arrange String clientId = "test-client-id"; + String audience = "test-audience"; Map additionalParameters = new HashMap<>(); - additionalParameters.put("vc", new HashMap<>()); additionalParameters.put("client_id", clientId); - additionalParameters.put("audience", "test-audience"); - additionalParameters.put("nonce", "test-nonce"); - OAuth2AuthorizationCodeAuthenticationToken auth = mock(OAuth2AuthorizationCodeAuthenticationToken.class); - when(auth.getAdditionalParameters()).thenReturn(additionalParameters); + // Configure the verifiable credential (VC) map + Map vcMap = new HashMap<>(); + vcMap.put("type", List.of("VerifiableCredential", "LEARCredentialEmployee")); + + + + additionalParameters.put("vc", vcMap); + additionalParameters.put(OAuth2ParameterNames.AUDIENCE, audience); + additionalParameters.put(OAuth2ParameterNames.SCOPE, "openid profile email"); + + OAuth2AuthorizationCodeAuthenticationToken authenticationToken = mock(OAuth2AuthorizationCodeAuthenticationToken.class); + when(authenticationToken.getAdditionalParameters()).thenReturn(additionalParameters); RegisteredClient registeredClient = mock(RegisteredClient.class); when(registeredClientRepository.findByClientId(clientId)).thenReturn(registeredClient); - when(securityProperties.token()).thenReturn(mock(SecurityProperties.TokenProperties.class)); - when(securityProperties.token().accessToken()).thenReturn(mock(SecurityProperties.TokenProperties.AccessTokenProperties.class)); - when(securityProperties.token().accessToken().expiration()).thenReturn("3600"); - when(securityProperties.token().accessToken().cronUnit()).thenReturn("SECONDS"); + SecurityProperties.TokenProperties tokenProperties = mock(SecurityProperties.TokenProperties.class); + SecurityProperties.TokenProperties.AccessTokenProperties accessTokenProperties = mock(SecurityProperties.TokenProperties.AccessTokenProperties.class); + SecurityProperties.TokenProperties.IdTokenProperties idTokenProperties = mock(SecurityProperties.TokenProperties.IdTokenProperties.class); - when(securityProperties.token().idToken()).thenReturn(mock(SecurityProperties.TokenProperties.IdTokenProperties.class)); - when(securityProperties.token().idToken().expiration()).thenReturn("3600"); - when(securityProperties.token().idToken().cronUnit()).thenReturn("SECONDS"); + when(securityProperties.token()).thenReturn(tokenProperties); + when(tokenProperties.accessToken()).thenReturn(accessTokenProperties); + when(accessTokenProperties.expiration()).thenReturn("3600"); + when(accessTokenProperties.cronUnit()).thenReturn("SECONDS"); + when(tokenProperties.idToken()).thenReturn(idTokenProperties); + when(idTokenProperties.expiration()).thenReturn("3600"); + when(idTokenProperties.cronUnit()).thenReturn("SECONDS"); + when(securityProperties.authorizationServer()).thenReturn("https://auth.server"); - JsonNode jsonNode = mock(JsonNode.class); - when(objectMapper.convertValue(additionalParameters.get("vc"), JsonNode.class)).thenReturn(jsonNode); + JsonNode vcJsonNode = mock(JsonNode.class); + when(objectMapper.convertValue(vcMap, JsonNode.class)).thenReturn(vcJsonNode); - LEARCredentialEmployee credential = getLEARCredentialEmployee(); - when(objectMapper.convertValue(any(), eq(LEARCredentialEmployee.class))).thenReturn(credential); + LEARCredentialEmployee learCredentialEmployee = getLEARCredentialEmployee(); - String jwtToken = "mock-jwt-token"; - when(jwtService.generateJWT(any())).thenReturn(jwtToken); + when(objectMapper.convertValue(vcJsonNode, LEARCredentialEmployee.class)).thenReturn(learCredentialEmployee); - Authentication result = customAuthenticationProvider.authenticate(auth); + when(objectMapper.writeValueAsString(learCredentialEmployee)).thenReturn("{\"credential\":\"value\"}"); + + when(jwtService.generateJWT(anyString())).thenReturn("mock-jwt-token"); + + Authentication result = customAuthenticationProvider.authenticate(authenticationToken); assertNotNull(result); assertInstanceOf(OAuth2AccessTokenAuthenticationToken.class, result); - OAuth2AccessToken accessToken = ((OAuth2AccessTokenAuthenticationToken) result).getAccessToken(); - assertEquals(jwtToken, accessToken.getTokenValue()); - assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType()); + OAuth2AccessTokenAuthenticationToken tokenResult = (OAuth2AccessTokenAuthenticationToken) result; + assertEquals("mock-jwt-token", tokenResult.getAccessToken().getTokenValue()); + + Map additionalParams = tokenResult.getAdditionalParameters(); + assertTrue(additionalParams.containsKey("id_token")); + assertEquals("mock-jwt-token", additionalParams.get("id_token")); + + verify(jwtService, times(2)).generateJWT(anyString()); } + @Test - void authenticate_validCodeGrant_success_With_LEARCredentialMachine() { + void authenticate_validClientCredentialsGrant_withMachineCredential_success() throws Exception { + // Arrange String clientId = "test-client-id"; Map additionalParameters = new HashMap<>(); - additionalParameters.put("vc", new HashMap<>()); additionalParameters.put("client_id", clientId); - additionalParameters.put("audience", "test-audience"); - OAuth2ClientCredentialsAuthenticationToken auth = mock(OAuth2ClientCredentialsAuthenticationToken.class); - when(auth.getAdditionalParameters()).thenReturn(additionalParameters); + Map vcMap = new HashMap<>(); + vcMap.put("type", List.of("VerifiableCredential", "LEARCredentialMachine")); + + additionalParameters.put("vc", vcMap); + additionalParameters.put(OAuth2ParameterNames.SCOPE, "machine"); + + OAuth2ClientCredentialsAuthenticationToken authenticationToken = mock(OAuth2ClientCredentialsAuthenticationToken.class); + when(authenticationToken.getAdditionalParameters()).thenReturn(additionalParameters); RegisteredClient registeredClient = mock(RegisteredClient.class); when(registeredClientRepository.findByClientId(clientId)).thenReturn(registeredClient); - when(securityProperties.token()).thenReturn(mock(SecurityProperties.TokenProperties.class)); - - when(securityProperties.token().accessToken()).thenReturn(mock(SecurityProperties.TokenProperties.AccessTokenProperties.class)); - when(securityProperties.token().accessToken().expiration()).thenReturn("3600"); - when(securityProperties.token().accessToken().cronUnit()).thenReturn("SECONDS"); + SecurityProperties.TokenProperties tokenProperties = mock(SecurityProperties.TokenProperties.class); + SecurityProperties.TokenProperties.AccessTokenProperties accessTokenProperties = mock(SecurityProperties.TokenProperties.AccessTokenProperties.class); + SecurityProperties.TokenProperties.IdTokenProperties idTokenProperties = mock(SecurityProperties.TokenProperties.IdTokenProperties.class); - when(securityProperties.token().idToken()).thenReturn(mock(SecurityProperties.TokenProperties.IdTokenProperties.class)); - when(securityProperties.token().idToken().expiration()).thenReturn("3600"); - when(securityProperties.token().idToken().cronUnit()).thenReturn("SECONDS"); + when(securityProperties.token()).thenReturn(tokenProperties); + when(tokenProperties.accessToken()).thenReturn(accessTokenProperties); + when(accessTokenProperties.expiration()).thenReturn("3600"); + when(accessTokenProperties.cronUnit()).thenReturn("SECONDS"); + when(tokenProperties.idToken()).thenReturn(idTokenProperties); + when(idTokenProperties.expiration()).thenReturn("3600"); + when(idTokenProperties.cronUnit()).thenReturn("SECONDS"); + when(securityProperties.authorizationServer()).thenReturn("https://auth.server"); - JsonNode jsonNode = mock(JsonNode.class); - when(objectMapper.convertValue(additionalParameters.get("vc"), JsonNode.class)).thenReturn(jsonNode); + JsonNode vcJsonNode = mock(JsonNode.class); + when(objectMapper.convertValue(vcMap, JsonNode.class)).thenReturn(vcJsonNode); LEARCredentialMachine credential = getLEARCredentialMachine(); - when(objectMapper.convertValue(any(), eq(LEARCredentialMachine.class))).thenReturn(credential); + when(objectMapper.convertValue(vcJsonNode, LEARCredentialMachine.class)).thenReturn(credential); - String jwtToken = "mock-jwt-token"; - when(jwtService.generateJWT(any())).thenReturn(jwtToken); + when(jwtService.generateJWT(anyString())).thenReturn("mock-jwt-token"); - Authentication result = customAuthenticationProvider.authenticate(auth); + // Act + Authentication result = customAuthenticationProvider.authenticate(authenticationToken); + // Assert assertNotNull(result); assertInstanceOf(OAuth2AccessTokenAuthenticationToken.class, result); - OAuth2AccessToken accessToken = ((OAuth2AccessTokenAuthenticationToken) result).getAccessToken(); - assertEquals(jwtToken, accessToken.getTokenValue()); - assertEquals(OAuth2AccessToken.TokenType.BEARER, accessToken.getTokenType()); + OAuth2AccessTokenAuthenticationToken tokenResult = (OAuth2AccessTokenAuthenticationToken) result; + assertEquals("mock-jwt-token", tokenResult.getAccessToken().getTokenValue()); + + verify(jwtService, times(2)).generateJWT(anyString()); } + @Test void authenticate_throw_OAuth2AuthenticationException() { Authentication invalidAuthentication = new UsernamePasswordAuthenticationToken("user", "password"); @@ -354,7 +383,7 @@ void authenticate_withProfileAndEmailScopes_addsCorrespondingClaims() throws Exc // Then assertNotNull(result); - assertTrue(result instanceof OAuth2AccessTokenAuthenticationToken); + assertInstanceOf(OAuth2AccessTokenAuthenticationToken.class, result); OAuth2AccessTokenAuthenticationToken tokenResult = (OAuth2AccessTokenAuthenticationToken) result; assertEquals("mock-jwt-token", tokenResult.getAccessToken().getTokenValue()); @@ -415,30 +444,40 @@ void supports_returnsFalse_forNonAuthenticationClass() { private LEARCredentialEmployee getLEARCredentialEmployee(){ - MandateeLCEmployee mandatee = MandateeLCEmployee.builder() + Mandatee mandatee = Mandatee.builder() .id("did:key:1234") .firstName("John") .lastName("Doe") .email("john.doe@example.com") .build(); - MandateLCEmployee mandate = MandateLCEmployee.builder() + Mandate mandate = Mandate.builder() .mandatee(mandatee) .build(); - CredentialSubjectLCEmployee credentialSubject = CredentialSubjectLCEmployee.builder() + CredentialSubject credentialSubject = CredentialSubject.builder() .mandate(mandate) .build(); return LEARCredentialEmployee.builder() + .type(List.of("VerifiableCredential", "LEARCredentialEmployee")) + .context(List.of("https://www.w3.org/2018/credentials/v1")) + .id("urn:uuid:1234") + .issuer("did:elsi:issuer") .credentialSubject(credentialSubject) .build(); } private LEARCredentialMachine getLEARCredentialMachine(){ - MandateeLCMachine mandateeLCEmployee = MandateeLCMachine.builder().id("mandatee-id").build(); - MandateLCMachine mandateLCEmployee = MandateLCMachine.builder().mandatee(mandateeLCEmployee).build(); - CredentialSubjectLCMachine credentialSubject = new CredentialSubjectLCMachine(mandateLCEmployee); + Mandatee mandateeLCEmployee = Mandatee.builder().id("mandatee-id").build(); + Mandate mandateLCEmployee = Mandate.builder().mandatee(mandateeLCEmployee).build(); + CredentialSubject credentialSubject = new CredentialSubject(mandateLCEmployee); return LEARCredentialMachine.builder() .credentialSubject(credentialSubject) + .context(List.of("https://www.w3.org/2018/credentials/v1")) + .id("urn:uuid:1234") + .issuer(Issuer.builder() + .id("did:elsi:issuer") + .build()) + .type(List.of("VerifiableCredential", "LEARCredentialMachine")) .build(); } diff --git a/src/test/java/es/in2/verifier/security/filters/CustomTokenRequestConverterTest.java b/src/test/java/es/in2/verifier/security/filters/CustomTokenRequestConverterTest.java index fc31354..ba8a123 100644 --- a/src/test/java/es/in2/verifier/security/filters/CustomTokenRequestConverterTest.java +++ b/src/test/java/es/in2/verifier/security/filters/CustomTokenRequestConverterTest.java @@ -2,13 +2,14 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.Payload; import com.nimbusds.jwt.SignedJWT; import es.in2.verifier.config.CacheStore; import es.in2.verifier.exception.InvalidCredentialTypeException; import es.in2.verifier.exception.InvalidVPtokenException; import es.in2.verifier.exception.UnsupportedGrantTypeException; import es.in2.verifier.model.AuthorizationCodeData; -import es.in2.verifier.model.credentials.machine.LEARCredentialMachine; +import es.in2.verifier.model.credentials.lear.machine.LEARCredentialMachine; import es.in2.verifier.model.enums.LEARCredentialType; import es.in2.verifier.service.ClientAssertionValidationService; import es.in2.verifier.service.JWTService; @@ -34,7 +35,6 @@ import static org.hibernate.validator.internal.util.Contracts.assertNotNull; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -91,45 +91,55 @@ void convert_authorizationCodeGrant_shouldReturnOAuth2ClientCredentialsAuthentic verify(oAuth2AuthorizationService).remove(authorizationCodeData.oAuth2Authorization()); } - @Test - void convert_clientCredentialsGrant_shouldReturnOAuth2ClientCredentialsAuthenticationToken() { + void convert_clientCredentialsGrant_success(){ + // Arrange HttpServletRequest mockRequest = mock(HttpServletRequest.class); Authentication clientPrincipal = mock(Authentication.class); SecurityContextHolder.getContext().setAuthentication(clientPrincipal); + String clientId = "client-id"; + String clientAssertion = "client-assertion"; + String vpToken = "vp-token"; + MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.add(OAuth2ParameterNames.GRANT_TYPE, "client_credentials"); - parameters.add(OAuth2ParameterNames.CLIENT_ID, "client-id"); - parameters.add(OAuth2ParameterNames.CLIENT_ASSERTION, "client-assertion"); + parameters.add(OAuth2ParameterNames.CLIENT_ID, clientId); + parameters.add(OAuth2ParameterNames.CLIENT_ASSERTION, clientAssertion); when(mockRequest.getParameterMap()).thenReturn(convertToMap(parameters)); + // Mock del JWTService SignedJWT signedJWT = mock(SignedJWT.class); - when(jwtService.parseJWT("client-assertion")).thenReturn(signedJWT); + Payload payload = mock(Payload.class); - String vpToken = "vp-token"; - when(jwtService.getClaimFromPayload(any(), eq("vp_token"))).thenReturn(vpToken); + when(jwtService.parseJWT(clientAssertion)).thenReturn(signedJWT); + when(jwtService.getPayloadFromSignedJWT(signedJWT)).thenReturn(payload); + when(jwtService.getClaimFromPayload(payload, "vp_token")).thenReturn(vpToken); JsonNode mockVC = mock(JsonNode.class); - when(vpService.getCredentialFromTheVerifiablePresentationAsJsonNode(anyString())).thenReturn(mockVC); + + when(vpService.getCredentialFromTheVerifiablePresentationAsJsonNode(vpToken)).thenReturn(mockVC); + + when(clientAssertionValidationService.validateClientAssertionJWTClaims(clientId, payload)).thenReturn(true); + when(vpService.validateVerifiablePresentation(vpToken)).thenReturn(true); LEARCredentialMachine learCredentialMachine = mock(LEARCredentialMachine.class); when(objectMapper.convertValue(mockVC, LEARCredentialMachine.class)).thenReturn(learCredentialMachine); when(learCredentialMachine.type()).thenReturn(List.of(LEARCredentialType.LEAR_CREDENTIAL_MACHINE.getValue())); - when(clientAssertionValidationService.validateClientAssertionJWTClaims(anyString(), any())).thenReturn(true); - when(vpService.validateVerifiablePresentation(anyString())).thenReturn(true); - - OAuth2ClientCredentialsAuthenticationToken result = - (OAuth2ClientCredentialsAuthenticationToken) customTokenRequestConverter.convert(mockRequest); + // Act + Authentication result = customTokenRequestConverter.convert(mockRequest); + // Assert assertNotNull(result); - assertEquals("client-id", result.getAdditionalParameters().get(OAuth2ParameterNames.CLIENT_ID)); - assertEquals(mockVC, result.getAdditionalParameters().get("vc")); + assertInstanceOf(OAuth2ClientCredentialsAuthenticationToken.class, result); + + OAuth2ClientCredentialsAuthenticationToken token = (OAuth2ClientCredentialsAuthenticationToken) result; + assertEquals(clientPrincipal, token.getPrincipal()); - verify(clientAssertionValidationService, times(1)).validateClientAssertionJWTClaims(anyString(), any()); - verify(vpService, times(1)).validateVerifiablePresentation(anyString()); + Map additionalParameters = token.getAdditionalParameters(); + assertEquals(clientId, additionalParameters.get(OAuth2ParameterNames.CLIENT_ID)); } @Test diff --git a/src/test/java/es/in2/verifier/service/VpServiceImplTest.java b/src/test/java/es/in2/verifier/service/VpServiceImplTest.java index 1608c70..bc3d85a 100644 --- a/src/test/java/es/in2/verifier/service/VpServiceImplTest.java +++ b/src/test/java/es/in2/verifier/service/VpServiceImplTest.java @@ -3,17 +3,16 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.Payload; import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import es.in2.verifier.exception.JsonConversionException; -import es.in2.verifier.model.credentials.Mandator; -import es.in2.verifier.model.credentials.employee.CredentialSubjectLCEmployee; -import es.in2.verifier.model.credentials.employee.LEARCredentialEmployee; -import es.in2.verifier.model.credentials.employee.MandateLCEmployee; -import es.in2.verifier.model.credentials.employee.MandateeLCEmployee; +import es.in2.verifier.model.credentials.lear.CredentialSubject; +import es.in2.verifier.model.credentials.lear.Mandate; +import es.in2.verifier.model.credentials.lear.Mandatee; +import es.in2.verifier.model.credentials.lear.Mandator; +import es.in2.verifier.model.credentials.lear.employee.LEARCredentialEmployee; import es.in2.verifier.model.issuer.IssuerCredentialsCapabilities; import es.in2.verifier.model.issuer.TimeRange; import es.in2.verifier.service.impl.VpServiceImpl; @@ -38,6 +37,7 @@ import java.util.List; import java.util.Map; +import static es.in2.verifier.util.Constants.DID_ELSI_PREFIX; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -262,6 +262,7 @@ void validateVerifiablePresentation_invalidVP_throws_JWTParsingException_and_ret void validateVerifiablePresentation_success() throws Exception { // Given String verifiablePresentation = "valid.vp.jwt"; + LEARCredentialEmployee learCredentialEmployee = getLEARCredentialEmployee(); // Step 1: Parse the VP JWT SignedJWT vpSignedJWT = mock(SignedJWT.class); @@ -287,22 +288,17 @@ void validateVerifiablePresentation_success() throws Exception { when(jwtService.getPayloadFromSignedJWT(jwtCredential)).thenReturn(payload); // Step 3: Validate the credential id is not in the revoked list - // Create a vcFromPayload Map with the "id" field + // Create a vcFromPayload Map LinkedTreeMap vcFromPayload = new LinkedTreeMap<>(); - vcFromPayload.put("id", "credential-id-123"); when(jwtService.getVCFromPayload(payload)).thenReturn(vcFromPayload); // Mock trustFrameworkService.getRevokedCredentialIds to return an empty list when(trustFrameworkService.getRevokedCredentialIds()).thenReturn(Collections.emptyList()); - // Step 4: Validate the issuer - String credentialIssuerDid = "did:elsi:issuer123"; - when(jwtService.getClaimFromPayload(payload, "iss")).thenReturn(credentialIssuerDid); - - // Step 5: Extract and validate credential types + // Step 4: Extract and validate credential types vcFromPayload.put("type", List.of("LEARCredentialEmployee")); - // Step 6: Retrieve the list of issuer capabilities + // Step 5: Retrieve the list of issuer capabilities List issuerCapabilitiesList = List.of( IssuerCredentialsCapabilities.builder() .validFor(new TimeRange(Instant.now().toString(), Instant.now().plusSeconds(3600).toString())) @@ -310,42 +306,16 @@ void validateVerifiablePresentation_success() throws Exception { .claims(null) .build() ); - when(trustFrameworkService.getTrustedIssuerListData(credentialIssuerDid)).thenReturn(issuerCapabilitiesList); - -// // Step 7: Verify the signature and the organizationId of the credential signature -// Map vcHeader = new HashMap<>(); -// vcHeader.put("x5c", List.of("base64Cert")); -// JWSHeader header = mock(JWSHeader.class); -// when(jwtCredential.getHeader()).thenReturn(header); -// when(header.toJSONObject()).thenReturn(vcHeader); -// -// -// when(jwtCredential.serialize()).thenReturn(vcJwt); -// -// doNothing().when(certificateValidationService).extractAndVerifyCertificate(any(), eq(vcHeader),eq("issuer123")); - - // Step 8: Mock the mapping to LEARCredentialEmployee - LEARCredentialEmployee learCredentialEmployee = mock(LEARCredentialEmployee.class); - CredentialSubjectLCEmployee credentialSubject = mock(CredentialSubjectLCEmployee.class); - MandateLCEmployee mandate = mock(MandateLCEmployee.class); - Mandator mandator = mock(Mandator.class); - MandateeLCEmployee mandatee = mock(MandateeLCEmployee.class); - - when(learCredentialEmployee.credentialSubject()).thenReturn(credentialSubject); - when(credentialSubject.mandate()).thenReturn(mandate); - when(mandate.mandator()).thenReturn(mandator); - when(mandator.organizationIdentifier()).thenReturn("org123"); - when(mandate.mandatee()).thenReturn(mandatee); - when(mandatee.id()).thenReturn("did:example:mandatee123"); + when(trustFrameworkService.getTrustedIssuerListData(learCredentialEmployee.issuer())).thenReturn(issuerCapabilitiesList); when(objectMapper.convertValue(vcFromPayload, LEARCredentialEmployee.class)).thenReturn(learCredentialEmployee); - // Step 9: Validate the mandator with trusted issuer service - when(trustFrameworkService.getTrustedIssuerListData("did:elsi:org123")).thenReturn(issuerCapabilitiesList); + // Step 7: Validate the mandator with trusted issuer service + when(trustFrameworkService.getTrustedIssuerListData(DID_ELSI_PREFIX + learCredentialEmployee.mandatorOrganizationIdentifier())).thenReturn(issuerCapabilitiesList); - // Step 11: Get the holder's public key + // Step 8: Get the holder's public key PublicKey holderPublicKey = generateECPublicKey(); - when(didService.getPublicKeyFromDid("did:example:mandatee123")).thenReturn(holderPublicKey); + when(didService.getPublicKeyFromDid(learCredentialEmployee.mandateeId())).thenReturn(holderPublicKey); // Mock jwtService.verifyJWTSignature for the Verifiable Presentation doNothing().when(jwtService).verifyJWTWithECKey(verifiablePresentation, holderPublicKey); @@ -367,4 +337,29 @@ private ECPublicKey generateECPublicKey() throws NoSuchAlgorithmException, Inval return (ECPublicKey) keyPairGenerator.generateKeyPair().getPublic(); } + private LEARCredentialEmployee getLEARCredentialEmployee(){ + Mandatee mandatee = Mandatee.builder() + .id("did:key:1234") + .firstName("John") + .lastName("Doe") + .email("john.doe@example.com") + .build(); + Mandator mandator = Mandator.builder() + .organizationIdentifier("VATIT-1234") + .build(); + Mandate mandate = Mandate.builder() + .mandatee(mandatee) + .mandator(mandator) + .build(); + CredentialSubject credentialSubject = CredentialSubject.builder() + .mandate(mandate) + .build(); + return LEARCredentialEmployee.builder() + .type(List.of("VerifiableCredential", "LEARCredentialEmployee")) + .context(List.of("https://www.w3.org/2018/credentials/v1")) + .id("urn:uuid:1234") + .issuer("did:elsi:issuer") + .credentialSubject(credentialSubject) + .build(); + } }