Skip to content

Commit

Permalink
Add authorization with jwt token in entrypoint restmvc (#402)
Browse files Browse the repository at this point in the history
* Add authorization with jwt token in entrypoint restmvc

* Fix architecture rule 2.7 and wrap log
  • Loading branch information
braduran authored Nov 29, 2023
1 parent 34d9049 commit feda805
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 10 deletions.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/co/com/bancolombia/task/GenerateEntryPointTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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;
Expand All @@ -68,11 +74,17 @@ public List<BooleanOption> getSwaggerOptions() {
return Arrays.asList(BooleanOption.values());
}

@OptionValues("authorization")
public List<BooleanOption> 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);
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/entry-point/rest-mvc/api.java.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>(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<String> getRoles(Map<String, Object> claims, String jsonExpClaim){
List<String> roles = List.of();
try {
var json = mapper.writeValueAsString(claims);
var chunk = mapper.readTree(json)
.at(jsonExpClaim);
return mapper.readerFor(new TypeReference<List<String>>() {})
.readValue(chunk);
} catch (IOException e) {
log.error(e.getMessage());
return roles;
}
}
}
Original file line number Diff line number Diff line change
@@ -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": {}
}
4 changes: 4 additions & 0 deletions src/main/resources/entry-point/rest-mvc/build.gradle.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
}

0 comments on commit feda805

Please sign in to comment.