Skip to content

Commit

Permalink
Client api (#10)
Browse files Browse the repository at this point in the history
* added modules

* [client-api]: refactored client refactored security configuration
  • Loading branch information
vlaship authored Feb 2, 2024
1 parent 57fec5f commit 4d5b485
Show file tree
Hide file tree
Showing 122 changed files with 671 additions and 196 deletions.
73 changes: 39 additions & 34 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,54 @@ permissions:
contents: read

jobs:
build:

runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref != 'refs/heads/master'

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'zulu'

- name: Cache Gradle dependencies
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Change wrapper permissions
run: chmod +x ./gradlew

- name: Build with Gradle
run: ./gradlew clean build -x test --no-daemon
# gradle-docker-build:
#
# runs-on: ubuntu-latest
# if: github.event_name == 'push' && github.ref != 'refs/heads/master'
#
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Set up JDK
# uses: actions/setup-java@v4
# with:
# java-version: '21'
# distribution: 'zulu'
#
# - name: Cache Gradle dependencies
# uses: actions/cache@v3
# with:
# path: |
# ~/.gradle/caches
# ~/.gradle/wrapper
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
# restore-keys: |
# ${{ runner.os }}-gradle-
#
# - name: Change wrapper permissions
# run: chmod +x ./gradlew
#
# - name: Build with Gradle
# run: ./gradlew clean backoffice-app:unpack -x test --no-daemon
#
# - name: Build Docker image
# uses: docker/build-push-action@v3
# with:
# context: .
# file: ./unpacked.Dockerfile

docker-build:

needs: build

runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build Docker image
uses: docker/build-push-action@v3
uses: docker/build-push-action@v5
with:
no-cache: true
context: .
file: ./jvm.Dockerfile
file: ./gradle.Dockerfile
12 changes: 12 additions & 0 deletions backoffice-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
id("java")
id("org.springframework.boot")
id("io.spring.dependency-management")
id("dev.vlaship.lombok")
}

dependencies{
implementation(libs.springdoc)
implementation("jakarta.validation:jakarta.validation-api")
implementation("org.springframework.data:spring-data-commons")
}
57 changes: 57 additions & 0 deletions backoffice-app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
plugins {
id("java")
id("org.springframework.boot")
id("io.spring.dependency-management")
id("dev.vlaship.lombok")
id("dev.vlaship.unpack")
id("dev.vlaship.git-properties")
}

val jwtVersion = "0.12.3" // "0.11.5" works with graalvm
val openApiVersion = "2.3.0"
val mapstructVersion = "1.5.5.Final"
val preLiquibaseVersion = "1.5.0"
val openTelemetryVersion = "2.0.0"
val micrometerVersion = "1.2.2"
val findbugsVersion = "3.0.2"

dependencies {
implementation(project(":backoffice-api"))

// avoid warnings
compileOnly("com.google.code.findbugs:jsr305:$findbugsVersion")

// security
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.jsonwebtoken:jjwt-api:$jwtVersion")
implementation("io.jsonwebtoken:jjwt-impl:$jwtVersion")
implementation("io.jsonwebtoken:jjwt-jackson:$jwtVersion")

// web
implementation(libs.springdoc)
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")

// mapper
compileOnly("org.mapstruct:mapstruct:$mapstructVersion")
annotationProcessor("org.mapstruct:mapstruct-processor:$mapstructVersion")

// db
runtimeOnly("org.postgresql:postgresql")
implementation("org.liquibase:liquibase-core")
implementation("net.lbruun.springboot:preliquibase-spring-boot-starter:$preLiquibaseVersion")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")

// actuator
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:$openTelemetryVersion")
implementation("io.micrometer:micrometer-tracing-bridge-brave")

// test
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
}

