diff --git a/charts/templates/configmap.yaml b/charts/templates/configmap.yaml index 03c9a2256b7596352e3e2b6cdd0b3e418845924f..3eaac48b0f35e75be0fcd712d73810bbd1876205 100644 --- a/charts/templates/configmap.yaml +++ b/charts/templates/configmap.yaml @@ -16,8 +16,14 @@ data: KEYPAIR_SIGNATURE_ALGORITHM: "{{ .Values.keypair.signatureAlgorithm }}" KEYPAIR_KEY_LENGTH: "{{ .Values.keypair.keyLength }}" - MICROSERVICE_USERSROLES_URL: "{{ .Values.microservices.usersRolesUrl }}" + MICROSERVICE_USERSROLES_URL: {{ tpl .Values.microservices.usersRolesUrl . | quote }} + +{{- if eq .Values.global.profile "authority" }} + MICROSERVICE_IDENTITY_PROVIDER_URL: {{ tpl .Values.microservices.identityProviderUrl . | quote }} +{{- end }} SIMPL_CERTIFICATE_SAN: "{{ .Values.simpl.certificate.san }}" CLIENT_AUTHORITY_URL: "{{- include "tls.gateway.url" . }}" + + SPRING_PROFILES_ACTIVE: {{ .Values.profile | quote }} diff --git a/charts/values.yaml b/charts/values.yaml index 1fc16de308a0f29556177431b8a397950fdc682c..5c9cd22d3cd9bf9a38233d790370f2faefb43a92 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -126,8 +126,11 @@ keypair: keyLength: 256 microservices: - usersRolesUrl: http://identity-provider.{{ .Release.Namespace }}.svc.cluster.local:8080 + usersRolesUrl: http://users-roles.{{ .Release.Namespace }}.svc.cluster.local:8080 + identityProviderUrl: http://identity-provider.{{ .Release.Namespace }}.svc.cluster.local:8080 simpl: certificate: san: <your-t2-fqdn> + +profile: # authority / participant \ No newline at end of file diff --git a/src/main/java/eu/europa/ec/simpl/authenticationprovider/configurations/ClientConfig.java b/src/main/java/eu/europa/ec/simpl/authenticationprovider/configurations/ClientConfig.java index 528b7a26d33d0df218e0ac27f90c64f14d6114c5..a17a2649d88fc53d6a24451ac866c57f09d17c78 100644 --- a/src/main/java/eu/europa/ec/simpl/authenticationprovider/configurations/ClientConfig.java +++ b/src/main/java/eu/europa/ec/simpl/authenticationprovider/configurations/ClientConfig.java @@ -1,7 +1,11 @@ package eu.europa.ec.simpl.authenticationprovider.configurations; +import eu.europa.ec.simpl.api.identityprovider.v1.exchanges.MtlsApi; import eu.europa.ec.simpl.api.usersroles.v1.exchanges.IdentityAttributesApi; +import eu.europa.ec.simpl.api.usersroles.v1.exchanges.PublicKeysApi; +import eu.europa.ec.simpl.common.annotations.Authority; import eu.europa.ec.simpl.common.exchanges.authenticationprovider.CredentialExchange; +import java.net.URI; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestClient; @@ -11,19 +15,36 @@ import org.springframework.web.service.invoker.HttpServiceProxyFactory; @Configuration public class ClientConfig { + private static final String V1_PREFIX = "v1"; + private final MicroserviceProperties properties; + + public ClientConfig(MicroserviceProperties properties) { + this.properties = properties; + } + + @Bean + @Authority + public MtlsApi tierOnePublicKeyApi(RestClient.Builder restClientBuilder) { + return buildExchange(properties.identityProvider().url().resolve(V1_PREFIX), restClientBuilder, MtlsApi.class); + } + + @Bean + public PublicKeysApi publicKeysApi(RestClient.Builder restClientBuilder) { + return buildExchange(properties.usersRoles().url().resolve(V1_PREFIX), restClientBuilder, PublicKeysApi.class); + } + @Bean - public CredentialExchange credentialExchange( - MicroserviceProperties properties, RestClient.Builder restClientBuilder) { + public CredentialExchange credentialExchange(RestClient.Builder restClientBuilder) { return buildExchange(properties.usersRoles().url(), restClientBuilder, CredentialExchange.class); } @Bean - public IdentityAttributesApi usersRolesIdentityAttributesApi( - MicroserviceProperties properties, RestClient.Builder restClientBuilder) { - return buildExchange(properties.usersRoles().url(), restClientBuilder, IdentityAttributesApi.class); + public IdentityAttributesApi usersRolesIdentityAttributesApi(RestClient.Builder restClientBuilder) { + return buildExchange( + properties.usersRoles().url().resolve(V1_PREFIX), restClientBuilder, IdentityAttributesApi.class); } - private <E> E buildExchange(String baseurl, RestClient.Builder restClientBuilder, Class<E> clazz) { + private static <E> E buildExchange(URI baseurl, RestClient.Builder restClientBuilder, Class<E> clazz) { var restClient = restClientBuilder.baseUrl(baseurl).build(); var adapter = RestClientAdapter.create(restClient); var factory = HttpServiceProxyFactory.builderFor(adapter).build(); diff --git a/src/main/java/eu/europa/ec/simpl/authenticationprovider/configurations/MicroserviceProperties.java b/src/main/java/eu/europa/ec/simpl/authenticationprovider/configurations/MicroserviceProperties.java index a826b9f86fc71504c192a055af030ceb03132f94..852d28250586b1e3061fb7f85123239f7e723653 100644 --- a/src/main/java/eu/europa/ec/simpl/authenticationprovider/configurations/MicroserviceProperties.java +++ b/src/main/java/eu/europa/ec/simpl/authenticationprovider/configurations/MicroserviceProperties.java @@ -1,8 +1,11 @@ package eu.europa.ec.simpl.authenticationprovider.configurations; +import java.net.URI; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "microservice") -public record MicroserviceProperties(UsersRoles usersRoles) { - public record UsersRoles(String url) {} +public record MicroserviceProperties(UsersRoles usersRoles, IdentityProvider identityProvider) { + public record UsersRoles(URI url) {} + + public record IdentityProvider(URI url) {} } diff --git a/src/main/java/eu/europa/ec/simpl/authenticationprovider/exchanges/PublicKeyExchange.java b/src/main/java/eu/europa/ec/simpl/authenticationprovider/exchanges/PublicKeyExchange.java new file mode 100644 index 0000000000000000000000000000000000000000..35aa8c189b9e350cf59c174b54bde9e9f6f72241 --- /dev/null +++ b/src/main/java/eu/europa/ec/simpl/authenticationprovider/exchanges/PublicKeyExchange.java @@ -0,0 +1,16 @@ +package eu.europa.ec.simpl.authenticationprovider.exchanges; + +import eu.europa.ec.simpl.common.constants.SimplHeaders; +import eu.europa.ec.simpl.common.model.dto.identityprovider.ParticipantDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PatchExchange; + +@HttpExchange +public interface PublicKeyExchange { + @PatchExchange(value = "/mtls/public-key", contentType = MediaType.TEXT_PLAIN_VALUE) + ParticipantDTO sendTierOnePublicKey( + @RequestHeader(SimplHeaders.CREDENTIAL_ID) String credentialId, @RequestBody String tierOnePublicKey); +} diff --git a/src/main/java/eu/europa/ec/simpl/authenticationprovider/mappers/ParticipantMapperV1.java b/src/main/java/eu/europa/ec/simpl/authenticationprovider/mappers/ParticipantMapperV1.java new file mode 100644 index 0000000000000000000000000000000000000000..fa70907a4735f78c8156afa36d05efce2eff2517 --- /dev/null +++ b/src/main/java/eu/europa/ec/simpl/authenticationprovider/mappers/ParticipantMapperV1.java @@ -0,0 +1,9 @@ +package eu.europa.ec.simpl.authenticationprovider.mappers; + +import eu.europa.ec.simpl.api.identityprovider.v1.model.ParticipantDTO; +import org.mapstruct.Mapper; + +@Mapper +public interface ParticipantMapperV1 { + ParticipantDTO toV1(eu.europa.ec.simpl.common.model.dto.identityprovider.ParticipantDTO dtoV0); +} diff --git a/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AbstractCredentialUpdateEventListener.java b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AbstractCredentialUpdateEventListener.java new file mode 100644 index 0000000000000000000000000000000000000000..ade64e88cdbcb0b7c6e59b5cfd9e57378a7c3f4e --- /dev/null +++ b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AbstractCredentialUpdateEventListener.java @@ -0,0 +1,49 @@ +package eu.europa.ec.simpl.authenticationprovider.services.impl; + +import eu.europa.ec.simpl.api.identityprovider.v1.model.ParticipantDTO; +import eu.europa.ec.simpl.api.usersroles.v1.exchanges.PublicKeysApi; +import eu.europa.ec.simpl.authenticationprovider.configurations.mtls.MtlsClientProperties; +import eu.europa.ec.simpl.authenticationprovider.entities.Credential; +import eu.europa.ec.simpl.authenticationprovider.event.InvalidateMtls; +import eu.europa.ec.simpl.authenticationprovider.services.CredentialService; +import eu.europa.ec.simpl.common.security.JwtService; +import org.springframework.context.event.EventListener; + +public abstract class AbstractCredentialUpdateEventListener { + + private final JwtService jwtService; + private final PublicKeysApi publicKeysApi; + protected final MtlsClientProperties mtlsClientProperties; + protected final CredentialService credentialService; + + protected AbstractCredentialUpdateEventListener( + JwtService jwtService, + MtlsClientProperties mtlsClientProperties, + CredentialService credentialService, + PublicKeysApi publicKeysApi) { + this.jwtService = jwtService; + this.publicKeysApi = publicKeysApi; + this.mtlsClientProperties = mtlsClientProperties; + this.credentialService = credentialService; + } + + @EventListener(InvalidateMtls.class) + public void handleInvalidateCredential(InvalidateMtls event) { + if (event.getNewCredential() != null) { + var participantInfo = + sendKeycloakPublicKeyToAuthority(event.getNewCredential().getContent()); + storeParticipantInfo(event.getNewCredential(), participantInfo); + } + } + + protected abstract ParticipantDTO sendKeycloakPublicKeyToAuthority(byte[] content); + + protected String getKeycloakPublicKey() { + var kid = jwtService.getKid(); + return publicKeysApi.getPublicKeyByKid(kid).getPublicKey(); + } + + private static void storeParticipantInfo(Credential newCredential, ParticipantDTO participantDTO) { + newCredential.setParticipantId(participantDTO.getId()); + } +} diff --git a/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AgentServiceImpl.java b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AgentServiceImpl.java index 4b7a85306d08e75d6d79cb67763e2b92373c87d6..60d3434d52da93b1a4f31abc9b3c3b38884926c4 100644 --- a/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AgentServiceImpl.java +++ b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AgentServiceImpl.java @@ -17,7 +17,8 @@ import feign.RetryableException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; -import java.util.*; +import java.util.List; +import java.util.Objects; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; @@ -26,6 +27,8 @@ import org.springframework.web.util.UriComponentsBuilder; @Log4j2 public class AgentServiceImpl implements AgentService { + private static final String HTTPS_VALUE = "https"; + private final AuthorityExchange authorityClient; private final IdentityAttributeService identityAttributeService; private final JwtService jwtService; @@ -96,25 +99,32 @@ public class AgentServiceImpl implements AgentService { return mtlsClientBuilder.buildParticipantClient(toUrl(fqdn)).ping(); } - private String toUrl(String fqdn) { + private static String toUrl(String fqdn) { try { var uri = new URI(fqdn); if (uri.isAbsolute()) { - - if (!uri.getScheme().equals("https")) throw new InvalidFqdnException(fqdn); - + if (!Objects.equals(HTTPS_VALUE, uri.getScheme())) { + throw new InvalidFqdnException(fqdn); + } return uri.toURL().toString(); } - return UriComponentsBuilder.newInstance().scheme("https").host(fqdn).toUriString(); + return UriComponentsBuilder.newInstance() + .scheme(HTTPS_VALUE) + .host(fqdn) + .toUriString(); } catch (URISyntaxException | MalformedURLException e) { + log.error("Failed to convert fqdn in url", e); throw new InvalidFqdnException(fqdn); } } @Override public String requestEphemeralProofFromAuthority() { - var ephemeralProof = authorityClient.token(); - ephemeralProofAdapter.storeEphemeralProof(ephemeralProof); - return ephemeralProof; + return ephemeralProofAdapter.loadEphemeralProof().orElseGet(() -> { + log.info("Ephemeral proof from authority not found in cache, starting request to authority"); + var ephemeralProof = authorityClient.token(); + ephemeralProofAdapter.storeEphemeralProof(ephemeralProof); + return ephemeralProof; + }); } } diff --git a/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AuthorityCredentialUpdateEventListener.java b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AuthorityCredentialUpdateEventListener.java new file mode 100644 index 0000000000000000000000000000000000000000..6d0eadb25a15d604b6830a230356e1d9a17f729d --- /dev/null +++ b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AuthorityCredentialUpdateEventListener.java @@ -0,0 +1,33 @@ +package eu.europa.ec.simpl.authenticationprovider.services.impl; + +import eu.europa.ec.simpl.api.identityprovider.v1.exchanges.MtlsApi; +import eu.europa.ec.simpl.api.identityprovider.v1.model.ParticipantDTO; +import eu.europa.ec.simpl.api.usersroles.v1.exchanges.PublicKeysApi; +import eu.europa.ec.simpl.authenticationprovider.configurations.mtls.MtlsClientProperties; +import eu.europa.ec.simpl.authenticationprovider.services.CredentialService; +import eu.europa.ec.simpl.common.annotations.Authority; +import eu.europa.ec.simpl.common.security.JwtService; +import org.springframework.stereotype.Component; + +@Component +@Authority +public class AuthorityCredentialUpdateEventListener extends AbstractCredentialUpdateEventListener { + + private final MtlsApi tierOnePublicKeyApi; + + public AuthorityCredentialUpdateEventListener( + MtlsApi tierOnePublicKeyApi, + PublicKeysApi publicKeyApi, + JwtService jwtService, + MtlsClientProperties mtlsClientProperties, + CredentialService credentialService) { + super(jwtService, mtlsClientProperties, credentialService, publicKeyApi); + this.tierOnePublicKeyApi = tierOnePublicKeyApi; + } + + @Override + protected ParticipantDTO sendKeycloakPublicKeyToAuthority(byte[] content) { + var credentialId = credentialService.getCredentialId(content); + return tierOnePublicKeyApi.sendTierOnePublicKey(credentialId, getKeycloakPublicKey()); + } +} diff --git a/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/EphemeralProofAdapterImpl.java b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/EphemeralProofAdapterImpl.java index 04a130ccca1de83e5933dcb6df541b96cb4ff3b7..4136cd398fd365fbb8d50adb07b67f949b1129b6 100644 --- a/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/EphemeralProofAdapterImpl.java +++ b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/EphemeralProofAdapterImpl.java @@ -38,6 +38,7 @@ public class EphemeralProofAdapterImpl implements EphemeralProofAdapter { @Override public Optional<String> loadEphemeralProof() { + log.info("Start loading Ephemeral proof from cache..."); var credentialId = credentialService.getCredentialId(); return ephemeralProofRepository.findById(credentialId).map(EphemeralProof::getContent); } @@ -53,7 +54,7 @@ public class EphemeralProofAdapterImpl implements EphemeralProofAdapter { identityAttributeService.updateAssignedIdentityAttributes(parser.getIdentityAttributes()); } - private Duration getTimeToLive(Instant expiration) { + private static Duration getTimeToLive(Instant expiration) { return Duration.between(Instant.now(), expiration); } } diff --git a/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/ParticipantCredentialUpdateEventListener.java b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/ParticipantCredentialUpdateEventListener.java new file mode 100644 index 0000000000000000000000000000000000000000..a1af312bbbeac1735445b571b805b40f55ea4b82 --- /dev/null +++ b/src/main/java/eu/europa/ec/simpl/authenticationprovider/services/impl/ParticipantCredentialUpdateEventListener.java @@ -0,0 +1,53 @@ +package eu.europa.ec.simpl.authenticationprovider.services.impl; + +import eu.europa.ec.simpl.api.identityprovider.v1.model.ParticipantDTO; +import eu.europa.ec.simpl.api.usersroles.v1.exchanges.PublicKeysApi; +import eu.europa.ec.simpl.authenticationprovider.configurations.mtls.MtlsClientFactory; +import eu.europa.ec.simpl.authenticationprovider.configurations.mtls.MtlsClientProperties; +import eu.europa.ec.simpl.authenticationprovider.mappers.ParticipantMapperV1; +import eu.europa.ec.simpl.authenticationprovider.services.CredentialService; +import eu.europa.ec.simpl.authenticationprovider.services.KeyPairService; +import eu.europa.ec.simpl.common.annotations.Participant; +import eu.europa.ec.simpl.common.security.JwtService; +import eu.europa.ec.simpl.common.utils.CredentialUtil; +import java.io.ByteArrayInputStream; +import java.security.PrivateKey; +import lombok.SneakyThrows; +import org.springframework.stereotype.Component; + +@Component +@Participant +public class ParticipantCredentialUpdateEventListener extends AbstractCredentialUpdateEventListener { + + private final MtlsClientFactory mtlsClientFactory; + private final KeyPairService keyPairService; + private final ParticipantMapperV1 participantMapperV1; + + public ParticipantCredentialUpdateEventListener( + MtlsClientProperties mtlsClientProperties, + MtlsClientFactory mtlsClientFactory, + PublicKeysApi publicKeysApi, + JwtService jwtService, + KeyPairService keyPairService, + CredentialService credentialService, + ParticipantMapperV1 participantMapperV1) { + super(jwtService, mtlsClientProperties, credentialService, publicKeysApi); + this.mtlsClientFactory = mtlsClientFactory; + this.keyPairService = keyPairService; + this.participantMapperV1 = participantMapperV1; + } + + @SneakyThrows + @Override + protected ParticipantDTO sendKeycloakPublicKeyToAuthority(byte[] content) { + var keyStore = CredentialUtil.loadCredential(new ByteArrayInputStream(content), getPrivateKey()); + var mtlsClient = mtlsClientFactory.buildAuthorityClient( + mtlsClientProperties.authority().url(), keyStore); + return participantMapperV1.toV1(mtlsClient.sendTierOnePublicKey(getKeycloakPublicKey())); + } + + public PrivateKey getPrivateKey() { + var privateKey = keyPairService.getInstalledKeyPair().getPrivateKey(); + return CredentialUtil.loadPrivateKey(privateKey, "EC"); + } +} diff --git a/src/main/resources/application-authority.yml b/src/main/resources/application-authority.yml new file mode 100644 index 0000000000000000000000000000000000000000..65c4c098eb50924e798e1391610e1117737881c2 --- /dev/null +++ b/src/main/resources/application-authority.yml @@ -0,0 +1,4 @@ +microservice: + identity-provider: + url: "http://localhost:8082" + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a43355c2a6193d29f9c88999b46c93fe7c914d98..8cfb3f1193d3535eaa66d1901ee981855b75696d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -51,5 +51,4 @@ crypto: client: type: FEIGN authority: - url: [CLIENT_AUTHORITY_URL] - + url: [CLIENT_AUTHORITY_URL] \ No newline at end of file diff --git a/src/test/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AgentServiceImplTest.java b/src/test/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AgentServiceImplTest.java index 5df4f071ccdcc609ce5980613394c3d6e1d98af0..f965f769b4a840ed8b74d61008f1a4eab294df7b 100644 --- a/src/test/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AgentServiceImplTest.java +++ b/src/test/java/eu/europa/ec/simpl/authenticationprovider/services/impl/AgentServiceImplTest.java @@ -6,9 +6,9 @@ import static eu.europa.ec.simpl.common.test.TestUtil.anURI; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchException; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -25,11 +25,8 @@ import eu.europa.ec.simpl.common.model.dto.securityattributesprovider.IdentityAt import eu.europa.ec.simpl.common.model.dto.securityattributesprovider.IdentityAttributeWithOwnershipDTO; import eu.europa.ec.simpl.common.security.JwtService; import feign.RetryableException; -import java.io.IOException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; import java.util.List; +import java.util.Optional; import org.instancio.Instancio; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -171,16 +168,14 @@ class AgentServiceImplTest { } @Test - void ping_withFqdn_shouldSucceed() - throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + void ping_withFqdn_shouldSucceed() { var expectedIdentityAttributesElement = a(IdentityAttributeDTO.class); var expectedIdentityAttributes = List.of(expectedIdentityAttributesElement); testPing("some.fqdn.com", expectedIdentityAttributes, false); } @Test - void ping_withUrlWithHttpsScheme_shouldSucceed() - throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException { + void ping_withUrlWithHttpsScheme_shouldSucceed() { var expectedIdentityAttributesElement = a(IdentityAttributeDTO.class); var expectedIdentityAttributes = List.of(expectedIdentityAttributesElement); testPing( @@ -218,14 +213,27 @@ class AgentServiceImplTest { assertThat(actualIdentityAttributes).isEqualTo(expectedIdentityAttributes); } + @Test + void requestEphemeralProofFromAuthority_whenIsCached_AuthorityIsNotCalled() { + var junitProof = "junit-prooof"; + given(ephemeralProofAdapter.loadEphemeralProof()).willReturn(Optional.of(junitProof)); + + var result = agentService.requestEphemeralProofFromAuthority(); + + assertThat(result).as("Ephemeral proof returned").isEqualTo(junitProof); + verify(authorityExchange, never()).token(); + verify(ephemeralProofAdapter, never()).storeEphemeralProof(junitProof); + } + @Test void requestEphemeralProofFromAuthority_whenValidAuthorityToken_storeEphemeralProf() { var junitProof = "junit-prooof"; + given(ephemeralProofAdapter.loadEphemeralProof()).willReturn(Optional.empty()); given(authorityExchange.token()).willReturn(junitProof); var result = agentService.requestEphemeralProofFromAuthority(); - assertThat(result).isEqualTo(junitProof); - verify(ephemeralProofAdapter, times(1)).storeEphemeralProof(eq(junitProof)); + assertThat(result).as("Ephemeral proof returned").isEqualTo(junitProof); + verify(ephemeralProofAdapter).storeEphemeralProof(junitProof); } }