Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ 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).

## [v3.0.0](https://github.com/in2workspace/in2-issuer-api/releases/tag/v3.0.0)
### Refactor
- Refactor

## [v1.7.0](https://github.com/in2workspace/in2-issuer-api/releases/tag/v1.7.0)
### Added
- Added remote signature configuration.
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.0'
version = '3.0.0'

java {
sourceCompatibility = '17'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,35 +57,42 @@ private Mono<CredentialOfferUriResponse> buildCredentialOfferUriInternal(String
.flatMap(credentialIssuanceRecord ->
preAuthorizedCodeWorkflow.generatePreAuthorizedCode()
.flatMap(preAuthorizedCodeResponse ->
credentialOfferService.buildCustomCredentialOffer(
credentialIssuanceRecord.getCredentialType(),
credentialIssuanceRecordService.setPreAuthorizedCodeById(
credentialIssuanceRecord,
preAuthorizedCodeResponse.preAuthorizedCode())
.flatMap(credentialOffer ->
buildIssuanceMetadata(
preAuthorizedCodeResponse.preAuthorizedCode(),
credentialIssuanceRecord.getId().toString(),
preAuthorizedCodeResponse.txCode(),
credentialIssuanceRecord.getEmail(),
credentialOffer)
.flatMap(issuanceMetadata ->
generateCustomNonce()
.flatMap(activationCodeNonce ->
cacheStoreForIssuanceMetadata.add(activationCodeNonce, issuanceMetadata)
.then(credentialOfferService.createCredentialOfferUriResponse(activationCodeNonce))
.flatMap(credentialOfferUri ->
generateCustomNonce()
.flatMap(cActivationCodeNonce ->
cacheStoreForCActivationCode.add(cActivationCodeNonce, activationCodeNonce)
.then(buildCredentialOfferUriResponse(credentialOfferUri, cActivationCodeNonce))
)
)
)
)
.then(credentialOfferService.buildCustomCredentialOffer(
credentialIssuanceRecord.getCredentialType(),
preAuthorizedCodeResponse.preAuthorizedCode())
.flatMap(credentialOffer ->
buildIssuanceMetadata(
preAuthorizedCodeResponse.preAuthorizedCode(),
credentialIssuanceRecord.getId().toString(),
preAuthorizedCodeResponse.txCode(),
credentialIssuanceRecord.getEmail(),
credentialOffer)
.flatMap(issuanceMetadata ->
generateCustomNonce()
.flatMap(activationCodeNonce ->
cacheStoreForIssuanceMetadata.add(activationCodeNonce, issuanceMetadata)
.then(
credentialOfferService.createCredentialOfferUriResponse(activationCodeNonce)
.flatMap(credentialOfferUri ->
generateCustomNonce()
.flatMap(cActivationCodeNonce ->
cacheStoreForCActivationCode.add(cActivationCodeNonce, activationCodeNonce)
.then(buildCredentialOfferUriResponse(credentialOfferUri, cActivationCodeNonce))
)
)
)
)
)
)
)
)
);
}


private Mono<IssuanceMetadata> buildIssuanceMetadata(String preAuthorizedCode, String credentialIssuanceRecordId,
String txCode, String email, CredentialOffer credentialOffer) {
return Mono.just(IssuanceMetadata.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ 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("refresh_token") String refreshToken) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
import es.in2.issuer.backend.oidc4vci.domain.model.TokenResponse;
import reactor.core.publisher.Mono;

public interface TokenService {
public interface TokenWorkflow {
Mono<TokenResponse> generateTokenResponse(String grantType, String preAuthorizedCode, String txCode);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package es.in2.issuer.backend.oidc4vci.domain.service.impl;

import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.Payload;
import es.in2.issuer.backend.oidc4vci.domain.model.TokenResponse;
import es.in2.issuer.backend.oidc4vci.domain.service.TokenWorkflow;
import es.in2.issuer.backend.shared.domain.service.CredentialIssuanceRecordService;
import es.in2.issuer.backend.shared.domain.service.JWTService;
import es.in2.issuer.backend.shared.domain.util.JwtUtils;
import es.in2.issuer.backend.shared.infrastructure.config.AppConfig;
import es.in2.issuer.backend.shared.infrastructure.repository.CacheStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.NoSuchElementException;

import static es.in2.issuer.backend.oidc4vci.domain.util.Constants.ACCESS_TOKEN_EXPIRATION_TIME_MINUTES;
import static es.in2.issuer.backend.oidc4vci.domain.util.Constants.REFRESH_TOKEN_EXPIRATION_TIME_DAYS;
import static es.in2.issuer.backend.shared.domain.util.Constants.GRANT_TYPE;

@Slf4j
@Service
@RequiredArgsConstructor
public class TokenWorkflowImpl implements TokenWorkflow {

private final CredentialIssuanceRecordService credentialIssuanceRecordService;
private final CacheStore<String> txCodeByPreAuthorizedCodeCacheStore;
private final JWTService jwtService;
private final AppConfig appConfig;
private final JwtUtils jwtUtils;

@Override
public Mono<TokenResponse> generateTokenResponse(
String grantType,
String preAuthorizedCode,
String txCode) {

return ensureGrantTypeIsPreAuthorizedCodeAndTxCodeAreCorrect(grantType, preAuthorizedCode, txCode)
.then(buildTokenResponse(preAuthorizedCode));
}

private Mono<TokenResponse> buildTokenResponse(String preAuthorizedCode) {
Instant issueTime = Instant.now();
long issueTimeEpochSeconds = issueTime.getEpochSecond();
long accessTokenExpirationTimeEpochSeconds = generateAccessTokenExpirationTime(issueTime);
String accessToken = generateAccessToken(preAuthorizedCode, issueTimeEpochSeconds, accessTokenExpirationTimeEpochSeconds);
String tokenType = "bearer";
long expiresIn = accessTokenExpirationTimeEpochSeconds - Instant.now().getEpochSecond();
long refreshTokenExpirationTimeEpochSeconds = generateRefreshTokenExpirationTime(issueTime);
String refreshToken = generateRefreshToken(issueTimeEpochSeconds, refreshTokenExpirationTimeEpochSeconds);

return credentialIssuanceRecordService.getIdByPreAuthorizedCode(preAuthorizedCode)
.flatMap(credentialIssuanceRecordId -> {
String accessTokenJti = jwtUtils.getJti(accessToken);
String refreshTokenJti = jwtUtils.getJti(refreshToken);
return credentialIssuanceRecordService.setJtis(
credentialIssuanceRecordId,
accessTokenJti,
refreshTokenJti)
.thenReturn(TokenResponse.builder()
.accessToken(accessToken)
.tokenType(tokenType)
.expiresIn(expiresIn)
.refreshToken(refreshToken)
.build());
});
}

private String generateRefreshToken(long issueTimeEpochSeconds, long expirationTimeEpochSeconds) {
Payload payload = new Payload(Map.of(
"iss", appConfig.getIssuerBackendUrl(),
"iat", issueTimeEpochSeconds,
"exp", expirationTimeEpochSeconds
));
return jwtService.generateJWT(payload.toString());
}

private long generateAccessTokenExpirationTime(Instant issueTime) {
return issueTime.plus(
ACCESS_TOKEN_EXPIRATION_TIME_MINUTES,
ChronoUnit.MINUTES)
.getEpochSecond();
}

private long generateRefreshTokenExpirationTime(Instant issueTime) {
return issueTime.plus(
REFRESH_TOKEN_EXPIRATION_TIME_DAYS,
ChronoUnit.DAYS)
.getEpochSecond();
}

private String generateAccessToken(String preAuthorizedCode, long issueTimeEpochSeconds, long expirationTimeEpochSeconds) {
Payload payload = new Payload(Map.of(
"iss", appConfig.getIssuerBackendUrl(),
"iat", issueTimeEpochSeconds,
"exp", expirationTimeEpochSeconds,
"jti", preAuthorizedCode
));
return jwtService.generateJWT(payload.toString());
}

private Mono<Void> ensureGrantTypeIsPreAuthorizedCodeAndTxCodeAreCorrect(String grantType, String preAuthorizedCode, String txCode) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⁠creus que es pot separar la reponsabilitat de ensureGrantTypeIsPreAuthorizedCodeAndTxCodeAreCorrect()? Per un costat el grant_type i per altre el pre-authorized_code y el tx_code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Abans ho teníem per separat (a main està així) i em vas demanar que ho unís en una sola funció.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separar-ho

if (!GRANT_TYPE.equals(grantType)) {
return Mono.error(new IllegalArgumentException("Invalid grant type"));
}

return txCodeByPreAuthorizedCodeCacheStore
.get(preAuthorizedCode)
.onErrorMap(NoSuchElementException.class, ex -> new IllegalArgumentException("Invalid pre-authorized code"))
.flatMap(cacheTxCode ->
cacheTxCode.equals(txCode)
? Mono.empty()
: Mono.error(new IllegalArgumentException("Invalid tx code"))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ private Constants() {
public static final String TX_CODE_DESCRIPTION =
"A PIN has been sent to your email. Check your inbox. Enter your PIN Code.";
public static final String TX_INPUT_MODE = "numeric";
public static final long ACCESS_TOKEN_EXPIRATION_TIME_DAYS = 30L;
public static final long ACCESS_TOKEN_EXPIRATION_TIME_MINUTES = 10L;
public static final long REFRESH_TOKEN_EXPIRATION_TIME_DAYS = 30L;
}
Loading