tasks.named<Test>("test") {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.vlaship.backoffice.security;

import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.crypto.SecretKey;

@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class JwtConfig {

@Bean
public SecretKey secretKey(JwtProperties jwtProperties) {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtProperties.secret()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.vlaship.backoffice.security;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("jwt")
public record JwtProperties(
String secret,
long expirationSeconds
) {
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,44 @@
package dev.vlaship.backoffice.security;

import dev.vlaship.backoffice.exception.JwtAuthenticationException;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Qualifier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.io.IOException;
import java.util.List;

@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {

private static final String TOKEN_PREFIX = "Bearer ";

private final UserDetailsService jwtUserDetailsService;
private final JwtTokenUtil jwtTokenUtil;
private final HandlerExceptionResolver resolver;
private final List<String> whiteUrls;

public JwtRequestFilter(
UserDetailsService jwtUserDetailsService,
JwtTokenUtil jwtTokenUtil,
@Qualifier("handlerExceptionResolver")
HandlerExceptionResolver resolver,
List<String> whiteUrls
) {
this.jwtUserDetailsService = jwtUserDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.resolver = resolver;
this.whiteUrls = whiteUrls;
}

Expand All @@ -50,35 +48,32 @@ protected void doFilterInternal(
@NonNull HttpServletResponse response,
@NonNull FilterChain chain
) throws ServletException, IOException {
var skip = whiteUrls.stream()
.anyMatch(url -> request.getServletPath().contains(url));

if (skip) {
chain.doFilter(request, response);
return;
}
// var skip = whiteUrls.stream()
// .anyMatch(url -> request.getServletPath().contains(url));
//
// if (skip) {
// chain.doFilter(request, response);
// return;
// }

var jwtToken = extractToken(request);

try {
if (jwtToken != null) {
jwtTokenUtil.validateToken(jwtToken);
var username = getUsername(jwtToken);

var userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
var auth = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);

var auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}

chain.doFilter(request, response);
} catch (Exception e) {
resolver.resolveException(request, response, null, e);
} catch (JwtAuthenticationException | UsernameNotFoundException ex) {
log.debug("error validate credentials: {}", ex.getMessage());
}

chain.doFilter(request, response);
}

@Nullable
Expand All @@ -101,5 +96,4 @@ private String extractToken(@NonNull HttpServletRequest request) {
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.stereotype.Component;
import dev.vlaship.backoffice.exception.JwtAuthenticationException;

import javax.crypto.SecretKey;
import java.io.Serializable;
import java.time.Instant;
import java.util.Date;
Expand All @@ -25,6 +26,7 @@ public class JwtTokenUtil implements Serializable {
private static final MacAlgorithm ALG = Jwts.SIG.HS512;

private final JwtProperties jwtProperties;
private final SecretKey secretKey;

@NonNull
public String generateToken(Authentication authentication) {
Expand All @@ -34,20 +36,20 @@ public String generateToken(Authentication authentication) {
@NonNull
private String doGenerateToken(Map<String, Object> claims, String subject) {
var iat = Instant.now();
var exp = iat.plusSeconds(jwtProperties.getExpirationSeconds());
var exp = iat.plusSeconds(jwtProperties.expirationSeconds());
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(Date.from(iat))
.expiration(Date.from(exp))
.signWith(jwtProperties.getSecretKey(), ALG)
.signWith(secretKey, ALG)
.compact();
}

public void validateToken(@NonNull String token) {
try {
Jwts.parser()
.verifyWith(jwtProperties.getSecretKey())
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);

Expand Down Expand Up @@ -79,7 +81,7 @@ public <T> T extractClaim(@NonNull String token, @NonNull Function<Claims, T> cl
@NonNull
private Claims extractAllClaims(@NonNull String token) {
return Jwts.parser()
.verifyWith(jwtProperties.getSecretKey())
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package dev.vlaship.backoffice.security;

import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
Expand All @@ -21,14 +18,14 @@
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;

@Configuration
@EnableMethodSecurity
@EnableWebSecurity
@RequiredArgsConstructor
@EnableConfigurationProperties(JwtProperties.class)
public class SecurityConfig {

private final UserDetailsService userDetailsService;
private final JwtRequestFilter jwtRequestFilter;
private final JwtProperties jwtProperties;
private final Unauthorized401EntryPoint unauthorized401EntryPoint;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -44,6 +41,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// Add a filter to validate the tokens with every request
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(authenticationProvider())
.exceptionHandling(cfg -> cfg.authenticationEntryPoint(unauthorized401EntryPoint))
.build();
}

Expand All @@ -66,9 +64,4 @@ public AuthenticationManager authenticationManager(
) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public CommandLineRunner setSecretKey() {
return args -> jwtProperties.setSecretKey(Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtProperties.getSecret())));
}
}
Loading

0 comments on commit 4d5b485

Please sign in to comment.