Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
dce36cf
fix(backend): adapt endpoints to oidc4vci (#111)
albertrodriguezin2 Jun 10, 2025
6bd78c4
build(gradle): update version
albertrodriguezin2 Jun 13, 2025
2b0b206
fix(workflow): remove unused if
albertrodriguezin2 Jun 13, 2025
d1d3042
build(gradle): update version
albertrodriguezin2 Jun 16, 2025
8080734
build(gradle): update version
albertrodriguezin2 Jun 16, 2025
48af78c
fix(shared): add new sad call in remote signature service
albertrodriguezin2 Jun 16, 2025
514579e
feat(workflow): lear credential machine
albertrodriguezin2 Jun 18, 2025
c881a88
fix(service): change remote signature
albertrodriguezin2 Jun 19, 2025
418b942
fix(service): change learcredentialmachine verifications
albertrodriguezin2 Jun 19, 2025
72f733b
fix(service): change media type on credentials authorize call
albertrodriguezin2 Jun 20, 2025
00a43e6
build(gradle): update version
albertrodriguezin2 Jun 23, 2025
87056fa
Merge branch 'feature/add-sign-access-request' into feature/2.x+sad+l…
albertrodriguezin2 Jun 23, 2025
5d8583c
test(controller): global exception handler
albertrodriguezin2 Jun 23, 2025
ebac08f
Merge branch 'feature/2.x-lear-credential-machine' into feature/2.x+s…
albertrodriguezin2 Jun 23, 2025
12e7e2d
test(controller): global exception handler
albertrodriguezin2 Jun 23, 2025
3c798a6
Merge branch 'feature/add-sign-access-request' into feature/2.x+sad+l…
albertrodriguezin2 Jun 23, 2025
0ef564a
test(util): lear credential machine factory
albertrodriguezin2 Jun 23, 2025
a84b9a2
Merge branch 'feature/2.x-lear-credential-machine' into feature/2.x+s…
albertrodriguezin2 Jun 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ 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.7.1](https://github.com/in2workspace/in2-issuer-api/releases/tag/v1.7.1)Add commentMore actions
### Fixed
- Assign always default config. email to Issuer.
## [v2.0.2](https://github.com/in2workspace/in2-issuer-api/releases/tag/v2.0.2)
### Added
- Add LEARCredentialMachine issuance.

## [v2.0.1](https://github.com/in2workspace/in2-issuer-api/releases/tag/v2.0.1)
### Added
- Add sign access request.

## [v2.0.0](https://github.com/in2workspace/in2-issuer-api/releases/tag/v2.0.0)
### Added
- Adapt endpoints to oid4vci.

## [v1.7.0](https://github.com/in2workspace/in2-issuer-api/releases/tag/v1.7.0)
### Added
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ plugins {
}

group = 'es.in2'
version = '1.7.1'
version = '2.0.2'

java {
sourceCompatibility = '17'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ private Constants() {
public static final String SIGNATURE_REMOTE_SCOPE_SERVICE = "service";
public static final String SIGNATURE_REMOTE_SCOPE_CREDENTIAL = "credential";
public static final String CREDENTIAL_ID = "credentialID";
public static final String NUM_SIGNATURES = "numSignatures";
public static final String AUTH_DATA = "authData";
public static final String AUTH_DATA_ID = "id";
public static final String AUTH_DATA_VALUE = "value";
public static final String CREDENTIAL_ACTIVATION_EMAIL_SUBJECT = "Activate your new credential";
public static final String ERROR_LOG_FORMAT = "[Error Instance ID: {}] Path: {}, Status: {}, Title: {}, Message: {}";
// ERROR MESSAGES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ private CredentialResponseErrorCodes() {
public static final String FORMAT_IS_NOT_SUPPORTED = "format_is_not_supported";
public static final String INSUFFICIENT_PERMISSION = "insufficient_permission";
public static final String MISSING_HEADER = "missing_header";
public static final String SAD_ERROR = "SAD_ERROR";

}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public Mono<ResponseEntity<CredentialErrorResponse>> handleExpiredCacheException

return Mono.just(ResponseEntity.badRequest().body(errorResponse));
}

@ExceptionHandler(ExpiredPreAuthorizedCodeException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Mono<ResponseEntity<CredentialErrorResponse>> expiredPreAuthorizedCode(Exception ex) {
Expand Down Expand Up @@ -104,7 +105,7 @@ public Mono<ResponseEntity<CredentialErrorResponse>> handleInvalidToken(Exceptio
CredentialErrorResponse errorResponse = new CredentialErrorResponse(
CredentialResponseErrorCodes.INVALID_TOKEN,
description
);
);

return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse));
}
Expand Down Expand Up @@ -172,31 +173,37 @@ public Mono<ResponseEntity<Void>> handleSignedDataParsingException(AuthenticSour
log.error(ex.getMessage());
return Mono.just(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}

@ExceptionHandler(ParseCredentialJsonException.class)
public Mono<ResponseEntity<Void>> handleParseCredentialJsonException(ParseCredentialJsonException ex) {
log.error(ex.getMessage());
return Mono.just(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}

@ExceptionHandler(TemplateReadException.class)
public Mono<ResponseEntity<Void>> handleTemplateReadException(TemplateReadException ex) {
log.error(ex.getMessage());
return Mono.just(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}

@ExceptionHandler(ProofValidationException.class)
public Mono<ResponseEntity<Void>> handleProofValidationException(ProofValidationException ex) {
log.error(ex.getMessage());
return Mono.just(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}

@ExceptionHandler(NoCredentialFoundException.class)
public Mono<ResponseEntity<Void>> handleNoCredentialFoundException(NoCredentialFoundException ex) {
log.error(ex.getMessage());
return Mono.just(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}

@ExceptionHandler(PreAuthorizationCodeGetException.class)
public Mono<ResponseEntity<Void>> handlePreAuthorizationCodeGetException(PreAuthorizationCodeGetException ex) {
log.error(ex.getMessage());
return Mono.just(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
}

@ExceptionHandler(CredentialOfferNotFoundException.class)
public Mono<ResponseEntity<Void>> handleCustomCredentialOfferNotFoundException(CredentialOfferNotFoundException ex) {
log.error(ex.getMessage());
Expand Down Expand Up @@ -284,8 +291,13 @@ public Mono<ResponseEntity<CredentialErrorResponse>> handleInsufficientPermissio
}

@ExceptionHandler(UnauthorizedRoleException.class)
public ResponseEntity<String> handleUnauthorizedRoleException(UnauthorizedRoleException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage());
public Mono<es.in2.issuer.backend.shared.domain.model.dto.GlobalErrorMessage> handleUnauthorizedRoleException(UnauthorizedRoleException ex) {
return Mono.just(
es.in2.issuer.backend.shared.domain.model.dto.GlobalErrorMessage.builder()
.status(HttpStatus.UNAUTHORIZED.value())
.message(ex.getMessage())
.error("UnauthorizedRoleException")
.build());
}

@ExceptionHandler(EmailCommunicationException.class)
Expand Down Expand Up @@ -407,4 +419,12 @@ public Mono<ResponseEntity<GlobalErrorMessage>> handleInvalidSignatureConfigurat

return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response));
}

@ExceptionHandler(SadException.class)
@ResponseStatus(HttpStatus.BAD_GATEWAY)
public Mono<CredentialErrorResponse> handleSadError(Exception ex) {
return Mono.just(new CredentialErrorResponse(
CredentialResponseErrorCodes.SAD_ERROR,
ex.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package es.in2.issuer.backend.backoffice.infrastructure.controller;

import es.in2.issuer.backend.shared.application.workflow.CredentialIssuanceWorkflow;
import es.in2.issuer.backend.shared.domain.model.dto.PreSubmittedCredentialRequest;
import es.in2.issuer.backend.shared.domain.model.dto.PreSubmittedCredentialDataRequest;
import es.in2.issuer.backend.shared.domain.service.AccessTokenService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

Expand All @@ -24,20 +25,23 @@ public class IssuanceController {
@PostMapping("/backoffice/v1/issuances")
@ResponseStatus(HttpStatus.CREATED)
public Mono<Void> internalIssueCredential(@RequestHeader(HttpHeaders.AUTHORIZATION) String bearerToken,
@RequestBody PreSubmittedCredentialRequest preSubmittedCredentialRequest) {
@RequestBody PreSubmittedCredentialDataRequest preSubmittedCredentialDataRequest) {
String processId = UUID.randomUUID().toString();
return accessTokenService.getCleanBearerToken(bearerToken).flatMap(
token -> credentialIssuanceWorkflow.execute(processId, preSubmittedCredentialRequest, token, null));
token -> credentialIssuanceWorkflow.execute(processId, preSubmittedCredentialDataRequest, token, null));
}

@PostMapping("/vci/v1/issuances")
@PostMapping(
value = "/vci/v1/issuances",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.CREATED)
public Mono<Void> externalIssueCredential(@RequestHeader(HttpHeaders.AUTHORIZATION) String bearerToken,
@RequestHeader(name = "X-Id-Token", required = false) String idToken,
@RequestBody PreSubmittedCredentialRequest preSubmittedCredentialRequest) {
@RequestBody PreSubmittedCredentialDataRequest preSubmittedCredentialDataRequest) {
String processId = UUID.randomUUID().toString();
return accessTokenService.getCleanBearerToken(bearerToken).flatMap(
token -> credentialIssuanceWorkflow.execute(processId, preSubmittedCredentialRequest, token, idToken));
token -> credentialIssuanceWorkflow.execute(processId, preSubmittedCredentialDataRequest, token, idToken));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

@Builder
public record AuthorizationServerMetadata(
@JsonProperty("issuer") String issuer,
@JsonProperty("token_endpoint") String tokenEndpoint,
@JsonProperty("response_types_supported") Set<String> responseTypesSupported,
@JsonProperty("pre-authorized_grant_anonymous_access_supported") boolean preAuthorizedGrantAnonymousAccessSupported
@JsonProperty(value = "issuer", required = true) String issuer,
@JsonProperty(value = "token_endpoint", required = true) String tokenEndpoint,
@JsonProperty(value = "response_types_supported", required = true) Set<String> responseTypesSupported,
@JsonProperty(value = "pre-authorized_grant_anonymous_access_supported", required = true) boolean preAuthorizedGrantAnonymousAccessSupported
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

@Builder
public record CredentialIssuerMetadata(
@JsonProperty("credential_issuer") String credentialIssuer,
@JsonProperty("issuance_endpoint") String issuanceEndpoint,
@JsonProperty("credential_endpoint") String credentialEndpoint,
@JsonProperty(value = "credential_issuer", required = true) String credentialIssuer,
@JsonProperty(value = "issuance_endpoint", required = true) String issuanceEndpoint,
@JsonProperty(value = "credential_endpoint", required = true) String credentialEndpoint,
@JsonProperty("deferred_credential_endpoint") String deferredCredentialEndpoint,
@JsonProperty("credential_configurations_supported") Map<String, CredentialConfiguration> credentialConfigurationsSupported
@JsonProperty(value = "credential_configurations_supported", required = true) Map<String, CredentialConfiguration> credentialConfigurationsSupported
) {

@Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@

@Builder
public record TokenResponse(
@JsonProperty("access_token") String accessToken,
@JsonProperty("token_type") String tokenType,
@JsonProperty("expires_in") long expiresIn,
@JsonProperty("c_nonce") String nonce,
@JsonProperty("c_nonce_expires_in") Long nonceExpiresIn) {
@JsonProperty(value = "access_token", required = true) String accessToken,
@JsonProperty(value = "token_type", required = true) String tokenType,
@JsonProperty(value = "expires_in", required = true) long expiresIn,
@JsonProperty(value = "refresh_token", required = true) String refreshToken) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;

import static es.in2.issuer.backend.oidc4vci.domain.util.Constants.ACCESS_TOKEN_EXPIRATION_TIME_DAYS;
import static es.in2.issuer.backend.shared.domain.util.Constants.GRANT_TYPE;
import static es.in2.issuer.backend.shared.domain.util.Constants.PRE_AUTH_CODE_EXPIRY_DURATION_MINUTES;
import static es.in2.issuer.backend.shared.domain.util.Utils.generateCustomNonce;

@Slf4j
Expand Down Expand Up @@ -49,16 +47,11 @@ public Mono<TokenResponse> generateTokenResponse(
String accessToken = generateAccessToken(preAuthorizedCode, issueTimeEpochSeconds, expirationTimeEpochSeconds);
String tokenType = "bearer";
long expiresIn = expirationTimeEpochSeconds - Instant.now().getEpochSecond();
long nonceExpiresIn = (int) TimeUnit.SECONDS.convert(
PRE_AUTH_CODE_EXPIRY_DURATION_MINUTES,
TimeUnit.MINUTES);

return TokenResponse.builder()
.accessToken(accessToken)
.tokenType(tokenType)
.expiresIn(expiresIn)
.nonce(nonce)
.nonceExpiresIn(nonceExpiresIn)
.build();
}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import es.in2.issuer.backend.shared.application.workflow.CredentialIssuanceWorkflow;
import es.in2.issuer.backend.shared.domain.model.dto.CredentialRequest;
import es.in2.issuer.backend.shared.domain.model.dto.VerifiableCredentialResponse;
import es.in2.issuer.backend.shared.domain.model.dto.CredentialResponse;
import es.in2.issuer.backend.shared.domain.service.AccessTokenService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -25,7 +25,8 @@ public class CredentialController {
private final AccessTokenService accessTokenService;

@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<ResponseEntity<VerifiableCredentialResponse>> createVerifiableCredential(
@ResponseStatus(HttpStatus.OK)
public Mono<ResponseEntity<CredentialResponse>> createVerifiableCredential(
@RequestHeader(HttpHeaders.AUTHORIZATION) String authorizationHeader,
@RequestBody CredentialRequest credentialRequest) {
String processId = UUID.randomUUID().toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import es.in2.issuer.backend.shared.application.workflow.CredentialIssuanceWorkflow;
import es.in2.issuer.backend.shared.domain.model.dto.DeferredCredentialRequest;
import es.in2.issuer.backend.shared.domain.model.dto.VerifiableCredentialResponse;
import es.in2.issuer.backend.shared.domain.model.dto.CredentialResponse;
import es.in2.issuer.backend.shared.domain.model.dto.DeferredCredentialResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
Expand All @@ -23,7 +24,7 @@ public class DeferredCredentialController {

@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
public Mono<VerifiableCredentialResponse> getCredential(
public Mono<DeferredCredentialResponse> getCredential(
@RequestHeader(HttpHeaders.AUTHORIZATION) String authorizationHeader,
@RequestBody DeferredCredentialRequest deferredCredentialRequest) {
// todo: Check if the authorization header is needed here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@

public interface CredentialIssuanceWorkflow {

Mono<Void> execute(String processId, PreSubmittedCredentialRequest preSubmittedCredentialRequest, String bearerToken, String idToken);
Mono<Void> execute(String processId, PreSubmittedCredentialDataRequest preSubmittedCredentialDataRequest, String bearerToken, String idToken);

// Refactor
Mono<VerifiableCredentialResponse> generateVerifiableCredentialResponse(String processId, CredentialRequest credentialRequest, String token);
Mono<CredentialResponse> generateVerifiableCredentialResponse(String processId, CredentialRequest credentialRequest, String token);

Mono<BatchCredentialResponse> generateVerifiableCredentialBatchResponse(String username, BatchCredentialRequest batchCredentialRequest, String token);

Mono<VerifiableCredentialResponse> generateVerifiableCredentialDeferredResponse(String processId, DeferredCredentialRequest deferredCredentialRequest);
Mono<DeferredCredentialResponse> generateVerifiableCredentialDeferredResponse(String processId, DeferredCredentialRequest deferredCredentialRequest);

Mono<Void> bindAccessTokenByPreAuthorizedCode(String processId, AuthServerNonceRequest authServerNonceRequest);
}
Loading