From 7e97e55fea6d99b8b0adab9a569c938e83f558a1 Mon Sep 17 00:00:00 2001 From: Pieter Van Eeckhout Date: Sun, 25 Jun 2023 23:38:19 +0200 Subject: [PATCH] #39 - wip --- boot/build.gradle | 6 +- .../BootJwtAuthenticationController.java | 13 +++ .../controller/BootTradeModuleController.java | 18 ++--- boot/src/main/java/module-info.java | 7 +- build.gradle | 8 ++ settings.gradle | 1 + .../DefaultTradeModuleController.java | 19 +++++ .../controller/TradeModuleController.java | 2 +- trade-module/src/main/java/module-info.java | 1 + user-module/build.gradle | 41 ++++++++++ .../DefaultJwtAuthenticationController.java | 59 ++++++++++++++ .../dto/AuthenticationResponse.java | 12 +++ .../service/DefaultJwtTokenService.java | 81 +++++++++++++++++++ .../configuration/SecurityConfiguration.java | 38 +++++++++ .../filter/JwtRequestFilter.java | 52 ++++++++++++ .../controller/AuthenticationController.java | 35 ++++++++ .../backend/user/domain/model/ApiKey.java | 21 +++++ .../backend/user/domain/model/EdpnUser.java | 20 +++++ .../user/domain/model/PricingPlan.java | 26 ++++++ .../backend/user/domain/model/UserRole.java | 13 +++ .../domain/repository/UserRepository.java | 8 ++ .../user/domain/service/JwtTokenService.java | 29 +++++++ user-module/src/main/java/module-info.java | 21 +++++ 23 files changed, 512 insertions(+), 19 deletions(-) create mode 100644 boot/src/main/java/io/edpn/backend/application/controller/BootJwtAuthenticationController.java create mode 100644 trade-module/src/main/java/io/edpn/backend/trade/application/controller/DefaultTradeModuleController.java rename trade-module/src/main/java/io/edpn/backend/trade/{application => domain}/controller/TradeModuleController.java (87%) create mode 100644 user-module/build.gradle create mode 100644 user-module/src/main/java/io/edpn/backend/user/application/controller/DefaultJwtAuthenticationController.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/application/dto/AuthenticationResponse.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/application/service/DefaultJwtTokenService.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/configuration/SecurityConfiguration.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/configuration/filter/JwtRequestFilter.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/domain/controller/AuthenticationController.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/domain/model/ApiKey.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/domain/model/EdpnUser.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/domain/model/PricingPlan.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/domain/model/UserRole.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/domain/repository/UserRepository.java create mode 100644 user-module/src/main/java/io/edpn/backend/user/domain/service/JwtTokenService.java create mode 100644 user-module/src/main/java/module-info.java diff --git a/boot/build.gradle b/boot/build.gradle index fd4d81b8..539e5388 100644 --- a/boot/build.gradle +++ b/boot/build.gradle @@ -2,13 +2,11 @@ plugins { id 'org.springframework.boot' version '3.1.0' } -repositories { - mavenCentral() -} - dependencies { implementation project(":trade-module") + implementation project(":user-module") implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui' diff --git a/boot/src/main/java/io/edpn/backend/application/controller/BootJwtAuthenticationController.java b/boot/src/main/java/io/edpn/backend/application/controller/BootJwtAuthenticationController.java new file mode 100644 index 00000000..a57efb2f --- /dev/null +++ b/boot/src/main/java/io/edpn/backend/application/controller/BootJwtAuthenticationController.java @@ -0,0 +1,13 @@ +package io.edpn.backend.application.controller; + +import io.edpn.backend.user.application.controller.DefaultJwtAuthenticationController; +import io.edpn.backend.user.domain.service.JwtTokenService; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class BootJwtAuthenticationController extends DefaultJwtAuthenticationController { + + public BootJwtAuthenticationController(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService, JwtTokenService jwtTokenUtil) { + super(passwordEncoder, userDetailsService, jwtTokenUtil); + } +} diff --git a/boot/src/main/java/io/edpn/backend/application/controller/BootTradeModuleController.java b/boot/src/main/java/io/edpn/backend/application/controller/BootTradeModuleController.java index e181b767..420b61b2 100644 --- a/boot/src/main/java/io/edpn/backend/application/controller/BootTradeModuleController.java +++ b/boot/src/main/java/io/edpn/backend/application/controller/BootTradeModuleController.java @@ -1,22 +1,16 @@ package io.edpn.backend.application.controller; +import io.edpn.backend.trade.application.controller.DefaultTradeModuleController; import io.edpn.backend.trade.domain.service.BestCommodityPriceService; -import io.edpn.backend.trade.application.controller.TradeModuleController; -import io.edpn.backend.trade.application.dto.CommodityMarketInfoResponse; -import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - @RestController -@RequiredArgsConstructor -public class BootTradeModuleController implements TradeModuleController { - - private final BestCommodityPriceService bestCommodityPriceService; +public class BootTradeModuleController extends DefaultTradeModuleController { - @Override - public List getBestCommodityPrice() { - return bestCommodityPriceService.getCommodityMarketInfo(); + @Autowired + public BootTradeModuleController(BestCommodityPriceService bestCommodityPriceService) { + super(bestCommodityPriceService); } } diff --git a/boot/src/main/java/module-info.java b/boot/src/main/java/module-info.java index 2119db3a..4563e38c 100644 --- a/boot/src/main/java/module-info.java +++ b/boot/src/main/java/module-info.java @@ -1,12 +1,15 @@ -module edpn.boot { +module io.edpn.backend.boot { requires static lombok; requires spring.boot; requires org.mybatis.spring; requires spring.boot.autoconfigure; requires spring.context; - requires io.edpn.backend.trade; requires spring.web; requires spring.beans; + requires spring.boot.starter.security; + + requires io.edpn.backend.trade; + requires io.edpn.backend.user; opens io.edpn.backend.application.controller to spring.core; } diff --git a/build.gradle b/build.gradle index a83f73fb..33083cf9 100644 --- a/build.gradle +++ b/build.gradle @@ -80,6 +80,9 @@ subprojects { backendUtilVersion = '0.0.1-SNAPSHOT' backendMybatisUtilVersion = '0.0.1-SNAPSHOT' backendMessageProcessorLibVersion = '0.0.1-SNAPSHOT' + bucket4jCoreVersion = '7.6.0' + jjwtVersion='0.9.1' + jakartaVersion='6.0.0' } dependencies { @@ -88,7 +91,9 @@ subprojects { implementation "io.edpn.backend:backend-mybatis-util:${backendUtilVersion}" implementation "io.edpn.backend:backend-messageprocessor-lib:${backendUtilVersion}" implementation "org.springframework.boot:spring-boot-starter:${springBootVersion}" + implementation "org.springframework.boot:spring-boot-starter-security:${springBootVersion}" implementation "org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}" + implementation "org.springframework.boot:spring-boot-starter-jersey:${springBootVersion}" implementation "org.springframework.boot:spring-boot-starter-web:${springBootVersion}" implementation "org.springframework.boot:spring-boot-starter-integration:${springBootVersion}" implementation "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}" @@ -100,9 +105,12 @@ subprojects { implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation "com.github.vladimir-bukhtoyarov:bucket4j-core:${bucket4jCoreVersion}" + implementation "io.jsonwebtoken:jjwt:${jjwtVersion}" runtimeOnly "org.postgresql:postgresql:${postgresqlVersion}" runtimeOnly "io.micrometer:micrometer-registry-prometheus:${prometheusVersion}" compileOnly "org.projectlombok:lombok:${lombokVersion}" + compileOnly "jakarta.servlet:jakarta.servlet-api:${jakartaVersion}" annotationProcessor "org.projectlombok:lombok:${lombokVersion}" testCompileOnly "org.projectlombok:lombok:${lombokVersion}" testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" diff --git a/settings.gradle b/settings.gradle index 06e7e539..38f550ec 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,3 +3,4 @@ rootProject.name = 'backend' // modules include 'boot' include 'trade-module' +include 'user-module' diff --git a/trade-module/src/main/java/io/edpn/backend/trade/application/controller/DefaultTradeModuleController.java b/trade-module/src/main/java/io/edpn/backend/trade/application/controller/DefaultTradeModuleController.java new file mode 100644 index 00000000..74e1ea1d --- /dev/null +++ b/trade-module/src/main/java/io/edpn/backend/trade/application/controller/DefaultTradeModuleController.java @@ -0,0 +1,19 @@ +package io.edpn.backend.trade.application.controller; + +import io.edpn.backend.trade.application.dto.CommodityMarketInfoResponse; +import io.edpn.backend.trade.domain.controller.TradeModuleController; +import io.edpn.backend.trade.domain.service.BestCommodityPriceService; +import java.util.List; +import lombok.RequiredArgsConstructor; + + +@RequiredArgsConstructor +public class DefaultTradeModuleController implements TradeModuleController { + + private final BestCommodityPriceService bestCommodityPriceService; + + @Override + public List getBestCommodityPrice() { + return bestCommodityPriceService.getCommodityMarketInfo(); + } +} diff --git a/trade-module/src/main/java/io/edpn/backend/trade/application/controller/TradeModuleController.java b/trade-module/src/main/java/io/edpn/backend/trade/domain/controller/TradeModuleController.java similarity index 87% rename from trade-module/src/main/java/io/edpn/backend/trade/application/controller/TradeModuleController.java rename to trade-module/src/main/java/io/edpn/backend/trade/domain/controller/TradeModuleController.java index c46139a9..bdb89f77 100644 --- a/trade-module/src/main/java/io/edpn/backend/trade/application/controller/TradeModuleController.java +++ b/trade-module/src/main/java/io/edpn/backend/trade/domain/controller/TradeModuleController.java @@ -1,4 +1,4 @@ -package io.edpn.backend.trade.application.controller; +package io.edpn.backend.trade.domain.controller; import io.edpn.backend.trade.application.dto.CommodityMarketInfoResponse; import org.springframework.web.bind.annotation.GetMapping; diff --git a/trade-module/src/main/java/module-info.java b/trade-module/src/main/java/module-info.java index 466253b1..cb977216 100644 --- a/trade-module/src/main/java/module-info.java +++ b/trade-module/src/main/java/module-info.java @@ -23,4 +23,5 @@ exports io.edpn.backend.trade.application.dto; exports io.edpn.backend.trade.application.service; exports io.edpn.backend.trade.domain.service; + exports io.edpn.backend.trade.domain.controller; } diff --git a/user-module/build.gradle b/user-module/build.gradle new file mode 100644 index 00000000..39f137ff --- /dev/null +++ b/user-module/build.gradle @@ -0,0 +1,41 @@ +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter' + implementation 'org.liquibase:liquibase-core' + implementation 'io.jsonwebtoken:jjwt' + implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core' + + compileOnly 'jakarta.servlet:jakarta.servlet-api' + compileOnly 'org.projectlombok:lombok' + + annotationProcessor 'org.projectlombok:lombok' + + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' +} + +extraJavaModuleInfo { + automaticModule("dependency-management-plugin-1.1.0.jar","dependency.management.plugin") + //automaticModule("tomlj-1.0.0.jar","tomlj") + automaticModule("jsr305-3.0.2.jar","jsr305") + automaticModule("snappy-java-1.1.8.4.jar","snappy.java") + automaticModule("liquibase-core-4.17.2.jar","liquibase.core") + automaticModule("jjwt-0.9.1.jar", "io.jsonwebtoken") + /* + automaticModule("jersey-media-json-jackson-3.1.1.jar", "jersy.jackson") + automaticModule("jersey-container-servlet-3.1.1.jar", "jersy.jackson") + automaticModule("jersey-spring6-3.1.1.jar", "jersy.jackson") + automaticModule("jersey-container-servlet-core-3.1.1.jar", "jersy.jackson") + automaticModule("jersey-bean-validation-3.1.1.jar", "jersy.jackson") + automaticModule("jersey-server-3.1.1.jar", "jersy.jackson") + automaticModule("jersey-client-3.1.1.jar", "jersy.jackson") + automaticModule("jersey-hk2-3.1.1.jar", "jersy.jackson") + automaticModule("hk2-3.0.3.jar", "jersy.jackson") + automaticModule("jersey-entity-filtering-3.1.1.jar", "jersy.jackson") + automaticModule("osgi-resource-locator-1.0.3.jar", "jersy.jackson") + automaticModule("javassist-3.29.0-GA.jar", "jersy.jackson") + automaticModule("jersey-common-3.1.1.jar", "jersy.jackson")*/ + +} diff --git a/user-module/src/main/java/io/edpn/backend/user/application/controller/DefaultJwtAuthenticationController.java b/user-module/src/main/java/io/edpn/backend/user/application/controller/DefaultJwtAuthenticationController.java new file mode 100644 index 00000000..f19a6f29 --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/application/controller/DefaultJwtAuthenticationController.java @@ -0,0 +1,59 @@ +package io.edpn.backend.user.application.controller; + + +import io.edpn.backend.user.domain.controller.AuthenticationController; +import io.edpn.backend.user.domain.service.JwtTokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; + +@RequiredArgsConstructor +public class DefaultJwtAuthenticationController implements AuthenticationController { + + private final PasswordEncoder passwordEncoder; + private final UserDetailsService userDetailsService; + private final JwtTokenService jwtTokenUtil; + + @Override + public ResponseEntity createAuthenticationToken(AuthenticationRequest jsonAuthenticationRequest) throws BadCredentialsException { + try { + final UserDetails userDetails = userDetailsService + .loadUserByUsername(jsonAuthenticationRequest.getUsername()); + if (passwordEncoder.matches(jsonAuthenticationRequest.getPassword(), userDetails.getPassword())) { + throw new BadCredentialsException("Incorrect username or password"); + } + + var response = io.edpn.backend.user.application.dto.AuthenticationResponse.builder() + .jwt(jwtTokenUtil.generateToken(userDetails)) + .refreshToken(jwtTokenUtil.generateRefreshToken(userDetails)) + .build(); + + return ResponseEntity.ok(response); + } catch (UsernameNotFoundException unfe) { + throw new BadCredentialsException("Incorrect username or password", unfe); + } + } + + @Override + public ResponseEntity refreshToken(RefreshTokenRequest refreshTokenRequest) throws Exception { + String refreshToken = refreshTokenRequest.getRefreshToken(); + String username = jwtTokenUtil.extractUsername(refreshToken); + + final UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + if (jwtTokenUtil.validateToken(refreshToken, userDetails)) { + var response = io.edpn.backend.user.application.dto.AuthenticationResponse.builder() + .jwt(jwtTokenUtil.generateToken(userDetails)) + .refreshToken(refreshToken) // do not send a new refresh token, need to log in again after 24 hours + .build(); + + return ResponseEntity.ok(response); + } else { + throw new Exception("Invalid refresh token"); + } + } +} diff --git a/user-module/src/main/java/io/edpn/backend/user/application/dto/AuthenticationResponse.java b/user-module/src/main/java/io/edpn/backend/user/application/dto/AuthenticationResponse.java new file mode 100644 index 00000000..8e4c7d17 --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/application/dto/AuthenticationResponse.java @@ -0,0 +1,12 @@ +package io.edpn.backend.user.application.dto; + +import io.edpn.backend.user.domain.controller.AuthenticationController; +import lombok.Builder; +import lombok.Value; + +@Value(staticConstructor = "of") +@Builder +public class AuthenticationResponse implements AuthenticationController.AuthenticationResponse { + String jwt; + String refreshToken; +} diff --git a/user-module/src/main/java/io/edpn/backend/user/application/service/DefaultJwtTokenService.java b/user-module/src/main/java/io/edpn/backend/user/application/service/DefaultJwtTokenService.java new file mode 100644 index 00000000..2e03ef2a --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/application/service/DefaultJwtTokenService.java @@ -0,0 +1,81 @@ +package io.edpn.backend.user.application.service; + +import io.edpn.backend.user.domain.service.JwtTokenService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DefaultJwtTokenService implements JwtTokenService { + + @Value("${jwt.secret:secret}") + private String secret; + + @Override + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + @Override + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + @Override + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + @Override + public Claims extractAllClaims(String token) { + return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); + } + + @Override + public Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + @Override + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + return createToken(claims, userDetails.getUsername()); + } + + @Override + public String createToken(Map claims, String subject) { + return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 1)) // 1 hours token validity + .signWith(SignatureAlgorithm.HS512, secret).compact(); + } + + @Override + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + @Override + public String generateRefreshToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + return createRefreshToken(claims, userDetails.getUsername()); + } + + @Override + public String createRefreshToken(Map claims, String subject) { + return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 24 hours token validity + .signWith(SignatureAlgorithm.HS512, secret).compact(); + } + +} diff --git a/user-module/src/main/java/io/edpn/backend/user/configuration/SecurityConfiguration.java b/user-module/src/main/java/io/edpn/backend/user/configuration/SecurityConfiguration.java new file mode 100644 index 00000000..44fe9d9c --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/configuration/SecurityConfiguration.java @@ -0,0 +1,38 @@ +package io.edpn.backend.user.configuration; + +import io.edpn.backend.user.configuration.filter.JwtRequestFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +public class SecurityConfiguration { + + @Bean + public SecurityFilterChain endpointSecurityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authz) -> authz + .anyRequest().authenticated() + ) + .httpBasic(withDefaults()); + return http.build(); + } + + @Bean + public SecurityFilterChain jwtRequestFilterFilterChain(HttpSecurity http, JwtRequestFilter jwtRequestFilter) throws Exception { + http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + + @Bean + public PasswordEncoder getPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/user-module/src/main/java/io/edpn/backend/user/configuration/filter/JwtRequestFilter.java b/user-module/src/main/java/io/edpn/backend/user/configuration/filter/JwtRequestFilter.java new file mode 100644 index 00000000..61b0d7db --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/configuration/filter/JwtRequestFilter.java @@ -0,0 +1,52 @@ +package io.edpn.backend.user.configuration.filter; + +import io.edpn.backend.user.domain.service.JwtTokenService; + + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +public class JwtRequestFilter extends OncePerRequestFilter { + + private final UserDetailsService userDetailsService; + private final JwtTokenService jwtTokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + final String authorizationHeader = request.getHeader("Authorization"); + + String username = null; + String jwt = null; + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); + username = jwtTokenService.extractUsername(jwt); + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + if (jwtTokenService.validateToken(jwt, userDetails)) { + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + usernamePasswordAuthenticationToken + .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + } + } + chain.doFilter(request, response); + } +} diff --git a/user-module/src/main/java/io/edpn/backend/user/domain/controller/AuthenticationController.java b/user-module/src/main/java/io/edpn/backend/user/domain/controller/AuthenticationController.java new file mode 100644 index 00000000..c32954c9 --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/domain/controller/AuthenticationController.java @@ -0,0 +1,35 @@ +package io.edpn.backend.user.domain.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +public interface AuthenticationController { + + @PostMapping("/authenticate") + ResponseEntity createAuthenticationToken(@RequestBody AuthenticationRequest jsonAuthenticationRequest) throws Exception; + + @PostMapping("/refresh-token") + ResponseEntity refreshToken(RefreshTokenRequest refreshTokenRequest) throws Exception; + + interface AuthenticationResponse { + String getJwt(); + + String getRefreshToken(); + } + + interface AuthenticationRequest { + String getUsername(); + + void setUsername(String username); + + String getPassword(); + + void setPassword(String password); + } + + interface RefreshTokenRequest { + String getRefreshToken(); + + } +} diff --git a/user-module/src/main/java/io/edpn/backend/user/domain/model/ApiKey.java b/user-module/src/main/java/io/edpn/backend/user/domain/model/ApiKey.java new file mode 100644 index 00000000..dbb53bd7 --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/domain/model/ApiKey.java @@ -0,0 +1,21 @@ +package io.edpn.backend.user.domain.model; + +import java.time.LocalDateTime; +import java.util.Set; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class ApiKey { + + String prefix; + String keyHash; + String name; + Set roles; + Set grants; + LocalDateTime expiryTimestamp; + boolean enabled; + + +} diff --git a/user-module/src/main/java/io/edpn/backend/user/domain/model/EdpnUser.java b/user-module/src/main/java/io/edpn/backend/user/domain/model/EdpnUser.java new file mode 100644 index 00000000..ae69c9d8 --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/domain/model/EdpnUser.java @@ -0,0 +1,20 @@ +package io.edpn.backend.user.domain.model; + +import java.time.LocalDateTime; +import java.util.Set; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class EdpnUser { + + String email; + String password; + LocalDateTime accountExpiryTimestamp; + LocalDateTime passwordExpiryTimestamp; + boolean enabled; + boolean locked; + Set roles; + Set grants; +} diff --git a/user-module/src/main/java/io/edpn/backend/user/domain/model/PricingPlan.java b/user-module/src/main/java/io/edpn/backend/user/domain/model/PricingPlan.java new file mode 100644 index 00000000..b2cfff4c --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/domain/model/PricingPlan.java @@ -0,0 +1,26 @@ +package io.edpn.backend.user.domain.model; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Refill; +import java.time.Duration; + +public enum PricingPlan { + ANONYMOUS { + Bandwidth getLimit() { + return Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1))); + } + }, + FREE { + Bandwidth getLimit() { + return Bandwidth.classic(60, Refill.intervally(60, Duration.ofMinutes(1))); + } + }, + INTERNAL { + Bandwidth getLimit() { + return Bandwidth.classic(6000, Refill.intervally(6000, Duration.ofMinutes(1))); + } + }; + + https://www.baeldung.com/spring-bucket4j + https://blog.hubspot.com/website/api-keys +} diff --git a/user-module/src/main/java/io/edpn/backend/user/domain/model/UserRole.java b/user-module/src/main/java/io/edpn/backend/user/domain/model/UserRole.java new file mode 100644 index 00000000..b08f6ffb --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/domain/model/UserRole.java @@ -0,0 +1,13 @@ +package io.edpn.backend.user.domain.model; + +import java.util.Set; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class UserRole { + + String name; + Set grants; +} diff --git a/user-module/src/main/java/io/edpn/backend/user/domain/repository/UserRepository.java b/user-module/src/main/java/io/edpn/backend/user/domain/repository/UserRepository.java new file mode 100644 index 00000000..9490536e --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/domain/repository/UserRepository.java @@ -0,0 +1,8 @@ +package io.edpn.backend.user.domain.repository; + +import io.edpn.backend.user.domain.model.EdpnUser; +import java.util.Optional; + +public interface UserRepository { + Optional findByUsername(String email); +} diff --git a/user-module/src/main/java/io/edpn/backend/user/domain/service/JwtTokenService.java b/user-module/src/main/java/io/edpn/backend/user/domain/service/JwtTokenService.java new file mode 100644 index 00000000..6fb6c858 --- /dev/null +++ b/user-module/src/main/java/io/edpn/backend/user/domain/service/JwtTokenService.java @@ -0,0 +1,29 @@ +package io.edpn.backend.user.domain.service; + +import io.jsonwebtoken.Claims; +import java.util.Date; +import java.util.Map; +import java.util.function.Function; +import org.springframework.security.core.userdetails.UserDetails; + +public interface JwtTokenService { + String extractUsername(String token); + + Date extractExpiration(String token); + + T extractClaim(String token, Function claimsResolver); + + Claims extractAllClaims(String token); + + Boolean isTokenExpired(String token); + + String generateToken(UserDetails userDetails); + + String createToken(Map claims, String subject); + + Boolean validateToken(String token, UserDetails userDetails); + + String generateRefreshToken(UserDetails userDetails); + + String createRefreshToken(Map claims, String subject); +} diff --git a/user-module/src/main/java/module-info.java b/user-module/src/main/java/module-info.java new file mode 100644 index 00000000..407e4467 --- /dev/null +++ b/user-module/src/main/java/module-info.java @@ -0,0 +1,21 @@ +module io.edpn.backend.user { + requires static lombok; + requires spring.boot; + requires spring.boot.autoconfigure; + requires spring.context; + requires spring.jdbc; + requires spring.tx; + requires java.sql; + requires org.slf4j; + requires org.mybatis.spring; + requires spring.beans; + requires org.mybatis; + requires io.jsonwebtoken; + requires jakarta.servlet; + requires spring.boot.starter.security; + + exports io.edpn.backend.user.application.controller; + exports io.edpn.backend.user.application.dto; + exports io.edpn.backend.user.application.service; + exports io.edpn.backend.user.domain.service; +}