From feda805d0542ea56cb34fa3f83048bb361c692e8 Mon Sep 17 00:00:00 2001 From: Brayan Durango Date: Wed, 29 Nov 2023 14:26:50 -0500 Subject: [PATCH] Add authorization with jwt token in entrypoint restmvc (#402) * Add authorization with jwt token in entrypoint restmvc * Fix architecture rule 2.7 and wrap log --- README.md | 20 ++-- .../entrypoints/EntryPointRestMvc.java | 11 ++ .../task/GenerateEntryPointTask.java | 12 ++ .../entry-point/rest-mvc/api.java.mustache | 6 + .../authorization-jwt.java.mustache | 109 ++++++++++++++++++ .../rest-mvc/authorization/definition.json | 10 ++ .../rest-mvc/build.gradle.mustache | 4 + 7 files changed, 162 insertions(+), 10 deletions(-) create mode 100644 src/main/resources/entry-point/rest-mvc/authorization/authorization-jwt.java.mustache create mode 100644 src/main/resources/entry-point/rest-mvc/authorization/definition.json diff --git a/README.md b/README.md index 1aa43051..a53aecfc 100644 --- a/README.md +++ b/README.md @@ -357,16 +357,16 @@ The **`generateEntryPoint | gep`** task will generate a module in Infrastructure gradle gep --type [entryPointType] ``` - | Reference for **entryPointType** | Name | Additional Options |Java | Kotlin | - |----------------------------------|----------------------------------------|------------------------------------------|-------|---------| - | generic | Empty Entry Point | --name [name] |☑| ☑ | - | restmvc | API REST (Spring Boot Starter Web) | --server [serverOption] default undertow |☑| ☑ | - | webflux | API REST (Spring Boot Starter WebFlux) | --router [true, false] default true |☑| ☑ | - | rsocket | Rsocket Controller Entry Point | |☑| ☑ | - | graphql | API GraphQL | --pathgql [name path] default /graphql |☑| ☑ | - | asynceventhandler | Async Event Handler | |☑| ☑ | - | mq | JMS MQ Client to listen messages | |☑| ☑ | - | sqs | SQS Listener | |☑| ☑ | + | Reference for **entryPointType** | Name | Additional Options |Java | Kotlin | + |----------------------------------|----------------------------------------|-----------------------------------------------------------------------|-------|---------| + | generic | Empty Entry Point | --name [name] |☑| ☑ | + | restmvc | API REST (Spring Boot Starter Web) | --server [serverOption] default undertow --authorization [true-false] |☑| ☑ | + | webflux | API REST (Spring Boot Starter WebFlux) | --router [true, false] default true |☑| ☑ | + | rsocket | Rsocket Controller Entry Point | |☑| ☑ | + | graphql | API GraphQL | --pathgql [name path] default /graphql |☑| ☑ | + | asynceventhandler | Async Event Handler | |☑| ☑ | + | mq | JMS MQ Client to listen messages | |☑| ☑ | + | sqs | SQS Listener | |☑| ☑ | Additionally, if you'll use a restmvc, you can specify the web server on which the application will run. By default, undertow. diff --git a/src/main/java/co/com/bancolombia/factory/entrypoints/EntryPointRestMvc.java b/src/main/java/co/com/bancolombia/factory/entrypoints/EntryPointRestMvc.java index a74a0814..f50e03b7 100644 --- a/src/main/java/co/com/bancolombia/factory/entrypoints/EntryPointRestMvc.java +++ b/src/main/java/co/com/bancolombia/factory/entrypoints/EntryPointRestMvc.java @@ -32,6 +32,17 @@ public void buildModule(ModuleBuilder builder) throws IOException, CleanExceptio } else { builder.appendToProperties("management.endpoints.web.exposure").put("include", "health"); } + + if (Boolean.TRUE.equals(builder.getBooleanParam("task-param-authorize"))) { + builder.setupFromTemplate("entry-point/rest-mvc/authorization"); + builder + .appendToProperties("spring.security.oauth2.resourceserver.jwt") + .put("issuer-uri", "https://idp.example.com/issuer"); + builder + .appendToProperties("spring.security.oauth2.resourceserver.jwt") + .put("client-id", "myclientid"); + builder.appendToProperties("jwt").put("json-exp-roles", "/roles"); + } builder.appendToProperties("management.endpoint.health.probes").put("enabled", true); builder .appendToProperties("cors") diff --git a/src/main/java/co/com/bancolombia/task/GenerateEntryPointTask.java b/src/main/java/co/com/bancolombia/task/GenerateEntryPointTask.java index 615b98ba..d0a6b0fb 100644 --- a/src/main/java/co/com/bancolombia/task/GenerateEntryPointTask.java +++ b/src/main/java/co/com/bancolombia/task/GenerateEntryPointTask.java @@ -20,6 +20,7 @@ public class GenerateEntryPointTask extends AbstractResolvableTypeTask { private BooleanOption router = BooleanOption.TRUE; private BooleanOption swagger = BooleanOption.FALSE; private BooleanOption eda = BooleanOption.FALSE; + private BooleanOption authorization = BooleanOption.FALSE; @Option( option = "server", @@ -43,6 +44,11 @@ public void setPathGraphql(String pathgql) { this.pathGraphql = pathgql; } + @Option(option = "authorization", description = "Enable authorization requests through a JWT") + public void setAuthorization(BooleanOption authorization) { + this.authorization = authorization; + } + @Option(option = "eda", description = "Use EDA variant") public void setEda(BooleanOption eda) { this.eda = eda; @@ -68,11 +74,17 @@ public List getSwaggerOptions() { return Arrays.asList(BooleanOption.values()); } + @OptionValues("authorization") + public List getAuthorizeOptions() { + return Arrays.asList(BooleanOption.values()); + } + @Override protected void prepareParams() { builder.addParam("task-param-server", server); builder.addParam("task-param-pathgql", pathGraphql); builder.addParam("task-param-router", router == BooleanOption.TRUE); + builder.addParam("task-param-authorize", authorization == BooleanOption.TRUE); builder.addParam("include-swagger", swagger == BooleanOption.TRUE); builder.addParam("eda", eda == BooleanOption.TRUE); } diff --git a/src/main/resources/entry-point/rest-mvc/api.java.mustache b/src/main/resources/entry-point/rest-mvc/api.java.mustache index c356345f..ebf6bd4e 100644 --- a/src/main/resources/entry-point/rest-mvc/api.java.mustache +++ b/src/main/resources/entry-point/rest-mvc/api.java.mustache @@ -6,6 +6,9 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +{{#task-param-authorize}} +import org.springframework.security.access.prepost.PreAuthorize; +{{/task-param-authorize}} @RestController @RequestMapping(value = "/api", produces = MediaType.APPLICATION_JSON_VALUE) @@ -21,6 +24,9 @@ public class ApiRest { //} {{/lombok}} +{{#task-param-authorize}} + @PreAuthorize("hasRole('permission')") +{{/task-param-authorize}} @GetMapping(path = "/path") public String commandName() { // return useCase.doAction(); diff --git a/src/main/resources/entry-point/rest-mvc/authorization/authorization-jwt.java.mustache b/src/main/resources/entry-point/rest-mvc/authorization/authorization-jwt.java.mustache new file mode 100644 index 00000000..9a2a7b1a --- /dev/null +++ b/src/main/resources/entry-point/rest-mvc/authorization/authorization-jwt.java.mustache @@ -0,0 +1,109 @@ +package {{package}}.api.config; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.web.SecurityFilterChain; +{{^lombok}} +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +{{/lombok}} +{{#lombok}} +import lombok.extern.log4j.Log4j2; +{{/lombok}} + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +{{#lombok}} +@Log4j2 +{{/lombok}} +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) +public class AuthorizationJwt { + + {{^lombok}} + private static final Logger log = LogManager.getLogger(AuthorizationJwt.class); + {{/lombok}} + private final String issuerUri; + private final String clientId; + private final String jsonExpRoles; + private final ObjectMapper mapper; + + private static final String ROLE = "ROLE_"; + private static final String AZP = "azp"; + + public AuthorizationJwt(@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuerUri, + @Value("${spring.security.oauth2.resourceserver.jwt.client-id}") String clientId, + @Value("${jwt.json-exp-roles}") String jsonExpRoles, + ObjectMapper mapper) { + this.issuerUri = issuerUri; + this.clientId = clientId; + this.jsonExpRoles = jsonExpRoles; + this.mapper = mapper; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) + .oauth2ResourceServer(oauth2 -> + oauth2.jwt(jwtSpec -> + jwtSpec + .decoder(jwtDecoder()) + .jwtAuthenticationConverter(grantedAuthoritiesExtractor()))) + .build(); + } + + public JwtDecoder jwtDecoder(){ + var defaultValidator = JwtValidators.createDefaultWithIssuer(issuerUri); + var audienceValidator = new JwtClaimValidator(AZP, + azp -> azp != null && !azp.isEmpty() && azp.contains(clientId)); + var tokenValidator = new DelegatingOAuth2TokenValidator<>(defaultValidator, audienceValidator); + var jwtDecoder = NimbusJwtDecoder + .withIssuerLocation(issuerUri) + .build(); + jwtDecoder.setJwtValidator(tokenValidator); + return jwtDecoder; + } + + public JwtAuthenticationConverter grantedAuthoritiesExtractor(){ + var jwtConverter = new JwtAuthenticationConverter(); + jwtConverter.setJwtGrantedAuthoritiesConverter(jwt -> + getRoles(jwt.getClaims(), jsonExpRoles) + .stream() + .map(ROLE::concat) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()) + ); + return jwtConverter; + } + + private List getRoles(Map claims, String jsonExpClaim){ + List roles = List.of(); + try { + var json = mapper.writeValueAsString(claims); + var chunk = mapper.readTree(json) + .at(jsonExpClaim); + return mapper.readerFor(new TypeReference>() {}) + .readValue(chunk); + } catch (IOException e) { + log.error(e.getMessage()); + return roles; + } + } +} \ No newline at end of file diff --git a/src/main/resources/entry-point/rest-mvc/authorization/definition.json b/src/main/resources/entry-point/rest-mvc/authorization/definition.json new file mode 100644 index 00000000..f31d731f --- /dev/null +++ b/src/main/resources/entry-point/rest-mvc/authorization/definition.json @@ -0,0 +1,10 @@ +{ + "folders": [ + "infrastructure/entry-points/api-rest/src/test/{{language}}/{{packagePath}}/api/config" + ], + "files": {}, + "java": { + "entry-point/rest-mvc/authorization/authorization-jwt.java.mustache": "infrastructure/entry-points/api-rest/src/main/{{language}}/{{packagePath}}/api/config/AuthorizationJwt.java" + }, + "kotlin": {} +} diff --git a/src/main/resources/entry-point/rest-mvc/build.gradle.mustache b/src/main/resources/entry-point/rest-mvc/build.gradle.mustache index 644f709d..2f26bdb8 100644 --- a/src/main/resources/entry-point/rest-mvc/build.gradle.mustache +++ b/src/main/resources/entry-point/rest-mvc/build.gradle.mustache @@ -9,4 +9,8 @@ dependencies { {{#include-swagger}} implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:{{springdocopenapiVersion}}' {{/include-swagger}} +{{#task-param-authorize}} + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-security' +{{/task-param-authorize}} }