Skip to content

Commit

Permalink
merge
Browse files Browse the repository at this point in the history
  • Loading branch information
lucaconsalvi committed Nov 21, 2024
2 parents 8b10730 + 570efbe commit 55297b2
Show file tree
Hide file tree
Showing 14 changed files with 386 additions and 15 deletions.
24 changes: 19 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,36 @@ repositories {
mavenCentral()
}

//ext {
// set('springCloudAzureVersion', "5.18.0")
//}
ext {
set('springCloudAzureVersion', "5.18.0")
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// spring security + oauth2 resource server
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.security:spring-security-oauth2-jose'

implementation("org.springframework.boot:spring-boot-starter-actuator")

// implementation 'com.azure.spring:spring-cloud-azure-starter-actuator'
implementation 'com.azure.spring:spring-cloud-azure-starter-data-cosmos'
implementation("io.swagger.core.v3:swagger-annotations:2.2.8")
implementation("org.openapitools:jackson-databind-nullable:0.2.6")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("org.springframework.boot:spring-boot-starter-validation")
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'io.projectreactor:reactor-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

dependencyManagement {
imports {
// mavenBom "com.azure.spring:spring-cloud-azure-dependencies:${springCloudAzureVersion}"
mavenBom "com.azure.spring:spring-cloud-azure-dependencies:${springCloudAzureVersion}"
}
}

Expand All @@ -66,16 +75,21 @@ openApiGenerate {
apiPackage.set("it.gov.pagopa.rtp.activator.controller.generated")
modelPackage.set("it.gov.pagopa.rtp.activator.model.generated")
modelNameSuffix.set("Dto")
generateApiTests.set(false)
generateApiDocumentation.set(false)
generateApiTests.set(false)
generateModelTests.set(false)
library.set("spring-boot")
configOptions.set([
"dateLibrary" : "java8",
"requestMappingMode" : "api_interface",
"useSpringBoot3" : "true",
"interfaceOnly" : "true",
"useTags" : "true",
"useSwaggerUI" : "false",
"reactive" : "true",
"swaggerAnnotations" : "false",
"skipDefaultInterface" : "true",
"openApiNullable" : "true",
])
typeMappings.set([
"DateTime" : "java.time.LocalDateTime",
Expand Down
4 changes: 2 additions & 2 deletions openapi/activation.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ components:
pattern: "^[ -~]{1,64}$"
minLength: 1
maxLength: 64
example: "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay"
example: "v1"

# --------------------------------------------------------------------------
# Domain specific basic types.
Expand Down Expand Up @@ -875,4 +875,4 @@ components:
scopes:
admin_rtp_activations: Admin RPT activation.
write_rtp_activations: Create, update or delete RTP activation.
read_rtp_activations: Read RTP activation.
read_rtp_activations: Read RTP activation.
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import reactor.core.publisher.Hooks;

@SpringBootApplication
public class RtpActivatorApplication {

public static void main(String[] args) {
Hooks.enableAutomaticContextPropagation();
SpringApplication.run(RtpActivatorApplication.class, args);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package it.gov.pagopa.rtp.activator.configuration;

import com.nimbusds.jwt.JWTParser;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.*;

import java.text.ParseException;
import java.util.Objects;

import static java.util.Collections.emptyMap;

public class NoSignatureJwtDecoder implements JwtDecoder {

private final OAuth2TokenValidator<Jwt> verifier = JwtValidators.createDefault();
private final MappedJwtClaimSetConverter claimMapper = MappedJwtClaimSetConverter.withDefaults(emptyMap());

@Override
public Jwt decode(String token) throws JwtException {
try {
final var parsedToken = JWTParser.parse(token);
// convert nimbus token to spring Jwt
final var convertedClaims = claimMapper.convert(parsedToken.getJWTClaimsSet().toJSONObject());

final var jwt = Jwt.withTokenValue(parsedToken.getParsedString())
.headers(headers -> headers.putAll(parsedToken.getHeader().toJSONObject()))
.claims(claims -> claims.putAll(convertedClaims))
.build();

final var validation = verifier.validate(jwt);
if (validation.hasErrors()) {
final var description = validation.getErrors().stream()
.filter(it -> Objects.nonNull(it) && !it.getDescription().isEmpty())
.map(OAuth2Error::getDescription)
.findFirst()
.orElse("Invalid jwt token");
throw new JwtValidationException(description, validation.getErrors());
}

return jwt;
} catch (ParseException e) {
throw new BadJwtException(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package it.gov.pagopa.rtp.activator.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtGrantedAuthoritiesConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;


@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(proxyTargetClass = true) // allows to use @PreAuthorize with roles
public class SecurityConfig {

@Bean
SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http,
ReactiveJwtAuthenticationConverter jwtConverter
) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.logout(ServerHttpSecurity.LogoutSpec::disable)
.authorizeExchange(it -> it
.pathMatchers("/actuator/**")
.permitAll()
.anyExchange()
.authenticated()
)
.oauth2ResourceServer(oauth2 ->
oauth2.jwt(it -> it.jwtAuthenticationConverter(jwtConverter))
)
.build();
}

@Bean
ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() {
final var authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthoritiesClaimName("groups"); // Map "groups" claim to authorities
authoritiesConverter.setAuthorityPrefix("ROLE_"); // Add "ROLE_" prefix for Spring Security

final var reactiveConverter = new ReactiveJwtAuthenticationConverter();
reactiveConverter.setJwtGrantedAuthoritiesConverter(
new ReactiveJwtGrantedAuthoritiesConverterAdapter(authoritiesConverter)
);
return reactiveConverter;
}

@Bean
ReactiveJwtDecoder jwtDecoder() {
final var decoder = new NoSignatureJwtDecoder();
return token -> Mono.fromSupplier(() -> decoder.decode(token));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
import it.gov.pagopa.rtp.activator.service.ActivationPayerService;
import reactor.core.publisher.Mono;

import org.springframework.security.access.prepost.PreAuthorize;

import java.net.URI;


import static it.gov.pagopa.rtp.activator.utils.Authorizations.verifySubjectRequest;

@RestController
@Validated
public class ActivationAPIControllerImpl implements CreateApi {
Expand All @@ -23,12 +30,16 @@ public ActivationAPIControllerImpl(ActivationPayerService activationPayerService
}

@Override
public Mono<ResponseEntity<Void>> activate(UUID requestId, String version, Mono<ActivationReqDto> activationReqDto,
ServerWebExchange exchange) {
// TODO Auto-generated method stub
@PreAuthorize("hasRole('write_rtp_activations')")
public Mono<ResponseEntity<Void>> activate(
UUID requestId,
String version,
Mono<ActivationReqDto> activationReqDto,
ServerWebExchange exchange
) {
activationPayerService.activatePayer(activationReqDto.block().getPayer().getFiscalCode(),activationReqDto.block().getPayer().getRtpSpId().toString());
throw new UnsupportedOperationException("Unimplemented method 'activate'");

}

return verifySubjectRequest(activationReqDto, it -> it.toString())
.map(request -> ResponseEntity.created(URI.create("http://localhost")).build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
public class ActivationPayerServiceImpl implements ActivationPayerService{

@Override
public ActivationDto activatePayer() {
public ActivationDto activatePayer(String payer, String fiscalCode) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'activatePayer'");
// Try to save params into db
// check the db response
// if it's ok response 200
// if the record already exists
// report 409 error
throw new UnsupportedOperationException("Unimplemented method 'activatePayer'");
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package it.gov.pagopa.rtp.activator.utils;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import reactor.core.publisher.Mono;

import java.util.function.BiPredicate;
import java.util.function.Function;

public final class Authorizations {

private Authorizations(){}

/**
* Verifies that the subject in the request matches the authenticated user's subject.
* It uses the provided {@code extractSubject} function to extract the subject from the request object,
* and compares it with the authenticated user's name.
*
* @param <T> The type of the request body.
* @param requestBody A {@link Mono} containing the request body that needs to be verified.
* @param extractSubject A function that extracts the subject (e.g., user identifier) from the request body.
* @return A {@link Mono} containing the request body if the subjects match, or an error if they don't.
*/
public static <T> Mono<T> verifySubjectRequest(Mono<T> requestBody, Function<T, String> extractSubject) {
return verifyRequestBody(requestBody, (request, auth) -> extractSubject.apply(request).equals(auth.getName()));
}

/**
* Verifies that the request body passes a custom verification function that involves the authenticated user.
* This method takes a {@link Mono} of the request body and checks the provided {@code verify} predicate to ensure
* the request meets the security requirements. If the predicate fails, an {@link AccessDeniedException} is thrown.
*
* @param <T> The type of the request body.
* @param requestBody A {@link Mono} containing the request body that needs to be verified.
* @param verify A {@link BiPredicate} that performs a custom verification on the request body and the authenticated user.
* @return A {@link Mono} containing the request body if the verification succeeds.
*/
public static <T> Mono<T> verifyRequestBody(Mono<T> requestBody, BiPredicate<T, Authentication> verify) {
return ReactiveSecurityContextHolder.getContext().flatMap(securityContext ->
requestBody.flatMap(request -> verify.test(request, securityContext.getAuthentication()) ?
Mono.just(request) :
Mono.error(new AccessDeniedException("Authenticated user doesn't have permission to perform this action."))
)
);
}

}
10 changes: 10 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
logging.level.root=INFO


spring.application.name=rtp-activator

# enable spring boot actuator health endpoint
management.endpoints.enabled-by-default=false
management.endpoints.web.exposure.include=health
management.endpoint.health.enabled=true
management.endpoint.health.probes.enabled=true

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package it.gov.pagopa.rtp.activator.configuration;

import com.nimbusds.jose.JOSEException;
import it.gov.pagopa.rtp.activator.utils.JwtUtils;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.jwt.JwtValidationException;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

class NoSignatureJwtDecoderTest {

@Test
void givenSignedTokenMustDecodeWithoutVerifySignature() throws JOSEException {
final var decoder = new NoSignatureJwtDecoder();
final var token = JwtUtils.generateToken("me", "none");
assertThat(decoder.decode(token), Matchers.notNullValue());
}

@Test
void givenExpiredTokenMustThrowError() throws JOSEException {
final var decoder = new NoSignatureJwtDecoder();
final var token = JwtUtils.generateExpiredToken("me", "none");
assertThrows(JwtValidationException.class, () -> decoder.decode(token));
}
}

Loading

0 comments on commit 55297b2

Please sign in to comment.